Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetter(
lazy,
"obliviousHttpService",
"@mozilla.org/network/oblivious-http-service;1",
"nsIObliviousHttpService"
);
/**
* For now, the configuration and relay we're using is the "common" one, but
* the prefs are stored in a newtab-specific way. In the future, it's possible
* that we'll have different OHTTP configurations and relays for different
* types of resource requests, so we map the moz-cached-ohttp host to relay and
* config prefs here.
*/
const HOST_MAP = new Map([
[
"newtab-image",
{
gatewayConfigURLPrefName:
"browser.newtabpage.activity-stream.discoverystream.ohttp.configURL",
relayURLPrefName:
"browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL",
},
],
]);
/**
* Protocol handler for the moz-cached-ohttp:// scheme. This handler enables
* loading resources over Oblivious HTTP (OHTTP) from privileged about: content
* processes, specifically for use for images in about:newtab. The handler
* implements a cache-first strategy to minimize OHTTP requests while providing
* fallback to OHTTP when resources are not available in the HTTP cache.
*/
export class MozCachedOHTTPProtocolHandler {
/**
* The protocol scheme handled by this handler.
*/
scheme = "moz-cached-ohttp";
/**
* Injectable OHTTP service for testing. If null, uses the default service.
*/
#injectedOHTTPService = null;
/**
* Determines whether a given port is allowed for this protocol.
*
* @param {number} _port
* The port number to check.
* @param {string} _scheme
* The protocol scheme.
* @returns {boolean}
* Always false as this protocol doesn't use ports.
*/
allowPort(_port, _scheme) {
return false;
}
/**
* Creates a new channel for handling moz-cached-ohttp:// URLs.
*
* @param {nsIURI} uri
* The URI to create a channel for.
* @param {nsILoadInfo} loadInfo
* Load information containing security context.
* @returns {MozCachedOHTTPChannel}
* A new channel instance.
* @throws {Components.Exception}
* If the request is not from a valid context
*/
newChannel(uri, loadInfo) {
// Check if we're in a privileged about content process
if (
Services.appinfo.remoteType == lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
) {
return new MozCachedOHTTPChannel(
uri,
loadInfo,
this,
this.#getOHTTPService()
);
}
// In main process, allow system principals and check loading principal's remote type
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
const loadingPrincipal = loadInfo?.loadingPrincipal;
if (loadingPrincipal) {
// Allow system principals in parent process (for tests and internal usage)
if (loadingPrincipal.isSystemPrincipal) {
return new MozCachedOHTTPChannel(
uri,
loadInfo,
this,
this.#getOHTTPService()
);
}
try {
const remoteType =
lazy.E10SUtils.getRemoteTypeForPrincipal(loadingPrincipal);
if (remoteType === lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE) {
return new MozCachedOHTTPChannel(
uri,
loadInfo,
this,
this.#getOHTTPService()
);
}
} catch (e) {
// E10SUtils might throw for invalid principals, fall through to rejection
}
}
}
throw Components.Exception(
"moz-cached-ohttp protocol only accessible from privileged about content",
Cr.NS_ERROR_INVALID_ARG
);
}
/**
* Gets both the gateway configuration and relay URI for making OHTTP
* requests.
*
* @param {string} host
* A key for an entry in HOST_MAP that determines which OHTTP configuration
* and relay will be used for the request (e.g., "newtab-image").
* @returns {Promise<{ohttpGatewayConfig: Uint8Array, relayURI: nsIURI}>}
* Promise that resolves to an object containing both OHTTP components:
* @returns {Uint8Array} returns.ohttpGatewayConfig
* The binary OHTTP gateway configuration
* @returns {nsIURI} returns.relayURI
* The nsIURI for the OHTTP relay endpoint
* @throws {Error}
* If the host is unrecognized, or if either the gateway config URL or relay
* URL preferences are not configured, or if fetching the gateway
* configuration fails.
*/
async getOHTTPGatewayConfigAndRelayURI(host) {
let hostMapping = HOST_MAP.get(host);
if (!hostMapping) {
throw new Error(`Unrecognized host for OHTTP config: ${host}`);
}
if (
hostMapping.gatewayConfigURL === undefined ||
hostMapping.relayURL === undefined
) {
XPCOMUtils.defineLazyPreferenceGetter(
hostMapping,
"gatewayConfigURL",
hostMapping.gatewayConfigURLPrefName,
""
);
XPCOMUtils.defineLazyPreferenceGetter(
hostMapping,
"relayURL",
hostMapping.relayURLPrefName,
""
);
}
if (!hostMapping.gatewayConfigURL) {
throw new Error(
`OHTTP Gateway config URL not configured for host: ${host}`
);
}
if (!hostMapping.relayURL) {
throw new Error(`OHTTP relay URL not configured for host: ${host}`);
}
const ohttpGatewayConfig = await lazy.ObliviousHTTP.getOHTTPConfig(
hostMapping.gatewayConfigURL
);
return {
ohttpGatewayConfig,
relayURI: Services.io.newURI(hostMapping.relayURL),
};
}
/**
* Injects an OHTTP service for testing purposes.
*
* @param {nsIObliviousHttpService} service
* The service to inject, or null to use default.
*/
injectOHTTPService(service) {
this.#injectedOHTTPService = service;
}
/**
* Gets the OHTTP service to use (injected or default).
*
* @returns {nsIObliviousHttpService}
* The OHTTP service to use.
*/
#getOHTTPService() {
return this.#injectedOHTTPService || lazy.obliviousHttpService;
}
QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]);
}
/**
* Channel implementation for moz-cached-ohttp:// URLs. This channel first attempts
* to load resources from the HTTP cache to avoid unnecessary OHTTP requests, and
* falls back to loading via OHTTP if the resource is not cached.
*/
export class MozCachedOHTTPChannel {
#uri;
#loadInfo;
#protocolHandler;
#ohttpService;
#listener = null;
#loadFlags = 0;
#status = Cr.NS_OK;
#cancelled = false;
#originalURI;
#contentType = "";
#contentLength = -1;
#owner = null;
#securityInfo = null;
#notificationCallbacks = null;
#loadGroup = null;
#pendingChannel = null;
#startedRequest = false;
/**
* Constructs a new MozCachedOHTTPChannel.
*
* @param {nsIURI} uri
* The moz-cached-ohttp:// URI to handle.
* @param {nsILoadInfo} loadInfo
* Load information for the request with security context.
* @param {MozCachedOHTTPProtocolHandler} protocolHandler
* The protocol handler instance that created this channel.
* @param {nsIObliviousHttpService} ohttpService
* The OHTTP service to use.
*/
constructor(uri, loadInfo, protocolHandler, ohttpService) {
this.#uri = uri;
this.#loadInfo = loadInfo;
this.#protocolHandler = protocolHandler;
this.#ohttpService = ohttpService;
this.#originalURI = uri;
}
/**
* Gets the URI for this channel.
*
* @returns {nsIURI}
* The channel's URI.
*/
get URI() {
return this.#uri;
}
/**
* Gets or sets the original URI for this channel.
*
* @type {nsIURI}
*/
get originalURI() {
return this.#originalURI;
}
set originalURI(aURI) {
this.#originalURI = aURI;
}
/**
* Gets the current status of the channel.
*
* @returns {number}
* The channel status (nsresult).
*/
get status() {
return this.#status;
}
/**
* Gets or sets the content type of the loaded resource.
*
* @type {string}
*/
get contentType() {
return this.#contentType;
}
set contentType(aContentType) {
this.#contentType = aContentType;
}
/**
* Gets or sets the content length of the loaded resource.
*
* @type {number}
* The content length in bytes, or -1 if unknown.
*/
get contentLength() {
return this.#contentLength;
}
set contentLength(aContentLength) {
this.#contentLength = aContentLength;
}
/**
* Gets or sets the load flags for this channel.
*
* @type {number}
*/
get loadFlags() {
return this.#loadFlags;
}
set loadFlags(aLoadFlags) {
this.#loadFlags = aLoadFlags;
}
/**
* Gets or sets the load info for this channel.
*
* @type {nsILoadInfo}
*/
get loadInfo() {
return this.#loadInfo;
}
set loadInfo(aLoadInfo) {
this.#loadInfo = aLoadInfo;
}
/**
* Gets or sets the owner (principal) of this channel.
*
* @type {nsIPrincipal}
*/
get owner() {
return this.#owner;
}
set owner(aOwner) {
this.#owner = aOwner;
}
/**
* Gets the security info for this channel.
*
* @returns {nsITransportSecurityInfo}
* The security information for this channel.
*/
get securityInfo() {
return this.#securityInfo;
}
/**
* Gets or sets the notification callbacks for this channel.
*
* @type {nsIInterfaceRequestor}
*/
get notificationCallbacks() {
return this.#notificationCallbacks;
}
set notificationCallbacks(aCallbacks) {
this.#notificationCallbacks = aCallbacks;
}
/**
* Gets or sets the load group for this channel.
*
* @type {nsILoadGroup}
*/
get loadGroup() {
return this.#loadGroup;
}
set loadGroup(aLoadGroup) {
this.#loadGroup = aLoadGroup;
}
/**
* Gets the name of this channel (its URI spec).
*
* @returns {string}
* The channel name.
*/
get name() {
return this.#uri.spec;
}
/**
* Checks if this channel has a pending request.
*
* @returns {boolean}
* True if there is a pending request.
*/
isPending() {
return this.#pendingChannel ? this.#pendingChannel.isPending() : false;
}
/**
* Cancels the channel with the given status.
*
* @param {number} status
* The status code to cancel with.
*/
cancel(status) {
this.#cancelled = true;
this.#status = status;
if (this.#pendingChannel) {
this.#pendingChannel.cancel(status);
}
}
/**
* Suspends the channel if it has a pending request.
*/
suspend() {
if (this.#pendingChannel) {
this.#pendingChannel.suspend();
}
}
/**
* Resumes the channel if it has a pending request.
*/
resume() {
if (this.#pendingChannel) {
this.#pendingChannel.resume();
}
}
/**
* Opens the channel synchronously. Not supported for this protocol.
*
* @throws {Components.Exception}
* Always throws as sync open is not supported.
*/
open() {
throw Components.Exception(
"moz-cached-ohttp protocol does not support synchronous open",
Cr.NS_ERROR_NOT_IMPLEMENTED
);
}
/**
* Opens the channel asynchronously.
*
* @param {nsIStreamListener} listener
* The stream listener to notify.
* @throws {Components.Exception}
* If the channel was already cancelled.
*/
asyncOpen(listener) {
if (this.#cancelled) {
throw Components.Exception("Channel was cancelled", this.#status);
}
this.#listener = listener;
this.#loadResource().catch(error => {
console.error("moz-cached-ohttp channel error:", error);
this.#notifyError(Cr.NS_ERROR_FAILURE);
});
}
/**
* Loads the requested resource using a cache-first strategy.
* First attempts to load from HTTP cache, then falls back to OHTTP if not
* cached.
*
* @returns {Promise<undefined>}
* Promise that resolves when loading is complete.
*/
async #loadResource() {
const { resourceURI, host } = this.#extractHostAndResourceURI();
if (!resourceURI) {
throw new Error("Invalid moz-cached-ohttp URL format");
}
// Try cache first (avoid network racing)
const wasCached = await this.#tryCache(resourceURI);
if (wasCached) {
// Nothing to do - we'll be streaming the response from the cache.
return;
}
// Fallback to OHTTP
if (!HOST_MAP.has(host)) {
throw new Error(`Unrecognized moz-cached-ohttp host: ${host}`);
}
await this.#loadViaOHTTP(resourceURI, host);
}
/**
* Extracts both the host and target resource URL from the moz-cached-ohttp://
* URL.
*
*
* @returns {{resourceURI: nsIURI, host: string}|null}
* Object containing extracted data, or null if invalid.
* Returns null if URL parsing fails, no 'url' parameter found,
* target URL is not HTTPS, or target URL is malformed.
* @returns {nsIURI} returns.resourceURI - The decoded target image URI (HTTPS only)
* @returns {string} returns.host - The moz-cached-ohttp host (e.g., "newtab-image")
*/
#extractHostAndResourceURI() {
try {
const url = new URL(this.#uri.spec);
const host = url.host;
const searchParams = new URLSearchParams(url.search);
const resourceURLString = searchParams.get("url");
if (!resourceURLString) {
return null;
}
// Validate that the extracted URL is a valid HTTP/HTTPS URL
const resourceURL = new URL(resourceURLString);
if (resourceURL.protocol !== "https:") {
return null;
}
return { resourceURI: Services.io.newURI(resourceURLString), host };
} catch (e) {
return null;
}
}
/**
* Attempts to load the resource from the HTTP cache without making network
* requests.
*
* @param {nsIURI} resourceURI
* The URI of the resource to load from cache
* @returns {Promise<boolean>}
* Promise that resolves to true if loaded from cache
*/
async #tryCache(resourceURI) {
let result;
// Bug 1977139: Transferring nsIInputStream currently causes crashes
// from in-process child actors, so only use the MozCachedOHTTPChild
// if we're running outside of the parent process. If we're running in the
// parent process, we can just talk to the parent actor directly.
if (
Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT
) {
const [parentProcess] = ChromeUtils.getAllDOMProcesses();
const parentActor = parentProcess.getActor("MozCachedOHTTP");
result = await parentActor.tryCache(resourceURI);
} else {
const childActor = ChromeUtils.domProcessChild.getActor("MozCachedOHTTP");
result = await childActor.sendQuery("tryCache", {
uriString: resourceURI.spec,
});
}
if (result.success) {
// Stream from parent-provided inputStream
const pump = this.#createInputStreamPump(result.inputStream);
await this.#streamFromCache(pump, result.inputStream);
return true;
}
return false;
}
/**
* Creates an input stream pump for efficient data streaming.
*
* @param {nsIInputStream} inputStream
* The input stream to pump.
* @returns {nsIInputStreamPump}
* The configured pump.
*/
#createInputStreamPump(inputStream) {
const pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
Ci.nsIInputStreamPump
);
pump.init(inputStream, 0, 0, false);
return pump;
}
/**
* Streams data from cache to the client listener.
*
* @param {nsIInputStreamPump} pump
* The input stream pump.
* @param {nsIInputStream} inputStream
* The input stream for cleanup.
* @returns {Promise<boolean>}
* Promise that resolves to true on success.
*/
#streamFromCache(pump, inputStream) {
return new Promise((resolve, reject) => {
const listener = this.#createCacheStreamListener(
pump,
inputStream,
resolve
);
try {
pump.asyncRead(listener);
} catch (error) {
this.#safeCloseStream(inputStream);
reject(error);
}
});
}
/**
* Creates a stream listener for cache data streaming.
*
* @param {nsIInputStreamPump} pump
* The pump for channel tracking.
* @param {nsIInputStream} inputStream
* Input stream for cleanup.
* @param {Function} resolve
* Promise resolve function.
* @returns {object}
* Stream listener implementation.
*/
#createCacheStreamListener(pump, inputStream, resolve) {
return {
onStartRequest: () => {
this.#startedRequest = true;
this.#pendingChannel = pump;
this.#listener.onStartRequest(this);
},
onDataAvailable: (request, innerInputStream, offset, count) => {
this.#listener.onDataAvailable(this, innerInputStream, offset, count);
},
onStopRequest: (request, status) => {
this.#pendingChannel = null;
this.#safeCloseStream(inputStream);
this.#listener.onStopRequest(this, status);
resolve(Components.isSuccessCode(status));
},
};
}
/**
* Safely closes an input stream, ignoring errors.
*
* @param {nsIInputStream} stream
* The stream to close.
*/
#safeCloseStream(stream) {
try {
stream.close();
} catch (e) {
// Ignore close errors
}
}
/**
* Loads the resource via Oblivious HTTP when it's not available in cache.
* Manually writes the response to the HTTP cache for future requests.
*
* @param {nsIURI} resourceURI
* The URI of the resource to load via OHTTP
* @param {string} host
* A host matching one of the entries in HOST_MAP
* @returns {Promise<void>} Promise that resolves when OHTTP loading is complete
*/
async #loadViaOHTTP(resourceURI, host) {
const { ohttpGatewayConfig, relayURI } =
await this.#protocolHandler.getOHTTPGatewayConfigAndRelayURI(host);
const ohttpChannel = this.#createOHTTPChannel(
relayURI,
resourceURI,
ohttpGatewayConfig
);
return this.#executeOHTTPRequest(ohttpChannel);
}
/**
* Creates and configures an OHTTP channel.
*
* @param {nsIURI} relayURI
* The OHTTP relay URI.
* @param {nsIURI} resourceURI
* The target resource URI.
* @param {Uint8Array} ohttpConfig
* The OHTTP configuration.
* @returns {nsIChannel}
* The configured OHTTP channel.
*/
#createOHTTPChannel(relayURI, resourceURI, ohttpConfig) {
ChromeUtils.releaseAssert(
Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT ||
Services.appinfo.remoteType ===
lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE,
"moz-cached-ohttp is only allowed in privileged content processes " +
"or the main process"
);
const ohttpChannel = this.#ohttpService.newChannel(
relayURI,
resourceURI,
ohttpConfig
);
// I'm not sure I love this, but in order for the privileged about content
// process to make requests to the relay and not get dinged by
// OpaqueResponseBlocking, we need to make the load come from the system
// principal.
const loadInfo = lazy.NetUtil.newChannel({
uri: relayURI,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
}).loadInfo;
// Copy relevant properties from our channel to the OHTTP channel
ohttpChannel.loadInfo = loadInfo;
ohttpChannel.loadFlags = this.#loadFlags | Ci.nsIRequest.LOAD_ANONYMOUS;
if (this.#notificationCallbacks) {
ohttpChannel.notificationCallbacks = this.#notificationCallbacks;
}
if (this.#loadGroup) {
ohttpChannel.loadGroup = this.#loadGroup;
}
return ohttpChannel;
}
/**
* Executes the OHTTP request with caching support.
*
* @param {nsIChannel} ohttpChannel
* The OHTTP channel to execute.
* @returns {Promise<undefined, Error>}
* Promise that resolves when request is complete.
*/
#executeOHTTPRequest(ohttpChannel) {
this.#pendingChannel = ohttpChannel;
let cachePipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
cachePipe.init(
true /* non-blocking input */,
false /* blocking output */,
0 /* segment size */,
0 /* max segments */
);
let cacheStreamUpdate = new MessageChannel();
return new Promise((resolve, reject) => {
// Bug 1977139: Transferring nsIInputStream currently causes crashes
// from in-process child actors, so only use the MozCachedOHTTPChild
// if we're running outside of the parent process. If we're running in the
// parent process, we can just talk to the parent actor directly.
if (
Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT
) {
const [parentProcess] = ChromeUtils.getAllDOMProcesses();
const parentActor = parentProcess.getActor("MozCachedOHTTP");
parentActor.writeCache(
ohttpChannel.URI,
cachePipe.inputStream,
cacheStreamUpdate.port2
);
} else {
const childActor =
ChromeUtils.domProcessChild.getActor("MozCachedOHTTP");
childActor.sendAsyncMessage(
"writeCache",
{
uriString: ohttpChannel.URI.spec,
cacheInputStream: cachePipe.inputStream,
cacheStreamUpdatePort: cacheStreamUpdate.port2,
},
[cacheStreamUpdate.port2]
);
}
const originalListener = this.#createOHTTPResponseListener(
cacheStreamUpdate.port1,
cachePipe.outputStream,
resolve,
reject
);
const finalListener = this.#setupStreamTee(
originalListener,
cachePipe.outputStream
);
try {
ohttpChannel.asyncOpen(finalListener);
} catch (error) {
this.#cleanupCacheOnError(
cacheStreamUpdate.port1,
cachePipe.outputStream
);
reject(error);
}
});
}
/**
* Creates a response listener for OHTTP requests with cache handling.
*
* @param {MessageChannelPort} cacheStreamUpdatePort
* MessagePort for sending cache control messages to the parent actor
* for operations like setting expiration time and dooming cache entries.
* @param {nsIOutputStream} cachePipeOutputStream
* Output stream of the pipe used to send response data to the cache
* writer in the parent process via the JSActor.
* @param {Function} resolve
* Promise resolve function.
* @param {Function} reject
* Promise reject function.
* @returns {nsIStreamListener}
* Stream listener implementation
*/
#createOHTTPResponseListener(
cacheStreamUpdatePort,
cachePipeOutputStream,
resolve,
reject
) {
return {
onStartRequest: request => {
// Copy content type from the OHTTP response to our channel
if (request instanceof Ci.nsIHttpChannel) {
try {
const contentType = request.getResponseHeader("content-type");
if (contentType) {
this.#contentType = contentType;
}
} catch (e) {
// Content-Type header not available
}
}
this.#startedRequest = true;
this.#listener.onStartRequest(this);
this.#processCacheControl(cacheStreamUpdatePort, request);
},
onDataAvailable: (request, inputStream, offset, count) => {
this.#listener.onDataAvailable(this, inputStream, offset, count);
},
onStopRequest: (request, status) => {
this.#pendingChannel = null;
this.#finalizeCacheEntry(
cacheStreamUpdatePort,
cachePipeOutputStream,
status
);
this.#listener.onStopRequest(this, status);
if (Components.isSuccessCode(status)) {
resolve();
} else {
reject(new Error(`OHTTP request failed with status: ${status}`));
}
},
};
}
/**
* Sets up stream tee for cache writing if cache output stream is available.
*
* @param {nsIStreamListener} originalListener
* The original stream listener.
* @param {nsIOutputStream} cacheOutputStream
* Cache output stream (may be null).
* @returns {nsIStreamListener}
* Final listener (tee or original).
*/
#setupStreamTee(originalListener, cacheOutputStream) {
if (!cacheOutputStream) {
return originalListener;
}
try {
const tee = Cc[
"@mozilla.org/network/stream-listener-tee;1"
].createInstance(Ci.nsIStreamListenerTee);
tee.init(originalListener, cacheOutputStream, null);
return tee;
} catch (error) {
console.warn(
"Failed to create stream tee, proceeding without caching:",
error
);
this.#safeCloseStream(cacheOutputStream);
return originalListener;
}
}
/**
* Finalizes cache entry after response completion.
*
* @param {MessageChannelPort} cacheStreamUpdatePort
* MessagePort for sending cache finalization messages to the parent actor.
* Used to doom the cache entry if the response failed.
* @param {nsIOutputStream} cacheOutputStream
* Output stream to close.
* @param {number} status
* Response status code.
*/
#finalizeCacheEntry(cacheStreamUpdatePort, cacheOutputStream, status) {
try {
if (cacheOutputStream) {
cacheOutputStream.closeWithStatus(Cr.NS_BASE_STREAM_CLOSED);
}
if (!Components.isSuccessCode(status)) {
cacheStreamUpdatePort.postMessage({ name: "DoomCacheEntry" });
}
} catch (error) {
console.warn("Failed to finalize cache entry:", error);
}
}
/**
* Cleans up cache resources on error.
*
* @param {MessageChannelPort} cacheStreamUpdatePort
* MessagePort for sending error cleanup messages to the parent actor.
* Used to doom the cache entry when an error occurs during the request.
*/
#cleanupCacheOnError(cacheStreamUpdatePort, cacheOutputStream) {
try {
if (cacheOutputStream) {
cacheOutputStream.closeWithStatus(Cr.NS_BASE_STREAM_CLOSED);
}
cacheStreamUpdatePort.postMessage({ name: "DoomCacheEntry" });
} catch (e) {
// Ignore cleanup errors
}
}
/**
* Notifies the listener of an error condition.
*
* @param {number} status
* The error status code.
*/
#notifyError(status) {
this.#status = status;
if (this.#listener) {
// Depending on when the error occurred, we may have already started
// the request - but if not, start it.
if (!this.#startedRequest) {
this.#listener.onStartRequest(this);
}
this.#listener.onStopRequest(this, status);
}
}
/**
* Sets appropriate cache expiration metadata based on response headers.
*
* @param {MessageChannelPort} cacheStreamUpdatePort
* MessagePort for sending cache metadata messages to the parent actor.
* Used to set cache expiration time and handle cache-control directives.
* @param {nsIHttpChannel} httpChannel
* The HTTP channel with response headers.
*/
#processCacheControl(cacheStreamUpdatePort, httpChannel) {
if (!(httpChannel instanceof Ci.nsIHttpChannel)) {
return;
}
let expirationTime = null;
// Check for Cache-Control header first (takes precedence)
let cacheControl = null;
try {
cacheControl = httpChannel.getResponseHeader("cache-control");
} catch (e) {
// Cache-Control header not available, continue to check Expires
}
if (cacheControl) {
let cacheControlParseResult =
Services.io.parseCacheControlHeader(cacheControl);
// Respect max-age directive
if (cacheControlParseResult.maxAge) {
expirationTime = Date.now() + cacheControlParseResult.maxAge * 1000;
} else if (
cacheControlParseResult.noCache ||
cacheControlParseResult.noStore
) {
// Don't cache if explicitly prohibited
cacheStreamUpdatePort.postMessage({ name: "DoomCacheEntry" });
return;
}
}
// Fallback to Expires header if Cache-Control max-age not found
if (!expirationTime) {
try {
const expires = httpChannel.getResponseHeader("expires");
if (expires) {
expirationTime = new Date(expires).getTime();
}
} catch (e) {
// Expires header not available
}
}
// Set default expiration if no headers found (24 hours)
expirationTime ??= Date.now() + 24 * 60 * 60 * 1000; // 24 hours
// Set the expiration time in cache metadata
cacheStreamUpdatePort.postMessage({
name: "WriteCacheExpiry",
expiry: Math.floor(expirationTime / 1000),
});
}
QueryInterface = ChromeUtils.generateQI(["nsIChannel"]);
}