/* 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/. */ // @ts-nocheck Defer for now. const { nsIHttpActivityObserver, nsISocketTransport } = Ci; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs", ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", SecurityInfo: "resource://gre/modules/SecurityInfo.sys.mjs", WebRequestUpload: "resource://gre/modules/WebRequestUpload.sys.mjs", }); // WebRequest.sys.mjs's only consumer is ext-webRequest.js, so we can depend on // the apiManager.global being initialized. ChromeUtils.defineLazyGetter(lazy, "tabTracker", () => { return lazy.ExtensionParent.apiManager.global.tabTracker; }); ChromeUtils.defineLazyGetter( lazy, "getCookieStoreIdForOriginAttributes", () => { return lazy.ExtensionParent.apiManager.global .getCookieStoreIdForOriginAttributes; } ); // URI schemes that service workers are allowed to load scripts from (any other // scheme is not allowed by the specs and it is not expected by the service workers // internals neither, which would likely trigger unexpected behaviors). const ALLOWED_SERVICEWORKER_SCHEMES = ["https", "http", "moz-extension"]; // Response HTTP Headers matching the following patterns are restricted for changes // applied by MV3 extensions. const MV3_RESTRICTED_HEADERS_PATTERNS = [ /^cross-origin-embedder-policy$/, /^cross-origin-opener-policy$/, /^cross-origin-resource-policy$/, /^x-frame-options$/, /^access-control-/, ]; // Classes of requests that should be sent immediately instead of batched. // Covers basically anything that can delay first paint or DOMContentLoaded: // top frame HTML, blocking CSS, fonts preflight, sync JS and XHR. const URGENT_CLASSES = Ci.nsIClassOfService.Leader | Ci.nsIClassOfService.Unblocked | Ci.nsIClassOfService.UrgentStart | Ci.nsIClassOfService.TailForbidden; function runLater(job) { Services.tm.dispatchToMainThread(job); } function parseFilter(filter) { if (!filter) { filter = {}; } return { urls: filter.urls || null, types: filter.types || null, tabId: filter.tabId ?? null, windowId: filter.windowId ?? null, incognito: filter.incognito ?? null, }; } function parseExtra(extra, allowed = [], optionsObj = {}) { if (extra) { for (let ex of extra) { if (!allowed.includes(ex)) { throw new lazy.ExtensionUtils.ExtensionError(`Invalid option ${ex}`); } } } let result = Object.assign({}, optionsObj); for (let al of allowed) { if (extra && extra.includes(al)) { result[al] = true; } } return result; } function isThenable(value) { return value && typeof value === "object" && typeof value.then === "function"; } // Verify a requested redirect and throw a more explicit error. function verifyRedirect(channel, redirectUri, finalUrl, addonId) { const { isServiceWorkerScript } = channel; if ( isServiceWorkerScript && channel.loadInfo?.internalContentPolicyType === Ci.nsIContentPolicy.TYPE_INTERNAL_SERVICE_WORKER ) { throw new Error( `Invalid redirectUrl ${redirectUri?.spec} on service worker main script ${finalUrl} requested by ${addonId}` ); } if ( isServiceWorkerScript && (channel.loadInfo?.internalContentPolicyType === Ci.nsIContentPolicy.TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS || channel.loadInfo?.internalContentPolicyType === Ci.nsIContentPolicy.TYPE_INTERNAL_WORKER_STATIC_MODULE) && !ALLOWED_SERVICEWORKER_SCHEMES.includes(redirectUri?.scheme) ) { throw new Error( `Invalid redirectUrl ${redirectUri?.spec} on service worker imported script ${finalUrl} requested by ${addonId}` ); } } class HeaderChanger { constructor(channel) { this.channel = channel; this.array = this.readHeaders(); } getMap() { if (!this.map) { this.map = new Map(); for (let header of this.array) { this.map.set(header.name.toLowerCase(), header); } } return this.map; } toArray() { return this.array; } validateHeaders(headers) { // We should probably use schema validation for this. if (!Array.isArray(headers)) { return false; } return headers.every(header => { if (typeof header !== "object" || header === null) { return false; } if (typeof header.name !== "string") { return false; } return ( typeof header.value === "string" || Array.isArray(header.binaryValue) ); }); } applyChanges(headers, opts = {}) { if (!this.validateHeaders(headers)) { Cu.reportError(`Invalid header array: ${uneval(headers)}`); return; } let newHeaders = new Set(headers.map(({ name }) => name.toLowerCase())); // Remove missing headers. let origHeaders = this.getMap(); for (let name of origHeaders.keys()) { if (!newHeaders.has(name)) { this.setHeader(name, "", false, opts, name); } } // Set new or changed headers. If there are multiple headers with the same // name (e.g. Set-Cookie), merge them, instead of having new values // overwrite previous ones. // // When the new value of a header is equal the existing value of the header // (e.g. the initial response set "Set-Cookie: examplename=examplevalue", // and an extension also added the header // "Set-Cookie: examplename=examplevalue") then the header value is not // re-set, but subsequent headers of the same type will be merged in. // // Multiple addons will be able to provide modifications to any headers // listed in the default set. let headersAlreadySet = new Set(); for (let { name, value, binaryValue } of headers) { if (binaryValue) { value = String.fromCharCode(...binaryValue); } let lowerCaseName = name.toLowerCase(); let original = origHeaders.get(lowerCaseName); if (!original || value !== original.value) { let shouldMerge = headersAlreadySet.has(lowerCaseName); this.setHeader(name, value, shouldMerge, opts, lowerCaseName); } headersAlreadySet.add(lowerCaseName); } } } const checkRestrictedHeaderValue = (value, opts = {}) => { let uri = Services.io.newURI(`https://${value}/`); let { policy } = opts; if (policy && !policy.allowedOrigins.matches(uri)) { throw new Error(`Unable to set host header, url missing from permissions.`); } if (WebExtensionPolicy.isRestrictedURI(uri)) { throw new Error(`Unable to set host header to restricted url.`); } }; class RequestHeaderChanger extends HeaderChanger { setHeader(name, value, merge, opts, lowerCaseName) { try { if (value && lowerCaseName === "host") { checkRestrictedHeaderValue(value, opts); } this.channel.setRequestHeader(name, value, merge); } catch (e) { Cu.reportError(new Error(`Error setting request header ${name}: ${e}`)); } } readHeaders() { return this.channel.getRequestHeaders(); } } class ResponseHeaderChanger extends HeaderChanger { didModifyCSP = false; setHeader(name, value, merge, opts, lowerCaseName) { if (lowerCaseName === "content-security-policy") { // When multiple add-ons change the CSP, enforce the combined (strictest) // policy - see bug 1462989 for motivation. // When value is unset, don't force the header to be merged, to allow // add-ons to clear the header if wanted. if (value) { merge = merge || this.didModifyCSP; } // For manifest_version 3 extension, we are currently only allowing to // merge additional CSP strings to the existing ones, which will be initially // stricter than currently allowed to manifest_version 2 extensions, then // following up with either a new permission and/or some more changes to the // APIs (and possibly making the behavior more deterministic than it is for // manifest_version 2 at the moment). if (opts.policy.manifestVersion > 2) { if (value) { // If the given CSP header value is non empty, then it should be // merged to the existing one. merge = true; } else { // If the given CSP header value is empty (which would be clearing the // CSP header), it should be considered a no-op and this.didModifyCSP // shouldn't be changed to true. return; } } this.didModifyCSP = true; } else if ( opts.policy.manifestVersion > 2 && this.isResponseHeaderRestricted(lowerCaseName) ) { // TODO (Bug 1787155 and Bug 1273281) open this up to MV3 extensions, // locked behind manifest.json declarative permission and a separate // explicit user-controlled permission (and ideally also check for // changes that would lead to security downgrades). Cu.reportError( `Disallowed change restricted response header ${name} on ${this.channel.finalURL} from ${opts.policy.debugName}` ); return; } try { this.channel.setResponseHeader(name, value, merge); } catch (e) { Cu.reportError(new Error(`Error setting response header ${name}: ${e}`)); } } isResponseHeaderRestricted(lowerCaseHeaderName) { return MV3_RESTRICTED_HEADERS_PATTERNS.some(regex => regex.test(lowerCaseHeaderName) ); } readHeaders() { return this.channel.getResponseHeaders(); } } const MAYBE_CACHED_EVENTS = new Set([ "onResponseStarted", "onHeadersReceived", "onBeforeRedirect", "onCompleted", "onErrorOccurred", ]); const OPTIONAL_PROPERTIES = [ "requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl", "requestBody", "scheme", "realm", "isProxy", "challenger", "proxyInfo", "ip", "frameAncestors", "urlClassification", "requestSize", "responseSize", ]; function serializeRequestData(eventName) { let data = { requestId: this.requestId, url: this.url, originUrl: this.originUrl, documentUrl: this.documentUrl, method: this.method, type: this.type, timeStamp: Date.now(), tabId: this.tabId, frameId: this.frameId, parentFrameId: this.parentFrameId, incognito: this.incognito, thirdParty: this.thirdParty, cookieStoreId: this.cookieStoreId, urgentSend: this.urgentSend, }; if (MAYBE_CACHED_EVENTS.has(eventName)) { data.fromCache = !!this.fromCache; } for (let opt of OPTIONAL_PROPERTIES) { if (typeof this[opt] !== "undefined") { data[opt] = this[opt]; } } if (this.urlClassification) { data.urlClassification = { firstParty: this.urlClassification.firstParty.filter( c => !c.startsWith("socialtracking_") ), thirdParty: this.urlClassification.thirdParty.filter( c => !c.startsWith("socialtracking_") ), }; } return data; } var HttpObserverManager; var ChannelEventSink = { _classDescription: "WebRequest channel event sink", _classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"), _contractID: "@mozilla.org/webrequest/channel-event-sink;1", QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink", "nsIFactory"]), init() { Components.manager .QueryInterface(Ci.nsIComponentRegistrar) .registerFactory( this._classID, this._classDescription, this._contractID, this ); }, register() { Services.catMan.addCategoryEntry( "net-channel-event-sinks", this._contractID, this._contractID, false, true ); }, unregister() { Services.catMan.deleteCategoryEntry( "net-channel-event-sinks", this._contractID, false ); }, // nsIChannelEventSink implementation asyncOnChannelRedirect(oldChannel, newChannel, flags, redirectCallback) { runLater(() => redirectCallback.onRedirectVerifyCallback(Cr.NS_OK)); try { HttpObserverManager.onChannelReplaced(oldChannel, newChannel); } catch (e) { // we don't wanna throw: it would abort the redirection } }, // nsIFactory implementation createInstance(iid) { return this.QueryInterface(iid); }, }; ChannelEventSink.init(); // nsIAuthPrompt2 implementation for onAuthRequired class AuthRequestor { constructor(channel, httpObserver) { this.notificationCallbacks = channel.notificationCallbacks; this.loadGroupCallbacks = channel.loadGroup && channel.loadGroup.notificationCallbacks; this.httpObserver = httpObserver; } getInterface(iid) { if (iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsIAuthPrompt2)) { return this; } try { return this.notificationCallbacks.getInterface(iid); } catch (e) {} throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); } _getForwardedInterface(iid) { try { return this.notificationCallbacks.getInterface(iid); } catch (e) { return this.loadGroupCallbacks.getInterface(iid); } } // nsIAuthPromptProvider getAuthPrompt getAuthPrompt(reason, iid) { // This should never get called without getInterface having been called first. if (iid.equals(Ci.nsIAuthPrompt2)) { return this; } return this._getForwardedInterface(Ci.nsIAuthPromptProvider).getAuthPrompt( reason, iid ); } // nsIAuthPrompt2 promptAuth promptAuth(channel, level, authInfo) { this._getForwardedInterface(Ci.nsIAuthPrompt2).promptAuth( channel, level, authInfo ); } _getForwardPrompt(data) { let reason = data.isProxy ? Ci.nsIAuthPromptProvider.PROMPT_PROXY : Ci.nsIAuthPromptProvider.PROMPT_NORMAL; for (let callbacks of [ this.notificationCallbacks, this.loadGroupCallbacks, ]) { try { return callbacks .getInterface(Ci.nsIAuthPromptProvider) .getAuthPrompt(reason, Ci.nsIAuthPrompt2); } catch (e) {} try { return callbacks.getInterface(Ci.nsIAuthPrompt2); } catch (e) {} } throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); } // nsIAuthPrompt2 asyncPromptAuth asyncPromptAuth(channel, callback, context, level, authInfo) { let wrapper = ChannelWrapper.get(channel); let uri = channel.URI; let proxyInfo; let isProxy = !!(authInfo.flags & authInfo.AUTH_PROXY); if (isProxy && channel instanceof Ci.nsIProxiedChannel) { proxyInfo = channel.proxyInfo; } let data = { scheme: authInfo.authenticationScheme, realm: authInfo.realm, isProxy, challenger: { host: proxyInfo ? proxyInfo.host : uri.host, port: proxyInfo ? proxyInfo.port : uri.port, }, }; // In the case that no listener provides credentials, we fallback to the // previously set callback class for authentication. wrapper.authPromptForward = () => { try { let prompt = this._getForwardPrompt(data); prompt.asyncPromptAuth(channel, callback, context, level, authInfo); } catch (e) { Cu.reportError(`webRequest asyncPromptAuth failure ${e}`); callback.onAuthCancelled(context, false); } wrapper.authPromptForward = null; wrapper.authPromptCallback = null; }; wrapper.authPromptCallback = authCredentials => { // The API allows for canceling the request, providing credentials or // doing nothing, so we do not provide a way to call onAuthCanceled. // Canceling the request will result in canceling the authentication. if ( authCredentials && typeof authCredentials.username === "string" && typeof authCredentials.password === "string" ) { authInfo.username = authCredentials.username; authInfo.password = authCredentials.password; try { callback.onAuthAvailable(context, authInfo); } catch (e) { Cu.reportError(`webRequest onAuthAvailable failure ${e}`); } // At least one addon has responded, so we won't forward to the regular // prompt handlers. wrapper.authPromptForward = null; wrapper.authPromptCallback = null; } }; this.httpObserver.runChannelListener(wrapper, "onAuthRequired", data); return { QueryInterface: ChromeUtils.generateQI(["nsICancelable"]), cancel() { try { callback.onAuthCancelled(context, false); } catch (e) { Cu.reportError(`webRequest onAuthCancelled failure ${e}`); } wrapper.authPromptForward = null; wrapper.authPromptCallback = null; }, }; } } AuthRequestor.prototype.QueryInterface = ChromeUtils.generateQI([ "nsIInterfaceRequestor", "nsIAuthPromptProvider", "nsIAuthPrompt2", ]); // Most WebRequest events are implemented via the observer services, but // a few use custom xpcom interfaces. This class (HttpObserverManager) // serves two main purposes: // 1. It abstracts away the names and details of the underlying // implementation (e.g., onBeforeBeforeRequest is dispatched from // the http-on-modify-request observable). // 2. It aggregates multiple listeners so that a single observer or // handler can serve multiple webRequest listeners. HttpObserverManager = { listeners: { // onBeforeRequest uses http-on-modify observer for HTTP(S). onBeforeRequest: new Map(), // onBeforeSendHeaders and onSendHeaders correspond to the // http-on-before-connect observer. onBeforeSendHeaders: new Map(), onSendHeaders: new Map(), // onHeadersReceived corresponds to the http-on-examine-* obserservers. onHeadersReceived: new Map(), // onAuthRequired is handled via the nsIAuthPrompt2 xpcom interface // which is managed here by AuthRequestor. onAuthRequired: new Map(), // onBeforeRedirect is handled by the nsIChannelEVentSink xpcom interface // which is managed here by ChannelEventSink. onBeforeRedirect: new Map(), // onResponseStarted, onErrorOccurred, and OnCompleted correspond // to events dispatched by the ChannelWrapper EventTarget. onResponseStarted: new Map(), onErrorOccurred: new Map(), onCompleted: new Map(), }, // Whether there are any registered declarativeNetRequest rules. These DNR // rules may match new requests and result in request modifications. dnrActive: false, openingInitialized: false, beforeConnectInitialized: false, examineInitialized: false, redirectInitialized: false, activityInitialized: false, needTracing: false, hasRedirects: false, getWrapper(nativeChannel) { let wrapper = ChannelWrapper.get(nativeChannel); if (!wrapper._addedListeners) { /* eslint-disable mozilla/balanced-listeners */ if (this.listeners.onErrorOccurred.size) { wrapper.addEventListener("error", this); } if (this.listeners.onResponseStarted.size) { wrapper.addEventListener("start", this); } if (this.listeners.onCompleted.size) { wrapper.addEventListener("stop", this); } /* eslint-enable mozilla/balanced-listeners */ wrapper._addedListeners = true; } return wrapper; }, get activityDistributor() { return Cc["@mozilla.org/network/http-activity-distributor;1"].getService( Ci.nsIHttpActivityDistributor ); }, // This method is called whenever webRequest listeners are added or removed. // It reconciles the set of listeners with underlying observers, event // handlers, etc. by adding new low-level handlers for any newly added // webRequest listeners and removing those that are no longer needed if // there are no more listeners for corresponding webRequest events. addOrRemove() { let needOpening = this.listeners.onBeforeRequest.size || this.dnrActive; let needBeforeConnect = this.listeners.onBeforeSendHeaders.size || this.listeners.onSendHeaders.size || this.dnrActive; if (needOpening && !this.openingInitialized) { this.openingInitialized = true; Services.obs.addObserver(this, "http-on-modify-request"); } else if (!needOpening && this.openingInitialized) { this.openingInitialized = false; Services.obs.removeObserver(this, "http-on-modify-request"); } if (needBeforeConnect && !this.beforeConnectInitialized) { this.beforeConnectInitialized = true; Services.obs.addObserver(this, "http-on-before-connect"); } else if (!needBeforeConnect && this.beforeConnectInitialized) { this.beforeConnectInitialized = false; Services.obs.removeObserver(this, "http-on-before-connect"); } let haveBlocking = Object.values(this.listeners).some(listeners => Array.from(listeners.values()).some(listener => listener.blockingAllowed) ); this.needTracing = this.listeners.onResponseStarted.size || this.listeners.onErrorOccurred.size || this.listeners.onCompleted.size || haveBlocking; let needExamine = this.needTracing || this.listeners.onHeadersReceived.size || this.listeners.onAuthRequired.size || this.dnrActive; if (needExamine && !this.examineInitialized) { this.examineInitialized = true; Services.obs.addObserver(this, "http-on-examine-response"); Services.obs.addObserver(this, "http-on-examine-cached-response"); Services.obs.addObserver(this, "http-on-examine-merged-response"); } else if (!needExamine && this.examineInitialized) { this.examineInitialized = false; Services.obs.removeObserver(this, "http-on-examine-response"); Services.obs.removeObserver(this, "http-on-examine-cached-response"); Services.obs.removeObserver(this, "http-on-examine-merged-response"); } // If we have any listeners, we need the channelsink so the channelwrapper is // updated properly. Otherwise events for channels that are redirected will not // happen correctly. If we have no listeners, shut it down. this.hasRedirects = this.listeners.onBeforeRedirect.size > 0; let needRedirect = this.hasRedirects || needExamine || needOpening || needBeforeConnect; if (needRedirect && !this.redirectInitialized) { this.redirectInitialized = true; ChannelEventSink.register(); } else if (!needRedirect && this.redirectInitialized) { this.redirectInitialized = false; ChannelEventSink.unregister(); } let needActivity = this.listeners.onErrorOccurred.size; if (needActivity && !this.activityInitialized) { this.activityInitialized = true; this.activityDistributor.addObserver(this); } else if (!needActivity && this.activityInitialized) { this.activityInitialized = false; this.activityDistributor.removeObserver(this); } }, addListener(kind, callback, opts) { this.listeners[kind].set(callback, opts); this.addOrRemove(); }, removeListener(kind, callback) { this.listeners[kind].delete(callback); this.addOrRemove(); }, setDNRHandlingEnabled(dnrActive) { this.dnrActive = dnrActive; this.addOrRemove(); }, observe(subject, topic, data) { let channel = this.getWrapper(subject); switch (topic) { case "http-on-modify-request": this.runChannelListener(channel, "onBeforeRequest"); break; case "http-on-before-connect": this.runChannelListener(channel, "onBeforeSendHeaders"); break; case "http-on-examine-cached-response": case "http-on-examine-merged-response": channel.fromCache = true; // falls through case "http-on-examine-response": this.examine(channel, topic, data); break; } }, // We map activity values with tentative error names, e.g. "STATUS_RESOLVING" => "NS_ERROR_NET_ON_RESOLVING". get activityErrorsMap() { let prefix = /^(?:ACTIVITY_SUBTYPE_|STATUS_)/; let map = new Map(); for (let iface of [nsIHttpActivityObserver, nsISocketTransport]) { for (let c of Object.keys(iface).filter(name => prefix.test(name))) { map.set(iface[c], c.replace(prefix, "NS_ERROR_NET_ON_")); } } delete this.activityErrorsMap; this.activityErrorsMap = map; return this.activityErrorsMap; }, GOOD_LAST_ACTIVITY: nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_HEADER, observeActivity( nativeChannel, activityType, activitySubtype /* , aTimestamp, aExtraSizeData, aExtraStringData */ ) { // Sometimes we get a NullHttpChannel, which implements // nsIHttpChannel but not nsIChannel. if (!(nativeChannel instanceof Ci.nsIChannel)) { return; } let channel = this.getWrapper(nativeChannel); let lastActivity = channel.lastActivity || 0; if ( activitySubtype === nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE && lastActivity && lastActivity !== this.GOOD_LAST_ACTIVITY ) { // Since the channel's security info is assigned in onStartRequest and // errorCheck is called in ChannelWrapper::onStartRequest, we should check // the errorString after onStartRequest to make sure errors have a chance // to be processed before we fall back to a generic error string. channel.addEventListener( "start", () => { if (!channel.errorString) { this.runChannelListener(channel, "onErrorOccurred", { error: this.activityErrorsMap.get(lastActivity) || `NS_ERROR_NET_UNKNOWN_${lastActivity}`, }); } }, { once: true } ); } else if ( lastActivity !== this.GOOD_LAST_ACTIVITY && lastActivity !== nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE ) { channel.lastActivity = activitySubtype; } }, getRequestData(channel, extraData) { let originAttributes = channel.loadInfo?.originAttributes; let cos = channel.channel.QueryInterface(Ci.nsIClassOfService); let data = { requestId: String(channel.id), url: channel.finalURL, method: channel.method, type: channel.type, fromCache: channel.fromCache, incognito: originAttributes?.privateBrowsingId > 0, thirdParty: channel.thirdParty, originUrl: channel.originURL || undefined, documentUrl: channel.documentURL || undefined, tabId: this.getBrowserData(channel).tabId, frameId: channel.frameId, parentFrameId: channel.parentFrameId, frameAncestors: channel.frameAncestors || undefined, ip: channel.remoteAddress, proxyInfo: channel.proxyInfo, serialize: serializeRequestData, requestSize: channel.requestSize, responseSize: channel.responseSize, urlClassification: channel.urlClassification, // Figure out if this is an urgent request that shouldn't be batched. urgentSend: (cos.classFlags & URGENT_CLASSES) > 0, }; if (originAttributes) { data.cookieStoreId = lazy.getCookieStoreIdForOriginAttributes(originAttributes); } return Object.assign(data, extraData); }, handleEvent(event) { let channel = event.currentTarget; switch (event.type) { case "error": this.runChannelListener(channel, "onErrorOccurred", { error: channel.errorString, }); break; case "start": this.runChannelListener(channel, "onResponseStarted"); break; case "stop": this.runChannelListener(channel, "onCompleted"); break; } }, STATUS_TYPES: new Set([ "onHeadersReceived", "onAuthRequired", "onBeforeRedirect", "onResponseStarted", "onCompleted", ]), FILTER_TYPES: new Set([ "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onAuthRequired", "onBeforeRedirect", ]), getBrowserData(wrapper) { let browserData = wrapper._browserData; if (!browserData) { if (wrapper.browserElement) { browserData = lazy.tabTracker.getBrowserData(wrapper.browserElement); } else { browserData = { tabId: -1, windowId: -1 }; } wrapper._browserData = browserData; } return browserData; }, runChannelListener(channel, kind, extraData = null) { let handlerResults = []; let requestHeaders; let responseHeaders; try { if (kind !== "onErrorOccurred" && channel.errorString) { return; } if (this.dnrActive) { // DNR may modify (but not cancel) the request at this stage. lazy.ExtensionDNR.beforeWebRequestEvent(channel, kind); } let registerFilter = this.FILTER_TYPES.has(kind); let commonData = null; let requestBody; this.listeners[kind].forEach((opts, callback) => { if (opts.filter.tabId !== null || opts.filter.windowId !== null) { const { tabId, windowId } = this.getBrowserData(channel); if ( (opts.filter.tabId !== null && tabId != opts.filter.tabId) || (opts.filter.windowId !== null && windowId != opts.filter.windowId) ) { return; } } if (!channel.matches(opts.filter, opts.policy, extraData)) { return; } let extension = opts.policy?.extension; // TODO: Move this logic to ChannelWrapper::matches, see bug 1699481 if ( extension?.userContextIsolation && !extension.canAccessContainer( channel.loadInfo?.originAttributes.userContextId ) ) { return; } if (!commonData) { commonData = this.getRequestData(channel, extraData); if (this.STATUS_TYPES.has(kind)) { commonData.statusCode = channel.statusCode; commonData.statusLine = channel.statusLine; } } let data = Object.create(commonData); data.urgentSend = data.urgentSend && opts.blocking; if (registerFilter && opts.blocking && opts.policy) { data.registerTraceableChannel = (policy, remoteTab) => { // `channel` is a ChannelWrapper, which contains the actual // underlying nsIChannel in `channel.channel`. For startup events // that are held until the extension background page is started, // it is possible that the underlying channel can be closed and // cleaned up between the time the event occurred and the time // we reach this code. if (channel.channel) { channel.registerTraceableChannel(policy, remoteTab); } }; } if (opts.requestHeaders) { requestHeaders = requestHeaders || new RequestHeaderChanger(channel); data.requestHeaders = requestHeaders.toArray(); } if (opts.responseHeaders) { try { responseHeaders = responseHeaders || new ResponseHeaderChanger(channel); data.responseHeaders = responseHeaders.toArray(); } catch (e) { /* headers may not be available on some redirects */ } } if (opts.requestBody && channel.canModify) { requestBody = requestBody || lazy.WebRequestUpload.createRequestBody(channel.channel); data.requestBody = requestBody; } try { let result = callback(data); // isProxy is set during onAuth if the auth request is for a proxy. // We allow handling proxy auth regardless of canModify. if ( (channel.canModify || data.isProxy) && typeof result === "object" && opts.blocking ) { handlerResults.push({ opts, result }); } } catch (e) { Cu.reportError(e); } }); } catch (e) { Cu.reportError(e); } if (this.dnrActive && lazy.ExtensionDNR.handleRequest(channel, kind)) { return; } return this.applyChanges( kind, channel, handlerResults, requestHeaders, responseHeaders ); }, async applyChanges( kind, channel, handlerResults, requestHeaders, responseHeaders ) { const { finalURL, id: chanId } = channel; let shouldResume = !channel.suspended; // NOTE: if a request has been suspended before the GeckoProfiler // has been activated and then resumed while the GeckoProfiler is active // and collecting data, the resulting "Extension Suspend" marker will be // recorded with an empty marker text (and so without url, chan id and // the supenders addon ids). let markerText = ""; if (Services.profiler?.IsActive()) { const suspenders = handlerResults .filter(({ result }) => isThenable(result)) .map(({ opts }) => opts.addonId) .join(", "); markerText = `${kind} ${finalURL} by ${suspenders} (chanId: ${chanId})`; } try { for (let { opts, result } of handlerResults) { if (isThenable(result)) { channel.suspend(markerText); try { result = await result; } catch (e) { let error; if (e instanceof Error) { error = e; } else if (typeof e === "object" && e.message) { error = new Error(e.message, e.fileName, e.lineNumber); } Cu.reportError(error); continue; } if (!result || typeof result !== "object") { continue; } } if ( kind === "onAuthRequired" && result.authCredentials && channel.authPromptCallback ) { channel.authPromptCallback(result.authCredentials); } // We allow proxy auth to cancel or handle authCredentials regardless of // canModify, but ensure we do nothing else. if (!channel.canModify) { continue; } if (result.cancel) { channel.resume(); channel.cancel( Cr.NS_ERROR_ABORT, Ci.nsILoadInfo.BLOCKING_REASON_EXTENSION_WEBREQUEST ); ChromeUtils.addProfilerMarker( "Extension Canceled", { category: "Network" }, `${kind} ${finalURL} canceled by ${opts.addonId} (chanId: ${chanId})` ); if (opts.policy) { let properties = channel.channel.QueryInterface( Ci.nsIWritablePropertyBag ); properties.setProperty("cancelledByExtension", opts.policy.id); } return; } if (result.redirectUrl) { try { const { redirectUrl } = result; channel.resume(); const redirectUri = Services.io.newURI(redirectUrl); verifyRedirect(channel, redirectUri, finalURL, opts.addonId); channel.redirectTo(redirectUri); ChromeUtils.addProfilerMarker( "Extension Redirected", { category: "Network" }, `${kind} ${finalURL} redirected to ${redirectUrl} by ${opts.addonId} (chanId: ${chanId})` ); if (opts.policy) { let properties = channel.channel.QueryInterface( Ci.nsIWritablePropertyBag ); properties.setProperty("redirectedByExtension", opts.policy.id); } // Web Extensions using the WebRequest API are allowed // to redirect a channel to a data: URI, hence we mark // the channel to let the redirect blocker know. Please // note that this marking needs to happen after the // channel.redirectTo is called because the channel's // RedirectTo() implementation explicitly drops the flag // to avoid additional redirects not caused by the // Web Extension. channel.loadInfo.allowInsecureRedirectToDataURI = true; // To pass CORS checks, we pretend the current request's // response allows the triggering origin to access. let origin = channel.getRequestHeader("Origin"); if (origin) { channel.setResponseHeader("Access-Control-Allow-Origin", origin); channel.setResponseHeader( "Access-Control-Allow-Credentials", "true" ); // Compute an arbitrary 'Access-Control-Allow-Headers' // for the internal Redirect let allowHeaders = channel .getRequestHeaders() .map(header => header.name) .join(); channel.setResponseHeader( "Access-Control-Allow-Headers", allowHeaders ); channel.setResponseHeader( "Access-Control-Allow-Methods", channel.method ); } return; } catch (e) { Cu.reportError(e); } } if (result.upgradeToSecure && kind === "onBeforeRequest") { try { channel.upgradeToSecure(); } catch (e) { Cu.reportError(e); } } if (opts.requestHeaders && result.requestHeaders && requestHeaders) { requestHeaders.applyChanges(result.requestHeaders, opts); } if (opts.responseHeaders && result.responseHeaders && responseHeaders) { responseHeaders.applyChanges(result.responseHeaders, opts); } } // If a listener did not cancel the request or provide credentials, we // forward the auth request to the base handler. if (kind === "onAuthRequired" && channel.authPromptForward) { channel.authPromptForward(); } if (kind === "onBeforeSendHeaders" && this.listeners.onSendHeaders.size) { this.runChannelListener(channel, "onSendHeaders"); } else if (kind !== "onErrorOccurred") { channel.errorCheck(); } } catch (e) { Cu.reportError(e); } // Only resume the channel if it was suspended by this call. if (shouldResume) { channel.resume(); } }, shouldHookListener(listener, channel, extraData) { if (listener.size == 0) { return false; } for (let opts of listener.values()) { if (channel.matches(opts.filter, opts.policy, extraData)) { return true; } } return false; }, examine(channel) { if (this.listeners.onHeadersReceived.size || this.dnrActive) { this.runChannelListener(channel, "onHeadersReceived"); } if ( !channel.hasAuthRequestor && this.shouldHookListener(this.listeners.onAuthRequired, channel, { isProxy: true, }) ) { channel.channel.notificationCallbacks = new AuthRequestor( channel.channel, this ); channel.hasAuthRequestor = true; } }, onChannelReplaced(oldChannel, newChannel) { let channel = this.getWrapper(oldChannel); // We want originalURI, this will provide a moz-ext rather than jar or file // uri on redirects. if (this.hasRedirects) { this.runChannelListener(channel, "onBeforeRedirect", { redirectUrl: newChannel.originalURI.spec, }); } channel.channel = newChannel; }, }; function HttpEvent(internalEvent, options) { this.internalEvent = internalEvent; this.options = options; } HttpEvent.prototype = { addListener(callback, filter = null, options = null, optionsObject = null) { let opts = parseExtra(options, this.options, optionsObject); opts.filter = parseFilter(filter); HttpObserverManager.addListener(this.internalEvent, callback, opts); }, removeListener(callback) { HttpObserverManager.removeListener(this.internalEvent, callback); }, }; var onBeforeRequest = new HttpEvent("onBeforeRequest", [ "blocking", "requestBody", ]); var onBeforeSendHeaders = new HttpEvent("onBeforeSendHeaders", [ "requestHeaders", "blocking", ]); var onSendHeaders = new HttpEvent("onSendHeaders", ["requestHeaders"]); var onHeadersReceived = new HttpEvent("onHeadersReceived", [ "blocking", "responseHeaders", ]); var onAuthRequired = new HttpEvent("onAuthRequired", [ "blocking", "responseHeaders", ]); var onBeforeRedirect = new HttpEvent("onBeforeRedirect", ["responseHeaders"]); var onResponseStarted = new HttpEvent("onResponseStarted", ["responseHeaders"]); var onCompleted = new HttpEvent("onCompleted", ["responseHeaders"]); var onErrorOccurred = new HttpEvent("onErrorOccurred"); export var WebRequest = { setDNRHandlingEnabled: dnrActive => { HttpObserverManager.setDNRHandlingEnabled(dnrActive); }, getTabIdForChannelWrapper: channel => { // Warning: This method should only be called after the initialization of // ExtensionParent.apiManager.global. Generally, this means that this method // should only be used by implementations of extension API methods (which // themselves are loaded in ExtensionParent.apiManager.global and therefore // imply the initialization of ExtensionParent.apiManager.global). return HttpObserverManager.getBrowserData(channel).tabId; }, onBeforeRequest, onBeforeSendHeaders, onSendHeaders, onHeadersReceived, onAuthRequired, onBeforeRedirect, onResponseStarted, onCompleted, onErrorOccurred, getSecurityInfo: details => { let channel = ChannelWrapper.getRegisteredChannel( details.id, details.policy, details.remoteTab ); if (channel) { return lazy.SecurityInfo.getSecurityInfo( channel.channel, details.options ); } }, };