diff --git a/netwerk/base/moz.build b/netwerk/base/moz.build index 49d716235edd..7882e827cf51 100644 --- a/netwerk/base/moz.build +++ b/netwerk/base/moz.build @@ -67,6 +67,7 @@ XPIDL_SOURCES += [ 'nsINSSErrorsService.idl', 'nsINullChannel.idl', 'nsIPACGenerator.idl', + 'nsIPackagedAppService.idl', 'nsIParentChannel.idl', 'nsIParentRedirectingChannel.idl', 'nsIPermission.idl', diff --git a/netwerk/base/nsIPackagedAppService.idl b/netwerk/base/nsIPackagedAppService.idl new file mode 100644 index 000000000000..fb2112f35b46 --- /dev/null +++ b/netwerk/base/nsIPackagedAppService.idl @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIURI; +interface nsILoadContextInfo; +interface nsICacheEntryOpenCallback; + +%{C++ + #define PACKAGED_APP_TOKEN "!//" +%} + +/** + * nsIPackagedAppService + */ +[scriptable, builtinclass, uuid(77f9a34d-d082-43f1-9f83-e852d0173cd5)] +interface nsIPackagedAppService : nsISupports +{ + /** + * @aURI is a URL to a packaged resource + * - format: package_url + PACKAGED_APP_TOKEN + resource_path + * - example: http://test.com/path/to/package!//resource.html + * @aCallback is an object implementing nsICacheEntryOpenCallback + * - this is the target of the async result of the operation + * - aCallback->OnCacheEntryCheck() is called to verify the entry is valid + * - aCallback->OnCacheEntryAvailable() is called with a pointer to the + * the cached entry, if one exists, or an error code otherwise + * - aCallback is kept alive using an nsCOMPtr until OnCacheEntryAvailable + * is called + * @aInfo is an object used to determine the cache jar this resource goes in. + * - usually created by calling GetLoadContextInfo(requestingChannel) + * + * Calling this method will either download the package containing the given + * resource URI, store it in the cache and pass the cache entry to aCallback, + * or if that resource has already been downloaded it will be served from + * the cache. + */ + void requestURI(in nsIURI aURI, in nsILoadContextInfo aInfo, in nsICacheEntryOpenCallback aCallback); +}; diff --git a/netwerk/build/nsNetCID.h b/netwerk/build/nsNetCID.h index 90ff3cf60476..66ebaf42ce4e 100644 --- a/netwerk/build/nsNetCID.h +++ b/netwerk/build/nsNetCID.h @@ -868,6 +868,16 @@ { 0x85, 0x44, 0x5a, 0x8d, 0x1a, 0xb7, 0x95, 0x37 } \ } +#define NS_PACKAGEDAPPSERVICE_CONTRACTID \ + "@mozilla.org/network/packaged-app-service;1" +#define NS_PACKAGEDAPPSERVICE_CID \ +{ /* adef6762-41b9-4470-a06a-dc29cf8de381 */ \ + 0xadef6762, \ + 0x41b9, \ + 0x4470, \ + { 0xa0, 0x6a, 0xdc, 0x29, 0xcf, 0x8d, 0xe3, 0x81 } \ +} + /****************************************************************************** * netwerk/cookie classes diff --git a/netwerk/build/nsNetModule.cpp b/netwerk/build/nsNetModule.cpp index 568d5152288d..258bdea8ee7e 100644 --- a/netwerk/build/nsNetModule.cpp +++ b/netwerk/build/nsNetModule.cpp @@ -251,9 +251,11 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsHttpDigestAuth) #endif // !NECKO_PROTOCOL_http #include "mozilla/net/Dashboard.h" +#include "mozilla/net/PackagedAppService.h" namespace mozilla { namespace net { NS_GENERIC_FACTORY_CONSTRUCTOR(Dashboard) + NS_GENERIC_FACTORY_CONSTRUCTOR(PackagedAppService) } } #include "AppProtocolHandler.h" @@ -709,6 +711,7 @@ NS_DEFINE_NAMED_CID(NS_BUFFEREDOUTPUTSTREAM_CID); NS_DEFINE_NAMED_CID(NS_MIMEINPUTSTREAM_CID); NS_DEFINE_NAMED_CID(NS_PROTOCOLPROXYSERVICE_CID); NS_DEFINE_NAMED_CID(NS_STREAMCONVERTERSERVICE_CID); +NS_DEFINE_NAMED_CID(NS_PACKAGEDAPPSERVICE_CID); NS_DEFINE_NAMED_CID(NS_DASHBOARD_CID); #ifdef NECKO_PROTOCOL_ftp NS_DEFINE_NAMED_CID(NS_FTPDIRLISTINGCONVERTER_CID); @@ -853,6 +856,7 @@ static const mozilla::Module::CIDEntry kNeckoCIDs[] = { { &kNS_MIMEINPUTSTREAM_CID, false, nullptr, nsMIMEInputStreamConstructor }, { &kNS_PROTOCOLPROXYSERVICE_CID, true, nullptr, nsProtocolProxyServiceConstructor }, { &kNS_STREAMCONVERTERSERVICE_CID, false, nullptr, CreateNewStreamConvServiceFactory }, + { &kNS_PACKAGEDAPPSERVICE_CID, false, NULL, mozilla::net::PackagedAppServiceConstructor }, { &kNS_DASHBOARD_CID, false, nullptr, mozilla::net::DashboardConstructor }, #ifdef NECKO_PROTOCOL_ftp { &kNS_FTPDIRLISTINGCONVERTER_CID, false, nullptr, CreateNewFTPDirListingConv }, @@ -999,6 +1003,7 @@ static const mozilla::Module::ContractIDEntry kNeckoContracts[] = { { NS_MIMEINPUTSTREAM_CONTRACTID, &kNS_MIMEINPUTSTREAM_CID }, { NS_PROTOCOLPROXYSERVICE_CONTRACTID, &kNS_PROTOCOLPROXYSERVICE_CID }, { NS_STREAMCONVERTERSERVICE_CONTRACTID, &kNS_STREAMCONVERTERSERVICE_CID }, + { NS_PACKAGEDAPPSERVICE_CONTRACTID, &kNS_PACKAGEDAPPSERVICE_CID }, { NS_DASHBOARD_CONTRACTID, &kNS_DASHBOARD_CID }, #ifdef NECKO_PROTOCOL_ftp { NS_ISTREAMCONVERTER_KEY FTP_TO_INDEX, &kNS_FTPDIRLISTINGCONVERTER_CID }, diff --git a/netwerk/protocol/http/PackagedAppService.cpp b/netwerk/protocol/http/PackagedAppService.cpp new file mode 100644 index 000000000000..852facf99c21 --- /dev/null +++ b/netwerk/protocol/http/PackagedAppService.cpp @@ -0,0 +1,554 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +#include "PackagedAppService.h" +#include "nsICacheStorage.h" +#include "LoadContextInfo.h" +#include "nsICacheStorageService.h" +#include "nsIResponseHeadProvider.h" +#include "nsIMultiPartChannel.h" +#include "../../cache2/CacheFileUtils.h" +#include "nsStreamUtils.h" + +namespace mozilla { +namespace net { + +static PackagedAppService *gPackagedAppService = nullptr; + +NS_IMPL_ISUPPORTS(PackagedAppService, nsIPackagedAppService) + +NS_IMPL_ISUPPORTS(PackagedAppService::CacheEntryWriter, nsIStreamListener) + +/* static */ nsresult +PackagedAppService::CacheEntryWriter::Create(nsIURI *aURI, + nsICacheStorage *aStorage, + CacheEntryWriter **aResult) +{ + nsRefPtr writer = new CacheEntryWriter(); + nsresult rv = aStorage->OpenTruncate(aURI, EmptyCString(), + getter_AddRefs(writer->mEntry)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = writer->mEntry->ForceValidFor(PR_UINT32_MAX); + if (NS_FAILED(rv)) { + return rv; + } + + writer.forget(aResult); + return NS_OK; +} + +NS_METHOD +PackagedAppService::CacheEntryWriter::ConsumeData(nsIInputStream *aStream, + void *aClosure, + const char *aFromRawSegment, + uint32_t aToOffset, + uint32_t aCount, + uint32_t *aWriteCount) +{ + MOZ_ASSERT(aClosure, "The closure must not be null"); + CacheEntryWriter *self = static_cast(aClosure); + MOZ_ASSERT(self->mOutputStream, "The stream should not be null"); + return self->mOutputStream->Write(aFromRawSegment, aCount, aWriteCount); +} + +NS_IMETHODIMP +PackagedAppService::CacheEntryWriter::OnStartRequest(nsIRequest *aRequest, + nsISupports *aContext) +{ + nsCOMPtr provider(do_QueryInterface(aRequest)); + if (!provider) { + return NS_ERROR_INVALID_ARG; + } + nsHttpResponseHead *responseHead = provider->GetResponseHead(); + if (!responseHead) { + return NS_ERROR_FAILURE; + } + + mEntry->SetPredictedDataSize(responseHead->TotalEntitySize()); + + nsAutoCString head; + responseHead->Flatten(head, true); + nsresult rv = mEntry->SetMetaDataElement("response-head", head.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mEntry->SetMetaDataElement("request-method", "GET"); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mEntry->OpenOutputStream(0, getter_AddRefs(mOutputStream)); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +PackagedAppService::CacheEntryWriter::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aStatusCode) +{ + if (mOutputStream) { + mOutputStream->Close(); + mOutputStream = nullptr; + } + + return NS_OK; +} + +NS_IMETHODIMP +PackagedAppService::CacheEntryWriter::OnDataAvailable(nsIRequest *aRequest, + nsISupports *aContext, + nsIInputStream *aInputStream, + uint64_t aOffset, + uint32_t aCount) +{ + if (!aInputStream) { + return NS_ERROR_INVALID_ARG; + } + // Calls ConsumeData to read the data into the cache entry + uint32_t n; + return aInputStream->ReadSegments(ConsumeData, this, aCount, &n); +} + + +NS_IMPL_ISUPPORTS(PackagedAppService::PackagedAppDownloader, nsIStreamListener) + +nsresult +PackagedAppService::PackagedAppDownloader::Init(nsILoadContextInfo* aInfo, + const nsCString& aKey) +{ + nsresult rv; + nsCOMPtr cacheStorageService = + do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + rv = cacheStorageService->DiskCacheStorage(aInfo, false, + getter_AddRefs(mCacheStorage)); + if (NS_FAILED(rv)) { + return rv; + } + + mPackageKey = aKey; + return NS_OK; +} + +NS_IMETHODIMP +PackagedAppService::PackagedAppDownloader::OnStartRequest(nsIRequest *aRequest, + nsISupports *aContext) +{ + // In case an error occurs in this method mWriter should be null + // so we don't accidentally write to the previous resource's cache entry. + mWriter = nullptr; + + nsCOMPtr uri; + nsresult rv = GetSubresourceURI(aRequest, getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + rv = CacheEntryWriter::Create(uri, mCacheStorage, getter_AddRefs(mWriter)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + MOZ_ASSERT(mWriter); + rv = mWriter->OnStartRequest(aRequest, aContext); + NS_WARN_IF(NS_FAILED(rv)); + return NS_OK; +} + +nsresult +PackagedAppService::PackagedAppDownloader::GetSubresourceURI(nsIRequest * aRequest, + nsIURI ** aResult) +{ + nsresult rv; + nsCOMPtr provider(do_QueryInterface(aRequest)); + nsCOMPtr chan(do_QueryInterface(aRequest)); + + if (NS_WARN_IF(!provider || !chan)) { + return NS_ERROR_INVALID_ARG; + } + + nsHttpResponseHead *responseHead = provider->GetResponseHead(); + if (NS_WARN_IF(!responseHead)) { + return NS_ERROR_FAILURE; + } + nsAutoCString contentLocation; + rv = responseHead->GetHeader(nsHttp::ResolveAtom("Content-Location"), contentLocation); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr uri; + rv = chan->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString path; + rv = uri->GetPath(path); + if (NS_FAILED(rv)) { + return rv; + } + + path += PACKAGED_APP_TOKEN; + + // TODO: make sure the path is normalized + if (StringBeginsWith(contentLocation, NS_LITERAL_CSTRING("/"))) { + contentLocation = Substring(contentLocation, 1); + } + + path += contentLocation; + + nsCOMPtr partURI; + rv = uri->CloneIgnoringRef(getter_AddRefs(partURI)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = partURI->SetPath(path); + if (NS_FAILED(rv)) { + return rv; + } + + partURI.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +PackagedAppService::PackagedAppDownloader::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aStatusCode) +{ + nsCOMPtr multiChannel(do_QueryInterface(aRequest)); + nsresult rv; + + + // The request is normally a multiPartChannel. If it isn't, it generally means + // an error has occurred in nsMultiMixedConv. + // If an error occurred in OnStartRequest, mWriter could be null. + if (multiChannel && mWriter) { + mWriter->OnStopRequest(aRequest, aContext, aStatusCode); + + nsCOMPtr uri; + rv = GetSubresourceURI(aRequest, getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + nsCOMPtr entry; + mWriter->mEntry.swap(entry); + + // We don't need the writer anymore - this will close its stream + mWriter = nullptr; + CallCallbacks(uri, entry, aStatusCode); + } + + bool lastPart = false; + if (multiChannel) { + rv = multiChannel->GetIsLastPart(&lastPart); + if (NS_SUCCEEDED(rv) && !lastPart) { + // If this isn't the last part, we don't do the cleanup yet + return NS_OK; + } + } + + // If this is the last part of the package, it means the requested resources + // have not been found in the package so we return an appropriate error. + if (NS_SUCCEEDED(aStatusCode) && lastPart) { + aStatusCode = NS_ERROR_FILE_NOT_FOUND; + } + + nsRefPtr kungFuDeathGrip(this); + // NotifyPackageDownloaded removes the ref from the array. Keep a temp ref + if (gPackagedAppService) { + gPackagedAppService->NotifyPackageDownloaded(mPackageKey); + } + ClearCallbacks(aStatusCode); + return NS_OK; +} + +NS_IMETHODIMP +PackagedAppService::PackagedAppDownloader::OnDataAvailable(nsIRequest *aRequest, + nsISupports *aContext, + nsIInputStream *aInputStream, + uint64_t aOffset, + uint32_t aCount) +{ + if (!mWriter) { + uint32_t n; + return aInputStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &n); + } + return mWriter->OnDataAvailable(aRequest, aContext, aInputStream, aOffset, + aCount); +} + +nsresult +PackagedAppService::PackagedAppDownloader::AddCallback(nsIURI *aURI, + nsICacheEntryOpenCallback *aCallback) +{ + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "mCallbacks hashtable is not thread safe"); + nsAutoCString spec; + aURI->GetAsciiSpec(spec); + + // Check if we already have a resource waiting for this resource + nsCOMArray* array = mCallbacks.Get(spec); + if (array) { + // Add this resource to the callback array + array->AppendObject(aCallback); + } else { + // This is the first callback for this URI. + // Create a new array and add the callback + nsCOMArray* newArray = + new nsCOMArray(); + newArray->AppendObject(aCallback); + mCallbacks.Put(spec, newArray); + } + return NS_OK; +} + +nsresult +PackagedAppService::PackagedAppDownloader::CallCallbacks(nsIURI *aURI, + nsICacheEntry *aEntry, + nsresult aResult) +{ + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "mCallbacks hashtable is not thread safe"); + // Hold on to this entry while calling the callbacks + nsCOMPtr handle(aEntry); + + nsAutoCString spec; + aURI->GetSpec(spec); + + nsCOMArray* array = mCallbacks.Get(spec); + if (array) { + // Call all the callbacks for this URI + for (uint32_t i = 0; i < array->Length(); ++i) { + nsCOMPtr callback(array->ObjectAt(i)); + // We call to AsyncOpenURI which automatically calls the callback. + mCacheStorage->AsyncOpenURI(aURI, EmptyCString(), + nsICacheStorage::OPEN_READONLY, callback); + } + // Clear the array and remove it from the hashtable + array->Clear(); + mCallbacks.Remove(spec); + aEntry->ForceValidFor(0); + } + return NS_OK; +} + +PLDHashOperator +PackagedAppService::PackagedAppDownloader::ClearCallbacksEnumerator(const nsACString& key, + nsAutoPtr >& callbackArray, + void* arg) +{ + MOZ_ASSERT(arg, "The void* parameter should be a pointer to nsresult"); + nsresult *result = static_cast(arg); + for (uint32_t i = 0; i < callbackArray->Length(); ++i) { + nsCOMPtr callback = callbackArray->ObjectAt(i); + callback->OnCacheEntryAvailable(nullptr, false, nullptr, *result); + } + // Remove entry from hashtable + return PL_DHASH_REMOVE; +} + +nsresult +PackagedAppService::PackagedAppDownloader::ClearCallbacks(nsresult aResult) +{ + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "mCallbacks hashtable is not thread safe"); + mCallbacks.Enumerate(ClearCallbacksEnumerator, &aResult); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(PackagedAppService::CacheEntryChecker, nsICacheEntryOpenCallback) + +NS_IMETHODIMP +PackagedAppService::CacheEntryChecker::OnCacheEntryCheck(nsICacheEntry *aEntry, + nsIApplicationCache *aApplicationCache, + uint32_t *_retval) +{ + return mCallback->OnCacheEntryCheck(aEntry, aApplicationCache, _retval); +} + +NS_IMETHODIMP +PackagedAppService::CacheEntryChecker::OnCacheEntryAvailable(nsICacheEntry *aEntry, + bool aNew, + nsIApplicationCache *aApplicationCache, + nsresult aResult) +{ + if (aResult == NS_ERROR_CACHE_KEY_NOT_FOUND) { + MOZ_ASSERT(!aEntry, "No entry"); + // trigger download + // download checks if package download is already in progress + gPackagedAppService->OpenNewPackageInternal(mURI, mCallback, + mLoadContextInfo); + } else { + // TODO: if aResult is another error code, should we pass it off to the + // consumer, or should we try to download the package again? + mCallback->OnCacheEntryAvailable(aEntry, aNew, aApplicationCache, aResult); + // TODO: update last access entry for the entire package + } + return NS_OK; +} + +PackagedAppService::PackagedAppService() +{ + gPackagedAppService = this; +} + +PackagedAppService::~PackagedAppService() +{ + gPackagedAppService = nullptr; +} + +NS_IMETHODIMP +PackagedAppService::RequestURI(nsIURI *aURI, + nsILoadContextInfo *aInfo, + nsICacheEntryOpenCallback *aCallback) +{ + // Check arguments are not null + if (!aURI || !aCallback || !aInfo) { + return NS_ERROR_INVALID_ARG; + } + + nsAutoCString path; + aURI->GetPath(path); + int32_t pos = path.Find(PACKAGED_APP_TOKEN); + if (pos == kNotFound) { + return NS_ERROR_INVALID_ARG; + } + + nsresult rv; + nsCOMPtr cacheStorageService = + do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr cacheStorage; + + rv = cacheStorageService->DiskCacheStorage(aInfo, false, + getter_AddRefs(cacheStorage)); + + nsRefPtr checker = new CacheEntryChecker(aURI, aCallback, aInfo); + return cacheStorage->AsyncOpenURI(aURI, EmptyCString(), + nsICacheStorage::OPEN_READONLY, checker); +} + +nsresult +PackagedAppService::NotifyPackageDownloaded(nsCString aKey) +{ + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "mDownloadingPackages hashtable is not thread safe"); + mDownloadingPackages.Remove(aKey); + return NS_OK; +} + +nsresult +PackagedAppService::OpenNewPackageInternal(nsIURI *aURI, + nsICacheEntryOpenCallback *aCallback, + nsILoadContextInfo *aInfo) +{ + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "mDownloadingPackages hashtable is not thread safe"); + + nsAutoCString path; + nsresult rv = aURI->GetPath(path); + if (NS_FAILED(rv)) { + return rv; + } + + int32_t pos = path.Find(PACKAGED_APP_TOKEN); + MOZ_ASSERT(pos != kNotFound, + "This should never be called if the token is missing"); + + nsCOMPtr packageURI; + rv = aURI->CloneIgnoringRef(getter_AddRefs(packageURI)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = packageURI->SetPath(Substring(path, 0, pos)); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString key; + CacheFileUtils::AppendKeyPrefix(aInfo, key); + + { + nsAutoCString spec; + packageURI->GetAsciiSpec(spec); + key += ":"; + key += spec; + } + + nsRefPtr downloader; + if (mDownloadingPackages.Get(key, getter_AddRefs(downloader))) { + // We have determined that the file is not in the cache. + // If we find that the package that the file belongs to is currently being + // downloaded, we will add the callback to the package's queue, and it will + // be called once the file is processed and saved in the cache. + + downloader->AddCallback(aURI, aCallback); + return NS_OK; + } + + nsCOMPtr channel; + rv = NS_NewChannel( + getter_AddRefs(channel), packageURI, nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_NORMAL, nsIContentPolicy::TYPE_OTHER, nullptr, nullptr, + nsIRequest::LOAD_NORMAL); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr cacheChan(do_QueryInterface(channel)); + if (cacheChan) { + // Each resource in the package will be put in its own cache entry + // during the first load of the package, so we only want the channel to + // cache the response head, not the entire content of the package. + cacheChan->SetCacheOnlyMetadata(true); + } + + downloader = new PackagedAppDownloader(); + rv = downloader->Init(aInfo, key); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + downloader->AddCallback(aURI, aCallback); + + nsCOMPtr streamconv = + do_GetService("@mozilla.org/streamConverters;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr mimeConverter; + rv = streamconv->AsyncConvertData("multipart/mixed", "*/*", downloader, nullptr, + getter_AddRefs(mimeConverter)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Add the package to the hashtable. + mDownloadingPackages.Put(key, downloader); + + return channel->AsyncOpen(mimeConverter, nullptr); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PackagedAppService.h b/netwerk/protocol/http/PackagedAppService.h new file mode 100644 index 000000000000..a91a34b25dc4 --- /dev/null +++ b/netwerk/protocol/http/PackagedAppService.h @@ -0,0 +1,191 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_net_PackagedAppService_h +#define mozilla_net_PackagedAppService_h + +#include "nsIPackagedAppService.h" +#include "nsILoadContextInfo.h" +#include "nsICacheStorage.h" + +namespace mozilla { +namespace net { + +// This service is used to download packages from the web. +// Individual resources in the package are saved in the browser cache. It also +// provides an interface to asynchronously request resources from packages, +// which are either returned from the cache if they exist and are valid, +// or downloads the package. +// The package format is defined at: +// https://w3ctag.github.io/packaging-on-the-web/#streamable-package-format +// Downloading the package is triggered by calling requestURI(aURI, aInfo, aCallback) +// aURI is the subresource uri - http://domain.com/path/package!//resource.html +// aInfo is a nsILoadContextInfo used to pick the cache jar the resource goes into +// aCallback is the target of the async call to requestURI +// When requestURI is called, a CacheEntryChecker is created to verify if the +// resource is already in the cache. If it is, it passes it to the callback. +// Otherwise, it starts downloading the package. When the packaged resource has +// been downloaded, its cache entry gets passed to the callback. +class PackagedAppService final + : public nsIPackagedAppService +{ + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPACKAGEDAPPSERVICE + + PackagedAppService(); + +private: + ~PackagedAppService(); + + // This method is called if an entry wasn't found in the cache. + // It checks to see if the package is currently being downloaded. + // If so, then it simply adds the callback to that PackageAppDownloader + // Else it begins downloading the new package and adds it to mDownloadingPackages + // - aURI is the packaged resource's URL + // - aCallback is the listener which gets called when the requested + // resource is available. + // - aInfo is needed because cache entries are located in separate cache jars + // If a resource isn't found in the package, aCallback->OnCacheEntryAvailable + // will be called with a null entry and an error result as a status. + nsresult OpenNewPackageInternal(nsIURI *aURI, + nsICacheEntryOpenCallback *aCallback, + nsILoadContextInfo *aInfo); + + // Called by PackageAppDownloader once the download has finished + // (or encountered an error) to remove the package from mDownloadingPackages + // Should be called on the main thread. + nsresult NotifyPackageDownloaded(nsCString aKey); + + // This class is used to write data into the cache entry corresponding to the + // packaged resource being downloaded. + // The PackagedAppDownloader will hold a ref to a CacheEntryWriter that + // corresponds to the entry that is currently being downloaded. + class CacheEntryWriter final + : public nsIStreamListener + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + // If successful, calling this static method will create a new + // CacheEntryWriter and will create the cache entry associated to the + // resource and open an output stream which we use for writing the resource's + // content into the cache entry. + static nsresult Create(nsIURI*, nsICacheStorage*, CacheEntryWriter**); + + nsCOMPtr mEntry; + private: + CacheEntryWriter() { } + ~CacheEntryWriter() { } + + // Static method used to write data into the cache entry + // Called from OnDataAvailable + static NS_METHOD ConsumeData(nsIInputStream *in, void *closure, + const char *fromRawSegment, uint32_t toOffset, + uint32_t count, uint32_t *writeCount); + // We write the data we read from the network into this stream which goes + // to the cache entry. + nsCOMPtr mOutputStream; + }; + + // This class is used to download a packaged app. It acts as a listener + // for the nsMultiMixedConv object that parses the package. + // There is an OnStartRequest, OnDataAvailable*, OnStopRequest sequence called + // for each resource + // The PackagedAppService holds a hash-table of the PackagedAppDownloaders + // that are in progress to coalesce same loads. + // Once the downloading is completed, it should call + // NotifyPackageDownloaded(packageURI), so the service releases the ref. + class PackagedAppDownloader final + : public nsIStreamListener + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + // Initializes mCacheStorage and saves aKey as mPackageKey which is later + // used to remove this object from PackagedAppService::mDownloadingPackages + // - aKey is a string which uniquely identifies this package within the + // packagedAppService + nsresult Init(nsILoadContextInfo* aInfo, const nsCString &aKey); + // Registers a callback which gets called when the given nsIURI is downloaded + // aURI is the full URI of a subresource, composed of packageURI + !// + subresourcePath + nsresult AddCallback(nsIURI *aURI, nsICacheEntryOpenCallback *aCallback); + + private: + ~PackagedAppDownloader() { } + + // Calls all the callbacks registered for the given URI. + // aURI is the full URI of a subresource, composed of packageURI + !// + subresourcePath + // It passes the cache entry and the result when calling OnCacheEntryAvailable + nsresult CallCallbacks(nsIURI *aURI, nsICacheEntry *aEntry, nsresult aResult); + // Clears all the callbacks for this package + // This would get called at the end of downloading the package and would + // cause us to call OnCacheEntryAvailable with a null entry. This would be + // equivalent to a 404 when loading from the net. + nsresult ClearCallbacks(nsresult aResult); + static PLDHashOperator ClearCallbacksEnumerator(const nsACString& key, + nsAutoPtr>& callbackArray, + void* arg); + // Returns a URI with the subresource's full URI + // The request must be QIable to nsIResponseHeadProvider since it looks + // at the Content-Location header to compute the full path. + static nsresult GetSubresourceURI(nsIRequest * aRequest, nsIURI **aResult); + // Used to write data into the cache entry of the resource currently being + // downloaded. It is kept alive until the downloader receives OnStopRequest + nsRefPtr mWriter; + // Cached value of nsICacheStorage + nsCOMPtr mCacheStorage; + // A hastable containing all the consumers which requested a resource and need + // to be notified once it is inserted into the cache. + // The key is a subresource URI - http://example.com/package.pak!//res.html + // Should only be used on the main thread. + nsClassHashtable> mCallbacks; + // The key with which this package is inserted in + // PackagedAppService::mDownloadingPackages + nsCString mPackageKey; + }; + + // This class is used to check if a packaged resource has already been + // downloaded and saved into the cache. + // It calls aCallback->OnCacheEntryAvailable if the resource exists in the + // cache or PackagedAppService::OpenNewPackageInternal if it needs + // to be downloaded + class CacheEntryChecker final + : public nsICacheEntryOpenCallback + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICACHEENTRYOPENCALLBACK + + CacheEntryChecker(nsIURI *aURI, nsICacheEntryOpenCallback * aCallback, + nsILoadContextInfo *aInfo) + : mURI(aURI) + , mCallback(aCallback) + , mLoadContextInfo(aInfo) + { + } + private: + ~CacheEntryChecker() { } + + nsCOMPtr mURI; + nsCOMPtr mCallback; + nsCOMPtr mLoadContextInfo; + }; + + // A hashtable of packages that are currently being downloaded. + // The key is a string formed by concatenating LoadContextInfo and package URI + // Should only be used on the main thread. + nsRefPtrHashtable mDownloadingPackages; +}; + + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_PackagedAppService_h \ No newline at end of file diff --git a/netwerk/protocol/http/moz.build b/netwerk/protocol/http/moz.build index e770a015cd48..dcb67a0ce61c 100644 --- a/netwerk/protocol/http/moz.build +++ b/netwerk/protocol/http/moz.build @@ -34,6 +34,7 @@ EXPORTS.mozilla.net += [ 'HttpChannelParent.h', 'HttpInfo.h', 'NullHttpChannel.h', + 'PackagedAppService.h', 'PHttpChannelParams.h', 'PSpdyPush.h', 'TimingStruct.h', @@ -78,6 +79,7 @@ UNIFIED_SOURCES += [ 'nsHttpTransaction.cpp', 'NullHttpChannel.cpp', 'NullHttpTransaction.cpp', + 'PackagedAppService.cpp', 'SpdyPush31.cpp', 'SpdySession31.cpp', 'SpdyStream31.cpp', diff --git a/netwerk/test/unit/test_packaged_app_service.js b/netwerk/test/unit/test_packaged_app_service.js new file mode 100644 index 000000000000..0817144d68fc --- /dev/null +++ b/netwerk/test/unit/test_packaged_app_service.js @@ -0,0 +1,255 @@ +// +// This file tests the packaged app service - nsIPackagedAppService +// NOTE: The order in which tests are run is important +// If you need to add more tests, it's best to define them at the end +// of the file and to add them at the end of run_test +// +// ---------------------------------------------------------------------------- +// +// test_bad_args +// - checks that calls to nsIPackagedAppService::requestURI do not accept a null argument +// test_callback_gets_called +// - checks the regular use case -> requesting a resource should asynchronously return an entry +// test_same_content +// - makes another request for the same file, and checks that the same content is returned +// test_request_number +// - this test does not make a request, but checks that the package has only +// been requested once. The entry returned by the call to requestURI in +// test_same_content should be returned from the cache. +// +// test_package_does_not_exist +// - checks that requesting a file from a +// calls the listener with an error code +// test_file_does_not_exist +// - checks that requesting a inside a +// package calls the listener with an error code +// +// test_bad_package +// - tests that a package with missing headers for some of the files +// will still return files that are correct +// test_bad_package_404 +// - tests that a request for a missing subresource doesn't hang if +// if the last file in the package is missing some headers + +Cu.import('resource://gre/modules/LoadContextInfo.jsm'); +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); + +// The number of times this package has been requested +// This number might be reset by tests that use it +var packagedAppRequestsMade = 0; +// The default content handler. It just responds by sending the package data +// with an application/package content type +function packagedAppContentHandler(metadata, response) +{ + packagedAppRequestsMade++; + response.setHeader("Content-Type", 'application/package'); + var body = testData.getData(); + response.bodyOutputStream.write(body, body.length); +} + +// The package content +// getData formats it as described at http://www.w3.org/TR/web-packaging/#streamable-package-format +var testData = { + content: [ + { headers: ["Content-Location: /index.html", "Content-Type: text/html"], data: "\r\n \r\n \r\n ...\r\n \r\n ...\r\n\r\n", type: "text/html" }, + { headers: ["Content-Location: /scripts/app.js", "Content-Type: text/javascript"], data: "module Math from '/scripts/helpers/math.js';\r\n...\r\n", type: "text/javascript" }, + { headers: ["Content-Location: /scripts/helpers/math.js", "Content-Type: text/javascript"], data: "export function sum(nums) { ... }\r\n...\r\n", type: "text/javascript" } + ], + token : "gc0pJq0M:08jU534c0p", + getData: function() { + var str = ""; + for (var i in this.content) { + str += "--" + this.token + "\r\n"; + for (var j in this.content[i].headers) { + str += this.content[i].headers[j] + "\r\n"; + } + str += "\r\n"; + str += this.content[i].data + "\r\n"; + } + + str += "--" + this.token + "--"; + return str; + } +} + +XPCOMUtils.defineLazyGetter(this, "uri", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +// The active http server initialized in run_test +var httpserver = null; +// The packaged app service initialized in run_test +var paservice = null; +// This variable is set before requestURI is called. The listener uses this variable +// to check the correct resource path for the returned entry +var packagePath = null; + +function run_test() +{ + // setup test + httpserver = new HttpServer(); + httpserver.registerPathHandler("/package", packagedAppContentHandler); + httpserver.registerPathHandler("/304Package", packagedAppContentHandler); + httpserver.registerPathHandler("/badPackage", packagedAppBadContentHandler); + httpserver.start(-1); + + paservice = Cc["@mozilla.org/network/packaged-app-service;1"] + .getService(Ci.nsIPackagedAppService); + ok(!!paservice, "test service exists"); + + add_test(test_bad_args); + + add_test(test_callback_gets_called); + add_test(test_same_content); + add_test(test_request_number); + + add_test(test_package_does_not_exist); + add_test(test_file_does_not_exist); + + add_test(test_bad_package); + add_test(test_bad_package_404); + + // run tests + run_next_test(); +} + +// This checks the proper metadata is on the entry +var metadataListener = { + onMetaDataElement: function(key, value) { + if (key == 'response-head') + equal(value, "HTTP/1.1 200 \r\nContent-Location: /index.html\r\nContent-Type: text/html\r\n"); + else if (key == 'request-method') + equal(value, "GET"); + else + ok(false, "unexpected metadata key") + } +} + +// A listener we use to check the proper cache entry is returned by the service +// NOTE: this listener only checks the content of index.html +// Don't use it when requesting other packaged resources! :) +var cacheListener = { + onCacheEntryCheck: function() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; }, + onCacheEntryAvailable: function (entry, isnew, appcache, status) { + ok(!!entry, "Needs to have an entry"); + equal(status, Cr.NS_OK, "status is NS_OK"); + equal(entry.key, uri + packagePath + "!//index.html", "Check entry has correct name"); + entry.visitMetaData(metadataListener); + var inputStream = entry.openInputStream(0); + pumpReadStream(inputStream, function(read) { + inputStream.close(); + equal(read,"\r\n \r\n \r\n ...\r\n \r\n ...\r\n\r\n"); // not using do_check_eq since logger will fail for the 1/4MB string + }); + run_next_test(); + } +}; + +// ---------------------------------------------------------------------------- + +// These calls should fail, since one of the arguments is invalid or null +function test_bad_args() { + Assert.throws(() => { paservice.requestURI(createURI("http://test.com"), LoadContextInfo.default, cacheListener); }, "url's with no !// aren't allowed"); + Assert.throws(() => { paservice.requestURI(createURI("http://test.com/package!//test"), LoadContextInfo.default, null); }, "should have a callback"); + Assert.throws(() => { paservice.requestURI(null, LoadContextInfo.default, cacheListener); }, "should have a URI"); + Assert.throws(() => { paservice.requestURI(createURI("http://test.com/package!//test"), null, cacheListener); }, "should have a LoadContextInfo"); + run_next_test(); +} + +// ---------------------------------------------------------------------------- + +// This tests that the callback gets called, and the cacheListener gets the proper content. +function test_callback_gets_called() { + packagePath = "/package"; + paservice.requestURI(createURI(uri + packagePath + "!//index.html"), LoadContextInfo.default, cacheListener); +} + +// Tests that requesting the same resource returns the same content +function test_same_content() { + packagePath = "/package"; + paservice.requestURI(createURI(uri + packagePath + "!//index.html"), LoadContextInfo.default, cacheListener); +} + +// Check the package has only been requested once. +function test_request_number() { + equal(packagedAppRequestsMade, 1, "only one request should be made. Second should be loaded from cache"); + run_next_test(); +} + +// ---------------------------------------------------------------------------- + +// This listener checks that the requested resources are not returned +// either because the package does not exist, or because the requested resource +// is not contained in the package. +var listener404 = { + onCacheEntryCheck: function() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; }, + onCacheEntryAvailable: function (entry, isnew, appcache, status) { + // XXX: it returns NS_ERROR_FAILURE for a missing package + // and NS_ERROR_FILE_NOT_FOUND for a missing file from the package. + // Maybe make them both return NS_ERROR_FILE_NOT_FOUND? + notEqual(status, Cr.NS_OK, "NOT FOUND"); + ok(!entry, "There should be no entry"); + run_next_test(); + } +}; + +// Tests that an error is returned for a non existing package +function test_package_does_not_exist() { + packagePath = "/package_non_existent"; + paservice.requestURI(createURI(uri + packagePath + "!//index.html"), LoadContextInfo.default, listener404); +} + +// Tests that an error is returned for a non existing resource in a package +function test_file_does_not_exist() { + packagePath = "/package"; // This package exists + paservice.requestURI(createURI(uri + packagePath + "!//file_non_existent.html"), LoadContextInfo.default, listener404); +} + +// ---------------------------------------------------------------------------- + +// Broken package. The first and last resources do not contain a "Content-Location" header +// and should be ignored. +var badTestData = { + content: [ + { headers: ["Content-Type: text/javascript"], data: "module Math from '/scripts/helpers/math.js';\r\n...\r\n", type: "text/javascript" }, + { headers: ["Content-Location: /index.html", "Content-Type: text/html"], data: "\r\n \r\n \r\n ...\r\n \r\n ...\r\n\r\n", type: "text/html" }, + { headers: ["Content-Type: text/javascript"], data: "export function sum(nums) { ... }\r\n...\r\n", type: "text/javascript" } + ], + token : "gc0pJq0M:08jU534c0p", + getData: function() { + var str = ""; + for (var i in this.content) { + str += "--" + this.token + "\r\n"; + for (var j in this.content[i].headers) { + str += this.content[i].headers[j] + "\r\n"; + } + str += "\r\n"; + str += this.content[i].data + "\r\n"; + } + + str += "--" + this.token + "--"; + return str; + } +} + +// Returns the content of the package with "Content-Location" headers missing for the first and last resource +function packagedAppBadContentHandler(metadata, response) +{ + response.setHeader("Content-Type", 'application/package'); + var body = badTestData.getData(); + response.bodyOutputStream.write(body, body.length); +} + +// Checks that the resource with the proper headers inside the bad package is still returned +function test_bad_package() { + packagePath = "/badPackage"; + paservice.requestURI(createURI(uri + packagePath + "!//index.html"), LoadContextInfo.default, cacheListener); +} + +// Checks that the request for a non-existent resource doesn't hang for a bad package +function test_bad_package_404() { + packagePath = "/badPackage"; + paservice.requestURI(createURI(uri + packagePath + "!//file_non_existent.html"), LoadContextInfo.default, listener404); +} + +// ---------------------------------------------------------------------------- diff --git a/netwerk/test/unit/xpcshell.ini b/netwerk/test/unit/xpcshell.ini index 800f2b1fd62a..613b6e676223 100644 --- a/netwerk/test/unit/xpcshell.ini +++ b/netwerk/test/unit/xpcshell.ini @@ -316,3 +316,4 @@ skip-if = os == "android" [test_1073747.js] [test_multipart_streamconv_application_package.js] [test_safeoutputstream_append.js] +[test_packaged_app_service.js]