Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "nsJAR.h"
#include "nsJARChannel.h"
#include "nsJARProtocolHandler.h"
#include "nsMimeTypes.h"
#include "nsNetUtil.h"
#include "nsEscape.h"
#include "nsContentUtils.h"
#include "nsProxyRelease.h"
#include "nsContentSecurityManager.h"
#include "nsComponentManagerUtils.h"
#include "nsIFileURL.h"
#include "nsIURIMutator.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/IntegerPrintfMacros.h"
#include "mozilla/Preferences.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/StaticPrefs_network.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TelemetryComms.h"
#include "private/pprio.h"
#include "nsInputStreamPump.h"
#include "nsThreadUtils.h"
#include "nsJARProtocolHandler.h"
using namespace mozilla;
using namespace mozilla::net;
static NS_DEFINE_CID(kZipReaderCID, NS_ZIPREADER_CID);
// the entry for a directory will either be empty (in the case of the
// top-level directory) or will end with a slash
#define ENTRY_IS_DIRECTORY(_entry) \
((_entry).IsEmpty() || '/' == (_entry).Last())
//-----------------------------------------------------------------------------
//
// set MOZ_LOG=nsJarProtocol:5
//
static LazyLogModule gJarProtocolLog("nsJarProtocol");
// Ignore any LOG macro that we inherit from arbitrary headers. (We define our
// own LOG macro below.)
#ifdef LOG
# undef LOG
#endif
#ifdef LOG_ENABLED
# undef LOG_ENABLED
#endif
#define LOG(args) MOZ_LOG(gJarProtocolLog, mozilla::LogLevel::Debug, args)
#define LOG_ENABLED() MOZ_LOG_TEST(gJarProtocolLog, mozilla::LogLevel::Debug)
//-----------------------------------------------------------------------------
// nsJARInputThunk
//
// this class allows us to do some extra work on the stream transport thread.
//-----------------------------------------------------------------------------
class nsJARInputThunk : public nsIInputStream {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIINPUTSTREAM
nsJARInputThunk(nsIZipReader* zipReader, const nsACString& jarEntry,
bool usingJarCache)
: mUsingJarCache(usingJarCache),
mJarReader(zipReader),
mJarEntry(jarEntry),
mContentLength(-1) {
MOZ_DIAGNOSTIC_ASSERT(zipReader, "zipReader must not be null");
}
int64_t GetContentLength() { return mContentLength; }
nsresult Init();
private:
virtual ~nsJARInputThunk() { Close(); }
bool mUsingJarCache;
nsCOMPtr<nsIZipReader> mJarReader;
nsCOMPtr<nsIInputStream> mJarStream;
nsCString mJarEntry;
int64_t mContentLength;
};
NS_IMPL_ISUPPORTS(nsJARInputThunk, nsIInputStream)
nsresult nsJARInputThunk::Init() {
if (!mJarReader) {
return NS_ERROR_INVALID_ARG;
}
nsresult rv =
mJarReader->GetInputStream(mJarEntry, getter_AddRefs(mJarStream));
if (NS_FAILED(rv)) {
return rv;
}
// ask the JarStream for the content length
uint64_t avail;
rv = mJarStream->Available((uint64_t*)&avail);
if (NS_FAILED(rv)) return rv;
mContentLength = avail < INT64_MAX ? (int64_t)avail : -1;
return NS_OK;
}
NS_IMETHODIMP
nsJARInputThunk::Close() {
nsresult rv = NS_OK;
if (mJarStream) rv = mJarStream->Close();
if (!mUsingJarCache && mJarReader) mJarReader->Close();
mJarReader = nullptr;
return rv;
}
NS_IMETHODIMP
nsJARInputThunk::Available(uint64_t* avail) {
return mJarStream->Available(avail);
}
NS_IMETHODIMP
nsJARInputThunk::StreamStatus() { return mJarStream->StreamStatus(); }
NS_IMETHODIMP
nsJARInputThunk::Read(char* buf, uint32_t count, uint32_t* countRead) {
return mJarStream->Read(buf, count, countRead);
}
NS_IMETHODIMP
nsJARInputThunk::ReadSegments(nsWriteSegmentFun writer, void* closure,
uint32_t count, uint32_t* countRead) {
// stream transport does only calls Read()
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
nsJARInputThunk::IsNonBlocking(bool* nonBlocking) {
*nonBlocking = false;
return NS_OK;
}
//-----------------------------------------------------------------------------
// nsJARChannel
//-----------------------------------------------------------------------------
nsJARChannel::nsJARChannel()
: mOpened(false),
mCanceled(false),
mContentLength(-1),
mLoadFlags(LOAD_NORMAL),
mStatus(NS_OK),
mIsPending(false),
mEnableOMT(true),
mPendingEvent() {
LOG(("nsJARChannel::nsJARChannel [this=%p]\n", this));
// hold an owning reference to the jar handler
mJarHandler = gJarHandler;
}
nsJARChannel::~nsJARChannel() {
LOG(("nsJARChannel::~nsJARChannel [this=%p]\n", this));
if (NS_IsMainThread()) {
return;
}
// Proxy release the following members to main thread.
NS_ReleaseOnMainThread("nsJARChannel::mLoadInfo", mLoadInfo.forget());
NS_ReleaseOnMainThread("nsJARChannel::mCallbacks", mCallbacks.forget());
NS_ReleaseOnMainThread("nsJARChannel::mProgressSink", mProgressSink.forget());
NS_ReleaseOnMainThread("nsJARChannel::mLoadGroup", mLoadGroup.forget());
NS_ReleaseOnMainThread("nsJARChannel::mListener", mListener.forget());
}
NS_IMPL_ISUPPORTS_INHERITED(nsJARChannel, nsHashPropertyBag, nsIRequest,
nsIChannel, nsIStreamListener, nsIRequestObserver,
nsIThreadRetargetableRequest,
nsIThreadRetargetableStreamListener, nsIJARChannel)
nsresult nsJARChannel::Init(nsIURI* uri) {
LOG(("nsJARChannel::Init [this=%p]\n", this));
nsresult rv;
mWorker = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
if (NS_FAILED(rv)) {
return rv;
}
mJarURI = do_QueryInterface(uri, &rv);
if (NS_FAILED(rv)) return rv;
mOriginalURI = mJarURI;
// Prevent loading jar:javascript URIs (see bug 290982).
nsCOMPtr<nsIURI> innerURI;
rv = mJarURI->GetJARFile(getter_AddRefs(innerURI));
if (NS_FAILED(rv)) {
return rv;
}
if (innerURI->SchemeIs("javascript")) {
NS_WARNING("blocking jar:javascript:");
return NS_ERROR_INVALID_ARG;
}
mJarURI->GetSpec(mSpec);
return rv;
}
nsresult nsJARChannel::CreateJarInput(nsIZipReaderCache* jarCache,
nsJARInputThunk** resultInput) {
LOG(("nsJARChannel::CreateJarInput [this=%p]\n", this));
MOZ_ASSERT(resultInput);
MOZ_ASSERT(mJarFile);
// important to pass a clone of the file since the nsIFile impl is not
// necessarily MT-safe
nsCOMPtr<nsIFile> clonedFile;
nsresult rv = NS_OK;
if (mJarFile) {
rv = mJarFile->Clone(getter_AddRefs(clonedFile));
if (NS_FAILED(rv)) return rv;
}
nsCOMPtr<nsIZipReader> reader;
if (mPreCachedJarReader) {
reader = mPreCachedJarReader;
} else if (jarCache) {
if (mInnerJarEntry.IsEmpty())
rv = jarCache->GetZip(clonedFile, getter_AddRefs(reader));
else
rv = jarCache->GetInnerZip(clonedFile, mInnerJarEntry,
getter_AddRefs(reader));
} else {
// create an uncached jar reader
nsCOMPtr<nsIZipReader> outerReader = do_CreateInstance(kZipReaderCID, &rv);
if (NS_FAILED(rv)) return rv;
rv = outerReader->Open(clonedFile);
if (NS_FAILED(rv)) return rv;
if (mInnerJarEntry.IsEmpty())
reader = outerReader;
else {
reader = do_CreateInstance(kZipReaderCID, &rv);
if (NS_FAILED(rv)) return rv;
rv = reader->OpenInner(outerReader, mInnerJarEntry);
}
}
if (NS_FAILED(rv)) return rv;
RefPtr<nsJARInputThunk> input =
new nsJARInputThunk(reader, mJarEntry, jarCache != nullptr);
rv = input->Init();
if (NS_FAILED(rv)) {
return rv;
}
// Make GetContentLength meaningful
mContentLength = input->GetContentLength();
input.forget(resultInput);
return NS_OK;
}
nsresult nsJARChannel::LookupFile() {
LOG(("nsJARChannel::LookupFile [this=%p %s]\n", this, mSpec.get()));
if (mJarFile) return NS_OK;
nsresult rv;
rv = mJarURI->GetJARFile(getter_AddRefs(mJarBaseURI));
if (NS_FAILED(rv)) return rv;
rv = mJarURI->GetJAREntry(mJarEntry);
if (NS_FAILED(rv)) return rv;
// The name of the JAR entry must not contain URL-escaped characters:
// we're moving from URL domain to a filename domain here. nsStandardURL
// does basic escaping by default, which breaks reading zipped files which
// have e.g. spaces in their filenames.
NS_UnescapeURL(mJarEntry);
if (mJarFileOverride) {
mJarFile = mJarFileOverride;
LOG(("nsJARChannel::LookupFile [this=%p] Overriding mJarFile\n", this));
return NS_OK;
}
// try to get a nsIFile directly from the url, which will often succeed.
{
nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(mJarBaseURI);
if (fileURL) fileURL->GetFile(getter_AddRefs(mJarFile));
}
// try to handle a nested jar
if (!mJarFile) {
nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(mJarBaseURI);
if (jarURI) {
nsCOMPtr<nsIFileURL> fileURL;
nsCOMPtr<nsIURI> innerJarURI;
rv = jarURI->GetJARFile(getter_AddRefs(innerJarURI));
if (NS_SUCCEEDED(rv)) fileURL = do_QueryInterface(innerJarURI);
if (fileURL) {
fileURL->GetFile(getter_AddRefs(mJarFile));
jarURI->GetJAREntry(mInnerJarEntry);
}
}
}
return rv;
}
nsresult CreateLocalJarInput(nsIZipReaderCache* aJarCache, nsIFile* aFile,
const nsACString& aInnerJarEntry,
const nsACString& aJarEntry,
nsJARInputThunk** aResultInput) {
LOG(("nsJARChannel::CreateLocalJarInput [aJarCache=%p, %s, %s]\n", aJarCache,
PromiseFlatCString(aInnerJarEntry).get(),
PromiseFlatCString(aJarEntry).get()));
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(aJarCache);
MOZ_ASSERT(aResultInput);
nsresult rv;
nsCOMPtr<nsIZipReader> reader;
if (aInnerJarEntry.IsEmpty()) {
rv = aJarCache->GetZip(aFile, getter_AddRefs(reader));
} else {
rv = aJarCache->GetInnerZip(aFile, aInnerJarEntry, getter_AddRefs(reader));
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
RefPtr<nsJARInputThunk> input =
new nsJARInputThunk(reader, aJarEntry, aJarCache != nullptr);
rv = input->Init();
if (NS_FAILED(rv)) {
return rv;
}
input.forget(aResultInput);
return NS_OK;
}
nsresult nsJARChannel::OpenLocalFile() {
LOG(("nsJARChannel::OpenLocalFile [this=%p]\n", this));
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mWorker);
MOZ_ASSERT(mIsPending);
MOZ_ASSERT(mJarFile);
nsresult rv;
// Set mLoadGroup and mOpened before AsyncOpen return, and set back if
// if failed when callback.
if (mLoadGroup) {
mLoadGroup->AddRequest(this, nullptr);
}
SetOpened();
if (mPreCachedJarReader || !mEnableOMT) {
RefPtr<nsJARInputThunk> input;
rv = CreateJarInput(gJarHandler->JarCache(), getter_AddRefs(input));
if (NS_WARN_IF(NS_FAILED(rv))) {
return OnOpenLocalFileComplete(rv, true);
}
return ContinueOpenLocalFile(input, true);
}
nsCOMPtr<nsIZipReaderCache> jarCache = gJarHandler->JarCache();
if (NS_WARN_IF(!jarCache)) {
return NS_ERROR_UNEXPECTED;
}
nsCOMPtr<nsIFile> clonedFile;
rv = mJarFile->Clone(getter_AddRefs(clonedFile));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
nsAutoCString jarEntry(mJarEntry);
nsAutoCString innerJarEntry(mInnerJarEntry);
RefPtr<nsJARChannel> self = this;
return mWorker->Dispatch(NS_NewRunnableFunction(
"nsJARChannel::OpenLocalFile",
[self, jarCache, clonedFile, jarEntry, innerJarEntry]() mutable {
RefPtr<nsJARInputThunk> input;
nsresult rv = CreateLocalJarInput(jarCache, clonedFile, innerJarEntry,
jarEntry, getter_AddRefs(input));
nsCOMPtr<nsIRunnable> target;
if (NS_SUCCEEDED(rv)) {
target = NewRunnableMethod<RefPtr<nsJARInputThunk>, bool>(
"nsJARChannel::ContinueOpenLocalFile", self,
&nsJARChannel::ContinueOpenLocalFile, input, false);
} else {
target = NewRunnableMethod<nsresult, bool>(
"nsJARChannel::OnOpenLocalFileComplete", self,
&nsJARChannel::OnOpenLocalFileComplete, rv, false);
}
// nsJARChannel must be release on main thread, and sometimes
// this still hold nsJARChannel after dispatched.
self = nullptr;
NS_DispatchToMainThread(target.forget());
}));
}
nsresult nsJARChannel::ContinueOpenLocalFile(nsJARInputThunk* aInput,
bool aIsSyncCall) {
LOG(("nsJARChannel::ContinueOpenLocalFile [this=%p %p]\n", this, aInput));
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mIsPending);
// Make GetContentLength meaningful
mContentLength = aInput->GetContentLength();
nsresult rv;
RefPtr<nsJARInputThunk> input = aInput;
// Create input stream pump and call AsyncRead as a block.
rv = NS_NewInputStreamPump(getter_AddRefs(mPump), input.forget());
if (NS_SUCCEEDED(rv)) {
rv = mPump->AsyncRead(this);
}
if (NS_SUCCEEDED(rv)) {
rv = CheckPendingEvents();
}
return OnOpenLocalFileComplete(rv, aIsSyncCall);
}
nsresult nsJARChannel::OnOpenLocalFileComplete(nsresult aResult,
bool aIsSyncCall) {
LOG(("nsJARChannel::OnOpenLocalFileComplete [this=%p %08x]\n", this,
static_cast<uint32_t>(aResult)));
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mIsPending);
if (NS_FAILED(aResult)) {
if (aResult == NS_ERROR_FILE_NOT_FOUND) {
CheckForBrokenChromeURL(mLoadInfo, mOriginalURI);
}
if (!aIsSyncCall) {
NotifyError(aResult);
}
if (mLoadGroup) {
mLoadGroup->RemoveRequest(this, nullptr, aResult);
}
mOpened = false;
mIsPending = false;
mListener = nullptr;
mCallbacks = nullptr;
mProgressSink = nullptr;
return aResult;
}
return NS_OK;
}
nsresult nsJARChannel::CheckPendingEvents() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mIsPending);
MOZ_ASSERT(mPump);
nsresult rv;
uint32_t suspendCount = mPendingEvent.suspendCount;
while (suspendCount--) {
if (NS_WARN_IF(NS_FAILED(rv = mPump->Suspend()))) {
return rv;
}
}
if (mPendingEvent.isCanceled) {
if (NS_WARN_IF(NS_FAILED(rv = mPump->Cancel(mStatus)))) {
return rv;
}
mPendingEvent.isCanceled = false;
}
return NS_OK;
}
void nsJARChannel::NotifyError(nsresult aError) {
MOZ_ASSERT(NS_FAILED(aError));
mStatus = aError;
OnStartRequest(nullptr);
OnStopRequest(nullptr, aError);
}
void nsJARChannel::FireOnProgress(uint64_t aProgress) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mProgressSink);
mProgressSink->OnProgress(this, aProgress, mContentLength);
}
//-----------------------------------------------------------------------------
// nsIRequest
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsJARChannel::GetName(nsACString& result) { return mJarURI->GetSpec(result); }
NS_IMETHODIMP
nsJARChannel::IsPending(bool* result) {
*result = mIsPending;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetStatus(nsresult* status) {
if (mPump && NS_SUCCEEDED(mStatus))
mPump->GetStatus(status);
else
*status = mStatus;
return NS_OK;
}
NS_IMETHODIMP nsJARChannel::SetCanceledReason(const nsACString& aReason) {
return SetCanceledReasonImpl(aReason);
}
NS_IMETHODIMP nsJARChannel::GetCanceledReason(nsACString& aReason) {
return GetCanceledReasonImpl(aReason);
}
NS_IMETHODIMP nsJARChannel::CancelWithReason(nsresult aStatus,
const nsACString& aReason) {
return CancelWithReasonImpl(aStatus, aReason);
}
NS_IMETHODIMP
nsJARChannel::Cancel(nsresult status) {
mCanceled = true;
mStatus = status;
if (mPump) {
return mPump->Cancel(status);
}
if (mIsPending) {
mPendingEvent.isCanceled = true;
}
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetCanceled(bool* aCanceled) {
*aCanceled = mCanceled;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::Suspend() {
++mPendingEvent.suspendCount;
if (mPump) {
return mPump->Suspend();
}
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::Resume() {
if (NS_WARN_IF(mPendingEvent.suspendCount == 0)) {
return NS_ERROR_UNEXPECTED;
}
--mPendingEvent.suspendCount;
if (mPump) {
return mPump->Resume();
}
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) {
*aLoadFlags = mLoadFlags;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::SetLoadFlags(nsLoadFlags aLoadFlags) {
mLoadFlags = aLoadFlags;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
return GetTRRModeImpl(aTRRMode);
}
NS_IMETHODIMP
nsJARChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
return SetTRRModeImpl(aTRRMode);
}
NS_IMETHODIMP
nsJARChannel::GetIsDocument(bool* aIsDocument) {
return NS_GetIsDocumentChannel(this, aIsDocument);
}
NS_IMETHODIMP
nsJARChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) {
NS_IF_ADDREF(*aLoadGroup = mLoadGroup);
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) {
mLoadGroup = aLoadGroup;
return NS_OK;
}
//-----------------------------------------------------------------------------
// nsIChannel
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsJARChannel::GetOriginalURI(nsIURI** aURI) {
*aURI = mOriginalURI;
NS_ADDREF(*aURI);
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::SetOriginalURI(nsIURI* aURI) {
NS_ENSURE_ARG_POINTER(aURI);
mOriginalURI = aURI;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetURI(nsIURI** aURI) {
NS_IF_ADDREF(*aURI = mJarURI);
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetOwner(nsISupports** aOwner) {
// JAR signatures are not processed to avoid main-thread network I/O (bug
// 726125)
*aOwner = mOwner;
NS_IF_ADDREF(*aOwner);
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::SetOwner(nsISupports* aOwner) {
mOwner = aOwner;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) {
NS_IF_ADDREF(*aLoadInfo = mLoadInfo);
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) {
MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null");
mLoadInfo = aLoadInfo;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetNotificationCallbacks(nsIInterfaceRequestor** aCallbacks) {
NS_IF_ADDREF(*aCallbacks = mCallbacks);
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks) {
mCallbacks = aCallbacks;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) {
MOZ_ASSERT(aSecurityInfo, "Null out param");
*aSecurityInfo = nullptr;
return NS_OK;
}
bool nsJARChannel::GetContentTypeGuess(nsACString& aResult) const {
const char *ext = nullptr, *fileName = mJarEntry.get();
int32_t len = mJarEntry.Length();
// check if we're displaying a directory
// mJarEntry will be empty if we're trying to display
// the topmost directory in a zip, e.g. jar:foo.zip!/
if (ENTRY_IS_DIRECTORY(mJarEntry)) {
aResult.AssignLiteral(APPLICATION_HTTP_INDEX_FORMAT);
return true;
}
// Not a directory, take a guess by its extension
for (int32_t i = len - 1; i >= 0; i--) {
if (fileName[i] == '.') {
ext = &fileName[i + 1];
break;
}
}
if (!ext) {
return false;
}
nsIMIMEService* mimeServ = gJarHandler->MimeService();
if (!mimeServ) {
return false;
}
mimeServ->GetTypeFromExtension(nsDependentCString(ext), aResult);
return !aResult.IsEmpty();
}
NS_IMETHODIMP
nsJARChannel::GetContentType(nsACString& aResult) {
// If the Jar file has not been open yet,
// We return application/x-unknown-content-type
if (!mOpened) {
aResult.AssignLiteral(UNKNOWN_CONTENT_TYPE);
return NS_OK;
}
aResult = mContentType;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::SetContentType(const nsACString& aContentType) {
// We behave like HTTP channels (treat this as a hint if called before open,
// and override the charset if called after open).
// mContentCharset is unchanged if not parsed
NS_ParseResponseContentType(aContentType, mContentType, mContentCharset);
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetContentCharset(nsACString& aContentCharset) {
// If someone gives us a charset hint we should just use that charset.
// So we don't care when this is being called.
aContentCharset = mContentCharset;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::SetContentCharset(const nsACString& aContentCharset) {
mContentCharset = aContentCharset;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetContentDisposition(uint32_t* aContentDisposition) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsJARChannel::SetContentDisposition(uint32_t aContentDisposition) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsJARChannel::GetContentDispositionFilename(
nsAString& aContentDispositionFilename) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsJARChannel::SetContentDispositionFilename(
const nsAString& aContentDispositionFilename) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsJARChannel::GetContentDispositionHeader(
nsACString& aContentDispositionHeader) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsJARChannel::GetContentLength(int64_t* result) {
*result = mContentLength;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::SetContentLength(int64_t aContentLength) {
// XXX does this really make any sense at all?
mContentLength = aContentLength;
return NS_OK;
}
static void RecordZeroLengthEvent(bool aIsSync, const nsCString& aSpec,
nsresult aStatus, bool aCanceled,
nsILoadInfo* aLoadInfo) {
if (!StaticPrefs::network_jar_record_failure_reason()) {
return;
}
if (aLoadInfo) {
bool shouldSkipCheckForBrokenURLOrZeroSized;
MOZ_ALWAYS_SUCCEEDS(aLoadInfo->GetShouldSkipCheckForBrokenURLOrZeroSized(
&shouldSkipCheckForBrokenURLOrZeroSized));
if (shouldSkipCheckForBrokenURLOrZeroSized) {
return;
}
}
// The event can only hold 80 characters.
// We only save the file name and path inside the jar.
auto findFilenameStart = [](const nsCString& aSpec) -> uint32_t {
int32_t pos = aSpec.Find("!/");
if (pos == kNotFound) {
MOZ_ASSERT(false, "This should not happen");
return 0;
}
int32_t from = aSpec.RFindChar('/', pos);
if (from == kNotFound) {
MOZ_ASSERT(false, "This should not happen");
return 0;
}
// Skip over the slash
from++;
return from;
};
// If for some reason we are unable to extract the filename we report the
// entire string, or 80 characters of it, to make sure we don't miss any
// events.
uint32_t from = findFilenameStart(aSpec);
const auto fileName = Substring(aSpec, from);
nsAutoCString errorCString;
mozilla::GetErrorName(aStatus, errorCString);
// To test this telemetry we use a zip file and we want to make
// sure don't filter it out.
bool isTest = fileName.Find("test_empty_file.zip!") != -1;
bool isOmniJa = StringBeginsWith(fileName, "omni.ja!"_ns);
Telemetry::SetEventRecordingEnabled("zero_byte_load"_ns, true);
Telemetry::EventID eventType = Telemetry::EventID::Zero_byte_load_Load_Others;
if (StringEndsWith(fileName, ".ftl"_ns)) {
eventType = Telemetry::EventID::Zero_byte_load_Load_Ftl;
} else if (StringEndsWith(fileName, ".dtd"_ns)) {
// We're going to skip reporting telemetry on res DTDs.
// See Bug 1693711 for investigation into those empty loads.
if (!isTest && StringBeginsWith(fileName, "omni.ja!/res/dtd"_ns)) {
return;
}
eventType = Telemetry::EventID::Zero_byte_load_Load_Dtd;
} else if (StringEndsWith(fileName, ".properties"_ns)) {
eventType = Telemetry::EventID::Zero_byte_load_Load_Properties;
} else if (StringEndsWith(fileName, ".js"_ns) ||
StringEndsWith(fileName, ".jsm"_ns) ||
StringEndsWith(fileName, ".mjs"_ns)) {
// We're going to skip reporting telemetry on JS loads
// coming not from omni.ja.
// See Bug 1693711 for investigation into those empty loads.
if (!isTest && !isOmniJa) {
return;
}
eventType = Telemetry::EventID::Zero_byte_load_Load_Js;
} else if (StringEndsWith(fileName, ".xml"_ns)) {
eventType = Telemetry::EventID::Zero_byte_load_Load_Xml;
} else if (StringEndsWith(fileName, ".xhtml"_ns)) {
// This error seems to be very common and is not strongly
// correlated to YSOD.
if (aStatus == NS_ERROR_PARSED_DATA_CACHED) {
return;
}
// We're not investigating YSODs from extensions for now.
if (!isOmniJa) {
return;
}
eventType = Telemetry::EventID::Zero_byte_load_Load_Xhtml;
} else if (StringEndsWith(fileName, ".css"_ns)) {
// Bug 1702937: Filter out svg+'css'+'png'/NS_BINDING_ABORTED combo.
if (aStatus == NS_BINDING_ABORTED) {
return;
}
// Bug 1702937: Filter css/NS_ERROR_CORRUPTED_CONTENT that is coming from
// outside of omni.ja.
if (!isOmniJa && aStatus == NS_ERROR_CORRUPTED_CONTENT) {
return;
}
eventType = Telemetry::EventID::Zero_byte_load_Load_Css;
} else if (StringEndsWith(fileName, ".json"_ns)) {
eventType = Telemetry::EventID::Zero_byte_load_Load_Json;
} else if (StringEndsWith(fileName, ".html"_ns)) {
eventType = Telemetry::EventID::Zero_byte_load_Load_Html;
// See bug 1695560. Filter out non-omni.ja HTML.
if (!isOmniJa) {
return;
}
// See bug 1695560. "activity-stream-noscripts.html" with NS_ERROR_FAILURE
// is filtered out.
if (fileName.EqualsLiteral("omni.ja!/chrome/browser/res/activity-stream/"
"prerendered/activity-stream-noscripts.html") &&
aStatus == NS_ERROR_FAILURE) {
return;
}
} else if (StringEndsWith(fileName, ".png"_ns)) {
eventType = Telemetry::EventID::Zero_byte_load_Load_Png;
// See bug 1695560.
// Bug 1702937: Filter out svg+'css'+'png'/NS_BINDING_ABORTED combo.
if (!isOmniJa || aStatus == NS_BINDING_ABORTED) {
return;
}
} else if (StringEndsWith(fileName, ".svg"_ns)) {
eventType = Telemetry::EventID::Zero_byte_load_Load_Svg;
// See bug 1695560.
// Bug 1702937: Filter out svg+'css'+'png'/NS_BINDING_ABORTED combo.
if (!isOmniJa || aStatus == NS_BINDING_ABORTED) {
return;
}
}
// We're going to, for now, filter out `other` category.
// See Bug 1693711 for investigation into those empty loads.
// Bug 1702937: Filter other/*.ico/NS_BINDING_ABORTED.
if (!isTest && eventType == Telemetry::EventID::Zero_byte_load_Load_Others &&
(!isOmniJa || (aStatus == NS_BINDING_ABORTED &&
StringEndsWith(fileName, ".ico"_ns)))) {
return;
}
// FTL uses I/O to test for file presence, so we get
// a high volume of events from it, but it is not erronous.
// Also, Fluent is resilient to empty loads, so even if any
// of the errors are real errors, they don't cause YSOD.
// We can investigate them separately.
if (!isTest &&
(eventType == Telemetry::EventID::Zero_byte_load_Load_Ftl ||
eventType == Telemetry::EventID::Zero_byte_load_Load_Json) &&
aStatus == NS_ERROR_FILE_NOT_FOUND) {
return;
}
// See bug 1695560. "search-extensions/google/favicon.ico" with
// NS_BINDING_ABORTED is filtered out.
if (fileName.EqualsLiteral(
"omni.ja!/chrome/browser/search-extensions/google/favicon.ico") &&
aStatus == NS_BINDING_ABORTED) {
return;
}
// See bug 1695560. "update.locale" with
// NS_ERROR_FILE_NOT_FOUND is filtered out.
if (fileName.EqualsLiteral("omni.ja!/update.locale") &&
aStatus == NS_ERROR_FILE_NOT_FOUND) {
return;
}
auto res = CopyableTArray<Telemetry::EventExtraEntry>{};
res.SetCapacity(4);
res.AppendElement(
Telemetry::EventExtraEntry{"sync"_ns, aIsSync ? "true"_ns : "false"_ns});
res.AppendElement(
Telemetry::EventExtraEntry{"file_name"_ns, nsCString(fileName)});
res.AppendElement(Telemetry::EventExtraEntry{"status"_ns, errorCString});
res.AppendElement(Telemetry::EventExtraEntry{
"cancelled"_ns, aCanceled ? "true"_ns : "false"_ns});
Telemetry::RecordEvent(eventType, Nothing{}, Some(res));
}
NS_IMETHODIMP
nsJARChannel::Open(nsIInputStream** aStream) {
LOG(("nsJARChannel::Open [this=%p]\n", this));
nsCOMPtr<nsIStreamListener> listener;
nsresult rv =
nsContentSecurityManager::doContentSecurityCheck(this, listener);
NS_ENSURE_SUCCESS(rv, rv);
auto recordEvent = MakeScopeExit([&] {
if (mContentLength <= 0 || NS_FAILED(rv)) {
RecordZeroLengthEvent(true, mSpec, rv, mCanceled, mLoadInfo);
}
});
LOG(("nsJARChannel::Open [this=%p]\n", this));
NS_ENSURE_TRUE(!mOpened, NS_ERROR_IN_PROGRESS);
NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS);
mJarFile = nullptr;
rv = LookupFile();
if (NS_FAILED(rv)) return rv;
// If mJarFile was not set by LookupFile, we can't open a channel.
if (!mJarFile) {
MOZ_ASSERT_UNREACHABLE("only file-backed jars are supported");
return NS_ERROR_NOT_IMPLEMENTED;
}
RefPtr<nsJARInputThunk> input;
rv = CreateJarInput(gJarHandler->JarCache(), getter_AddRefs(input));
if (NS_FAILED(rv)) return rv;
input.forget(aStream);
SetOpened();
return NS_OK;
}
void nsJARChannel::SetOpened() {
MOZ_ASSERT(!mOpened, "Opening channel twice?");
mOpened = true;
// Compute the content type now.
if (!GetContentTypeGuess(mContentType)) {
mContentType.Assign(UNKNOWN_CONTENT_TYPE);
}
}
NS_IMETHODIMP
nsJARChannel::AsyncOpen(nsIStreamListener* aListener) {
LOG(("nsJARChannel::AsyncOpen [this=%p]\n", this));
nsCOMPtr<nsIStreamListener> listener = aListener;
nsresult rv =
nsContentSecurityManager::doContentSecurityCheck(this, listener);
if (NS_FAILED(rv)) {
mIsPending = false;
mListener = nullptr;
mCallbacks = nullptr;
mProgressSink = nullptr;
return rv;
}
LOG(("nsJARChannel::AsyncOpen [this=%p]\n", this));
MOZ_ASSERT(
mLoadInfo->GetSecurityMode() == 0 ||
mLoadInfo->GetInitialSecurityCheckDone() ||
(mLoadInfo->GetSecurityMode() ==
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL &&
mLoadInfo->GetLoadingPrincipal() &&
mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()),
"security flags in loadInfo but doContentSecurityCheck() not called");
NS_ENSURE_ARG_POINTER(listener);
NS_ENSURE_TRUE(!mOpened, NS_ERROR_IN_PROGRESS);
NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS);
mJarFile = nullptr;
// Initialize mProgressSink
NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, mProgressSink);
mListener = listener;
mIsPending = true;
rv = LookupFile();
if (NS_FAILED(rv) || !mJarFile) {
// Not a local file...
mIsPending = false;
mListener = nullptr;
mCallbacks = nullptr;
mProgressSink = nullptr;
return mJarFile ? rv : NS_ERROR_UNSAFE_CONTENT_TYPE;
}
rv = OpenLocalFile();
if (NS_FAILED(rv)) {
mIsPending = false;
mListener = nullptr;
mCallbacks = nullptr;
mProgressSink = nullptr;
return rv;
}
return NS_OK;
}
//-----------------------------------------------------------------------------
// nsIJARChannel
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsJARChannel::GetJarFile(nsIFile** aFile) {
NS_IF_ADDREF(*aFile = mJarFile);
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::SetJarFile(nsIFile* aFile) {
if (mOpened) {
return NS_ERROR_IN_PROGRESS;
}
mJarFileOverride = aFile;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::EnsureCached(bool* aIsCached) {
nsresult rv;
*aIsCached = false;
if (mOpened) {
return NS_ERROR_ALREADY_OPENED;
}
if (mPreCachedJarReader) {
// We've already been called and found the JAR is cached
*aIsCached = true;
return NS_OK;
}
nsCOMPtr<nsIURI> innerFileURI;
rv = mJarURI->GetJARFile(getter_AddRefs(innerFileURI));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFileURL> innerFileURL = do_QueryInterface(innerFileURI, &rv);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> jarFile;
rv = innerFileURL->GetFile(getter_AddRefs(jarFile));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIProtocolHandler> handler;
rv = ioService->GetProtocolHandler("jar", getter_AddRefs(handler));
NS_ENSURE_SUCCESS(rv, rv);
auto jarHandler = static_cast<nsJARProtocolHandler*>(handler.get());
MOZ_ASSERT(jarHandler);
nsIZipReaderCache* jarCache = jarHandler->JarCache();
rv = jarCache->GetZipIfCached(jarFile, getter_AddRefs(mPreCachedJarReader));
if (rv == NS_ERROR_CACHE_KEY_NOT_FOUND) {
return NS_OK;
}
NS_ENSURE_SUCCESS(rv, rv);
*aIsCached = true;
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::GetZipEntry(nsIZipEntry** aZipEntry) {
nsresult rv = LookupFile();
if (NS_FAILED(rv)) return rv;
if (!mJarFile) return NS_ERROR_NOT_AVAILABLE;
nsCOMPtr<nsIZipReader> reader;
rv = gJarHandler->JarCache()->GetZip(mJarFile, getter_AddRefs(reader));
if (NS_FAILED(rv)) return rv;
return reader->GetEntry(mJarEntry, aZipEntry);
}
//-----------------------------------------------------------------------------
// nsIStreamListener
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsJARChannel::OnStartRequest(nsIRequest* req) {
LOG(("nsJARChannel::OnStartRequest [this=%p %s]\n", this, mSpec.get()));
mRequest = req;
nsresult rv = mListener->OnStartRequest(this);
if (NS_FAILED(rv)) {
return rv;
}
// Restrict loadable content types.
nsAutoCString contentType;
GetContentType(contentType);
auto contentPolicyType = mLoadInfo->GetExternalContentPolicyType();
if (contentType.Equals(APPLICATION_HTTP_INDEX_FORMAT) &&
contentPolicyType != ExtContentPolicy::TYPE_DOCUMENT &&
contentPolicyType != ExtContentPolicy::TYPE_FETCH) {
return NS_ERROR_CORRUPTED_CONTENT;
}
if (contentPolicyType == ExtContentPolicy::TYPE_STYLESHEET &&
!contentType.EqualsLiteral(TEXT_CSS)) {
return NS_ERROR_CORRUPTED_CONTENT;
}
if (contentPolicyType == ExtContentPolicy::TYPE_SCRIPT &&
!nsContentUtils::IsJavascriptMIMEType(
NS_ConvertUTF8toUTF16(contentType))) {
return NS_ERROR_CORRUPTED_CONTENT;
}
return rv;
}
NS_IMETHODIMP
nsJARChannel::OnStopRequest(nsIRequest* req, nsresult status) {
LOG(("nsJARChannel::OnStopRequest [this=%p %s status=%" PRIx32 "]\n", this,
mSpec.get(), static_cast<uint32_t>(status)));
if (NS_SUCCEEDED(mStatus)) mStatus = status;
if (mListener) {
if (!mOnDataCalled || NS_FAILED(status)) {
RecordZeroLengthEvent(false, mSpec, status, mCanceled, mLoadInfo);
}
mListener->OnStopRequest(this, status);
mListener = nullptr;
}
if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, status);
mRequest = nullptr;
mPump = nullptr;
mIsPending = false;
// Drop notification callbacks to prevent cycles.
mCallbacks = nullptr;
mProgressSink = nullptr;
#if defined(XP_WIN) || defined(MOZ_WIDGET_COCOA)
#else
// To deallocate file descriptor by RemoteOpenFileChild destructor.
mJarFile = nullptr;
#endif
return NS_OK;
}
NS_IMETHODIMP
nsJARChannel::OnDataAvailable(nsIRequest* req, nsIInputStream* stream,
uint64_t offset, uint32_t count) {
LOG(("nsJARChannel::OnDataAvailable [this=%p %s]\n", this, mSpec.get()));
nsresult rv;
// don't send out OnDataAvailable notifications if we've been canceled.
if (mCanceled) {
return mStatus;
}
mOnDataCalled = true;
rv = mListener->OnDataAvailable(this, stream, offset, count);
// simply report progress here instead of hooking ourselves up as a
// nsITransportEventSink implementation.
// XXX do the 64-bit stuff for real
if (mProgressSink && NS_SUCCEEDED(rv)) {
if (NS_IsMainThread()) {
FireOnProgress(offset + count);
} else {
NS_DispatchToMainThread(NewRunnableMethod<uint64_t>(
"nsJARChannel::FireOnProgress", this, &nsJARChannel::FireOnProgress,
offset + count));
}
}
return rv; // let the pump cancel on failure
}
NS_IMETHODIMP
nsJARChannel::RetargetDeliveryTo(nsISerialEventTarget* aEventTarget) {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIThreadRetargetableRequest> request = do_QueryInterface(mRequest);
if (!request) {
return NS_ERROR_NO_INTERFACE;
}
return request->RetargetDeliveryTo(aEventTarget);
}
NS_IMETHODIMP
nsJARChannel::GetDeliveryTarget(nsISerialEventTarget** aEventTarget) {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIThreadRetargetableRequest> request = do_QueryInterface(mRequest);
if (!request) {
return NS_ERROR_NO_INTERFACE;
}
return request->GetDeliveryTarget(aEventTarget);
}
NS_IMETHODIMP
nsJARChannel::CheckListenerChain() {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIThreadRetargetableStreamListener> listener =
do_QueryInterface(mListener);
if (!listener) {
return NS_ERROR_NO_INTERFACE;
}
return listener->CheckListenerChain();
}
NS_IMETHODIMP
nsJARChannel::OnDataFinished(nsresult aStatus) {
nsCOMPtr<nsIThreadRetargetableStreamListener> listener =
do_QueryInterface(mListener);
if (listener) {
return listener->OnDataFinished(aStatus);
}
return NS_OK;
}