Bug 1036275 - Add Packaged App Service r=honzab

This commit is contained in:
Valentin Gosu 2015-06-03 01:46:15 +03:00
parent 332b9471a3
commit b2e6518351
9 changed files with 1061 additions and 0 deletions

View File

@ -67,6 +67,7 @@ XPIDL_SOURCES += [
'nsINSSErrorsService.idl',
'nsINullChannel.idl',
'nsIPACGenerator.idl',
'nsIPackagedAppService.idl',
'nsIParentChannel.idl',
'nsIParentRedirectingChannel.idl',
'nsIPermission.idl',

View File

@ -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);
};

View File

@ -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

View File

@ -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 },

View File

@ -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<CacheEntryWriter> 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<CacheEntryWriter*>(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<nsIResponseHeadProvider> 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<nsICacheStorageService> 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<nsIURI> 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<nsIResponseHeadProvider> provider(do_QueryInterface(aRequest));
nsCOMPtr<nsIChannel> 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<nsIURI> 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<nsIURI> 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<nsIMultiPartChannel> 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<nsIURI> uri;
rv = GetSubresourceURI(aRequest, getter_AddRefs(uri));
if (NS_WARN_IF(NS_FAILED(rv))) {
return NS_OK;
}
nsCOMPtr<nsICacheEntry> 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<PackagedAppDownloader> 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<nsICacheEntryOpenCallback>* 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<nsICacheEntryOpenCallback>* newArray =
new nsCOMArray<nsICacheEntryOpenCallback>();
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<nsICacheEntry> handle(aEntry);
nsAutoCString spec;
aURI->GetSpec(spec);
nsCOMArray<nsICacheEntryOpenCallback>* array = mCallbacks.Get(spec);
if (array) {
// Call all the callbacks for this URI
for (uint32_t i = 0; i < array->Length(); ++i) {
nsCOMPtr<nsICacheEntryOpenCallback> 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<nsCOMArray<nsICacheEntryOpenCallback> >& callbackArray,
void* arg)
{
MOZ_ASSERT(arg, "The void* parameter should be a pointer to nsresult");
nsresult *result = static_cast<nsresult*>(arg);
for (uint32_t i = 0; i < callbackArray->Length(); ++i) {
nsCOMPtr<nsICacheEntryOpenCallback> 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<nsICacheStorageService> cacheStorageService =
do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
nsCOMPtr<nsICacheStorage> cacheStorage;
rv = cacheStorageService->DiskCacheStorage(aInfo, false,
getter_AddRefs(cacheStorage));
nsRefPtr<CacheEntryChecker> 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<nsIURI> 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<PackagedAppDownloader> 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<nsIChannel> 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<nsICachingChannel> 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<nsIStreamConverterService> streamconv =
do_GetService("@mozilla.org/streamConverters;1", &rv);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
nsCOMPtr<nsIStreamListener> 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

View File

@ -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<nsICacheEntry> 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<nsIOutputStream> 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<nsCOMArray<nsICacheEntryOpenCallback>>& 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<CacheEntryWriter> mWriter;
// Cached value of nsICacheStorage
nsCOMPtr<nsICacheStorage> 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<nsCStringHashKey, nsCOMArray<nsICacheEntryOpenCallback>> 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<nsIURI> mURI;
nsCOMPtr<nsICacheEntryOpenCallback> mCallback;
nsCOMPtr<nsILoadContextInfo> 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<nsCStringHashKey, PackagedAppDownloader> mDownloadingPackages;
};
} // namespace net
} // namespace mozilla
#endif // mozilla_net_PackagedAppService_h

View File

@ -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',

View File

@ -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 <package that does not exist>
// calls the listener with an error code
// test_file_does_not_exist
// - checks that requesting a <subresource that doesn't exist> 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: "<html>\r\n <head>\r\n <script src=\"/scripts/app.js\"></script>\r\n ...\r\n </head>\r\n ...\r\n</html>\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,"<html>\r\n <head>\r\n <script src=\"/scripts/app.js\"></script>\r\n ...\r\n </head>\r\n ...\r\n</html>\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: "<html>\r\n <head>\r\n <script src=\"/scripts/app.js\"></script>\r\n ...\r\n </head>\r\n ...\r\n</html>\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);
}
// ----------------------------------------------------------------------------

View File

@ -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]