tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

MozCachedOHTTPProtocolHandler.sys.mjs (32430B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs",
     11  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
     12  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     13 });
     14 
     15 XPCOMUtils.defineLazyServiceGetter(
     16  lazy,
     17  "obliviousHttpService",
     18  "@mozilla.org/network/oblivious-http-service;1",
     19  Ci.nsIObliviousHttpService
     20 );
     21 
     22 /**
     23 * For now, the configuration and relay we're using is the "common" one, but
     24 * the prefs are stored in a newtab-specific way. In the future, it's possible
     25 * that we'll have different OHTTP configurations and relays for different
     26 * types of resource requests, so we map the moz-cached-ohttp host to relay and
     27 * config prefs here.
     28 */
     29 const HOST_MAP = new Map([
     30  [
     31    "newtab-image",
     32    {
     33      gatewayConfigURLPrefName:
     34        "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL",
     35      relayURLPrefName:
     36        "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL",
     37    },
     38  ],
     39 ]);
     40 
     41 /**
     42 * Protocol handler for the moz-cached-ohttp:// scheme. This handler enables
     43 * loading resources over Oblivious HTTP (OHTTP) from privileged about: content
     44 * processes, specifically for use for images in about:newtab. The handler
     45 * implements a cache-first strategy to minimize OHTTP requests while providing
     46 * fallback to OHTTP when resources are not available in the HTTP cache.
     47 */
     48 export class MozCachedOHTTPProtocolHandler {
     49  /**
     50   * The protocol scheme handled by this handler.
     51   */
     52  scheme = "moz-cached-ohttp";
     53 
     54  /**
     55   * Injectable OHTTP service for testing. If null, uses the default service.
     56   */
     57  #injectedOHTTPService = null;
     58 
     59  /**
     60   * Determines whether a given port is allowed for this protocol.
     61   *
     62   * @param {number} _port
     63   *   The port number to check.
     64   * @param {string} _scheme
     65   *   The protocol scheme.
     66   * @returns {boolean}
     67   *   Always false as this protocol doesn't use ports.
     68   */
     69  allowPort(_port, _scheme) {
     70    return false;
     71  }
     72 
     73  /**
     74   * Creates a new channel for handling moz-cached-ohttp:// URLs.
     75   *
     76   * @param {nsIURI} uri
     77   *   The URI to create a channel for.
     78   * @param {nsILoadInfo} loadInfo
     79   *   Load information containing security context.
     80   * @returns {MozCachedOHTTPChannel}
     81   *   A new channel instance.
     82   * @throws {Components.Exception}
     83   *   If the request is not from a valid context
     84   */
     85  newChannel(uri, loadInfo) {
     86    // Check if we're in a privileged about content process
     87    if (
     88      Services.appinfo.remoteType == lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
     89    ) {
     90      return new MozCachedOHTTPChannel(
     91        uri,
     92        loadInfo,
     93        this,
     94        this.#getOHTTPService(),
     95        !!this.#injectedOHTTPService /* inTestingMode */
     96      );
     97    }
     98 
     99    // In main process, allow system principals and check loading principal's remote type
    100    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
    101      const loadingPrincipal = loadInfo?.loadingPrincipal;
    102      if (loadingPrincipal) {
    103        // Allow system principals in parent process (for tests and internal usage)
    104        if (loadingPrincipal.isSystemPrincipal) {
    105          return new MozCachedOHTTPChannel(
    106            uri,
    107            loadInfo,
    108            this,
    109            this.#getOHTTPService(),
    110            !!this.#injectedOHTTPService /* inTestingMode */
    111          );
    112        }
    113 
    114        try {
    115          const remoteType =
    116            lazy.E10SUtils.getRemoteTypeForPrincipal(loadingPrincipal);
    117          if (remoteType === lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE) {
    118            return new MozCachedOHTTPChannel(
    119              uri,
    120              loadInfo,
    121              this,
    122              this.#getOHTTPService(),
    123              !!this.#injectedOHTTPService /* inTestingMode */
    124            );
    125          }
    126        } catch (e) {
    127          // E10SUtils might throw for invalid principals, fall through to rejection
    128        }
    129      }
    130    }
    131 
    132    throw Components.Exception(
    133      "moz-cached-ohttp protocol only accessible from privileged about content",
    134      Cr.NS_ERROR_INVALID_ARG
    135    );
    136  }
    137 
    138  /**
    139   * Gets both the gateway configuration and relay URI for making OHTTP
    140   * requests.
    141   *
    142   * @param {string} host
    143   *   A key for an entry in HOST_MAP that determines which OHTTP configuration
    144   *   and relay will be used for the request (e.g., "newtab-image").
    145   * @returns {Promise<{ohttpGatewayConfig: Uint8Array, relayURI: nsIURI}>}
    146   *   Promise that resolves to an object containing both OHTTP components:
    147   * @returns {Uint8Array} returns.ohttpGatewayConfig
    148   *   The binary OHTTP gateway configuration
    149   * @returns {nsIURI} returns.relayURI
    150   *   The nsIURI for the OHTTP relay endpoint
    151   * @throws {Error}
    152   *   If the host is unrecognized, or if either the gateway config URL or relay
    153   *   URL preferences are not configured, or if fetching the gateway
    154   *   configuration fails.
    155   */
    156  async getOHTTPGatewayConfigAndRelayURI(host) {
    157    let hostMapping = HOST_MAP.get(host);
    158    if (!hostMapping) {
    159      throw new Error(`Unrecognized host for OHTTP config: ${host}`);
    160    }
    161    if (
    162      hostMapping.gatewayConfigURL === undefined ||
    163      hostMapping.relayURL === undefined
    164    ) {
    165      XPCOMUtils.defineLazyPreferenceGetter(
    166        hostMapping,
    167        "gatewayConfigURL",
    168        hostMapping.gatewayConfigURLPrefName,
    169        ""
    170      );
    171      XPCOMUtils.defineLazyPreferenceGetter(
    172        hostMapping,
    173        "relayURL",
    174        hostMapping.relayURLPrefName,
    175        ""
    176      );
    177    }
    178    if (!hostMapping.gatewayConfigURL) {
    179      throw new Error(
    180        `OHTTP Gateway config URL not configured for host: ${host}`
    181      );
    182    }
    183    if (!hostMapping.relayURL) {
    184      throw new Error(`OHTTP relay URL not configured for host: ${host}`);
    185    }
    186 
    187    const ohttpGatewayConfig = await lazy.ObliviousHTTP.getOHTTPConfig(
    188      hostMapping.gatewayConfigURL
    189    );
    190 
    191    return {
    192      ohttpGatewayConfig,
    193      relayURI: Services.io.newURI(hostMapping.relayURL),
    194    };
    195  }
    196 
    197  /**
    198   * Injects an OHTTP service for testing purposes.
    199   *
    200   * @param {nsIObliviousHttpService} service
    201   *   The service to inject, or null to use default.
    202   */
    203  injectOHTTPService(service) {
    204    this.#injectedOHTTPService = service;
    205  }
    206 
    207  /**
    208   * Gets the OHTTP service to use (injected or default).
    209   *
    210   * @returns {nsIObliviousHttpService}
    211   *   The OHTTP service to use.
    212   */
    213  #getOHTTPService() {
    214    return this.#injectedOHTTPService || lazy.obliviousHttpService;
    215  }
    216 
    217  QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]);
    218 }
    219 
    220 /**
    221 * Channel implementation for moz-cached-ohttp:// URLs. This channel first attempts
    222 * to load resources from the HTTP cache to avoid unnecessary OHTTP requests, and
    223 * falls back to loading via OHTTP if the resource is not cached.
    224 */
    225 export class MozCachedOHTTPChannel {
    226  #uri;
    227  #loadInfo;
    228  #protocolHandler;
    229  #ohttpService;
    230  #listener = null;
    231  #loadFlags = 0;
    232  #status = Cr.NS_OK;
    233  #cancelled = false;
    234  #originalURI;
    235  #contentType = "";
    236  #contentCharset = "";
    237  #contentLength = -1;
    238  #owner = null;
    239  #securityInfo = null;
    240  #notificationCallbacks = null;
    241  #loadGroup = null;
    242  #pendingChannel = null;
    243  #startedRequest = false;
    244  #inTestingMode = false;
    245 
    246  /**
    247   * Constructs a new MozCachedOHTTPChannel.
    248   *
    249   * @param {nsIURI} uri
    250   *   The moz-cached-ohttp:// URI to handle.
    251   * @param {nsILoadInfo} loadInfo
    252   *   Load information for the request with security context.
    253   * @param {MozCachedOHTTPProtocolHandler} protocolHandler
    254   *   The protocol handler instance that created this channel.
    255   * @param {nsIObliviousHttpService} ohttpService
    256   *   The OHTTP service to use.
    257   * @param {boolean} inTestingMode
    258   *   True if the channel is in a mode that makes it easier to stub and
    259   *   mock things out while under test.
    260   */
    261  constructor(
    262    uri,
    263    loadInfo,
    264    protocolHandler,
    265    ohttpService,
    266    inTestingMode = false
    267  ) {
    268    this.#uri = uri;
    269    this.#loadInfo = loadInfo;
    270    this.#protocolHandler = protocolHandler;
    271    this.#ohttpService = ohttpService;
    272    this.#originalURI = uri;
    273    this.#inTestingMode = inTestingMode;
    274  }
    275 
    276  /**
    277   * Gets the URI for this channel.
    278   *
    279   * @returns {nsIURI}
    280   *   The channel's URI.
    281   */
    282  get URI() {
    283    return this.#uri;
    284  }
    285 
    286  /**
    287   * Gets or sets the original URI for this channel.
    288   *
    289   * @type {nsIURI}
    290   */
    291  get originalURI() {
    292    return this.#originalURI;
    293  }
    294 
    295  set originalURI(aURI) {
    296    this.#originalURI = aURI;
    297  }
    298 
    299  /**
    300   * Gets the current status of the channel.
    301   *
    302   * @returns {number}
    303   *   The channel status (nsresult).
    304   */
    305  get status() {
    306    return this.#status;
    307  }
    308 
    309  /**
    310   * Gets or sets the content type of the loaded resource.
    311   *
    312   * @type {string}
    313   */
    314  get contentType() {
    315    return this.#contentType;
    316  }
    317 
    318  set contentType(aContentType) {
    319    this.#contentType = aContentType;
    320  }
    321 
    322  /**
    323   * Gets or sets the content charset of the loaded resource.
    324   *
    325   * @type {string}
    326   */
    327  get contentCharset() {
    328    return this.#contentCharset;
    329  }
    330 
    331  set contentCharset(aContentCharset) {
    332    this.#contentCharset = aContentCharset;
    333  }
    334 
    335  /**
    336   * Gets or sets the content length of the loaded resource.
    337   *
    338   * @type {number}
    339   *   The content length in bytes, or -1 if unknown.
    340   */
    341  get contentLength() {
    342    return this.#contentLength;
    343  }
    344 
    345  set contentLength(aContentLength) {
    346    this.#contentLength = aContentLength;
    347  }
    348 
    349  /**
    350   * Gets or sets the load flags for this channel.
    351   *
    352   * @type {number}
    353   */
    354  get loadFlags() {
    355    return this.#loadFlags;
    356  }
    357 
    358  set loadFlags(aLoadFlags) {
    359    this.#loadFlags = aLoadFlags;
    360  }
    361 
    362  /**
    363   * Gets or sets the load info for this channel.
    364   *
    365   * @type {nsILoadInfo}
    366   */
    367  get loadInfo() {
    368    return this.#loadInfo;
    369  }
    370 
    371  set loadInfo(aLoadInfo) {
    372    this.#loadInfo = aLoadInfo;
    373  }
    374 
    375  /**
    376   * Gets or sets the owner (principal) of this channel.
    377   *
    378   * @type {nsIPrincipal}
    379   */
    380  get owner() {
    381    return this.#owner;
    382  }
    383 
    384  set owner(aOwner) {
    385    this.#owner = aOwner;
    386  }
    387 
    388  /**
    389   * Gets the security info for this channel.
    390   *
    391   * @returns {nsITransportSecurityInfo}
    392   *   The security information for this channel.
    393   */
    394  get securityInfo() {
    395    return this.#securityInfo;
    396  }
    397 
    398  /**
    399   * Gets or sets the notification callbacks for this channel.
    400   *
    401   * @type {nsIInterfaceRequestor}
    402   */
    403  get notificationCallbacks() {
    404    return this.#notificationCallbacks;
    405  }
    406 
    407  set notificationCallbacks(aCallbacks) {
    408    this.#notificationCallbacks = aCallbacks;
    409  }
    410 
    411  /**
    412   * Gets or sets the load group for this channel.
    413   *
    414   * @type {nsILoadGroup}
    415   */
    416  get loadGroup() {
    417    return this.#loadGroup;
    418  }
    419 
    420  set loadGroup(aLoadGroup) {
    421    this.#loadGroup = aLoadGroup;
    422  }
    423 
    424  /**
    425   * Gets the name of this channel (its URI spec).
    426   *
    427   * @returns {string}
    428   *   The channel name.
    429   */
    430  get name() {
    431    return this.#uri.spec;
    432  }
    433 
    434  /**
    435   * Checks if this channel has a pending request.
    436   *
    437   * @returns {boolean}
    438   *   True if there is a pending request.
    439   */
    440  isPending() {
    441    return this.#pendingChannel ? this.#pendingChannel.isPending() : false;
    442  }
    443 
    444  /**
    445   * Cancels the channel with the given status.
    446   *
    447   * @param {number} status
    448   *   The status code to cancel with.
    449   */
    450  cancel(status) {
    451    this.#cancelled = true;
    452    this.#status = status;
    453 
    454    if (this.#pendingChannel) {
    455      this.#pendingChannel.cancel(status);
    456    }
    457  }
    458 
    459  /**
    460   * Suspends the channel if it has a pending request.
    461   */
    462  suspend() {
    463    if (this.#pendingChannel) {
    464      this.#pendingChannel.suspend();
    465    }
    466  }
    467 
    468  /**
    469   * Resumes the channel if it has a pending request.
    470   */
    471  resume() {
    472    if (this.#pendingChannel) {
    473      this.#pendingChannel.resume();
    474    }
    475  }
    476 
    477  /**
    478   * Opens the channel synchronously. Not supported for this protocol.
    479   *
    480   * @throws {Components.Exception}
    481   *   Always throws as sync open is not supported.
    482   */
    483  open() {
    484    throw Components.Exception(
    485      "moz-cached-ohttp protocol does not support synchronous open",
    486      Cr.NS_ERROR_NOT_IMPLEMENTED
    487    );
    488  }
    489 
    490  /**
    491   * Opens the channel asynchronously.
    492   *
    493   * @param {nsIStreamListener} listener
    494   *   The stream listener to notify.
    495   * @throws {Components.Exception}
    496   *   If the channel was already cancelled.
    497   */
    498  asyncOpen(listener) {
    499    if (this.#cancelled) {
    500      throw Components.Exception("Channel was cancelled", this.#status);
    501    }
    502 
    503    this.#listener = listener;
    504 
    505    this.#loadResource().catch(error => {
    506      console.error("moz-cached-ohttp channel error:", error);
    507      this.#notifyError(Cr.NS_ERROR_FAILURE);
    508    });
    509  }
    510 
    511  /**
    512   * Loads the requested resource using a cache-first strategy.
    513   * First attempts to load from HTTP cache, then falls back to OHTTP if not
    514   * cached.
    515   *
    516   * @returns {Promise<undefined>}
    517   *   Promise that resolves when loading is complete.
    518   */
    519  async #loadResource() {
    520    const { resourceURI, host } = this.#extractHostAndResourceURI();
    521    if (!resourceURI) {
    522      throw new Error("Invalid moz-cached-ohttp URL format");
    523    }
    524 
    525    // Try cache first (avoid network racing)
    526    const wasCached = await this.#tryCache(resourceURI);
    527    if (wasCached) {
    528      // Nothing to do - we'll be streaming the response from the cache.
    529      return;
    530    }
    531 
    532    // Fallback to OHTTP
    533    if (!HOST_MAP.has(host)) {
    534      throw new Error(`Unrecognized moz-cached-ohttp host: ${host}`);
    535    }
    536 
    537    await this.#loadViaOHTTP(resourceURI, host);
    538  }
    539 
    540  /**
    541   * Extracts both the host and target resource URL from the moz-cached-ohttp://
    542   * URL.
    543   *
    544   * Expected format: moz-cached-ohttp://newtab-image/?url=<encoded-https-url>
    545   *
    546   * @returns {{resourceURI: nsIURI, host: string}|null}
    547   *   Object containing extracted data, or null if invalid.
    548   *   Returns null if URL parsing fails, no 'url' parameter found,
    549   *   target URL is not HTTPS, or target URL is malformed.
    550   *   `resourceURI` is the decoded target image URI (HTTPS only).
    551   *   `host` is the moz-cached-ohttp host (e.g., "newtab-image").
    552   */
    553  #extractHostAndResourceURI() {
    554    try {
    555      const url = new URL(this.#uri.spec);
    556      const host = url.host;
    557      const searchParams = new URLSearchParams(url.search);
    558      const resourceURLString = searchParams.get("url");
    559 
    560      if (!resourceURLString) {
    561        return null;
    562      }
    563 
    564      // Validate that the extracted URL is a valid HTTP/HTTPS URL
    565      const resourceURL = new URL(resourceURLString);
    566      if (resourceURL.protocol !== "https:") {
    567        return null;
    568      }
    569 
    570      return { resourceURI: Services.io.newURI(resourceURLString), host };
    571    } catch (e) {
    572      return null;
    573    }
    574  }
    575 
    576  /**
    577   * Attempts to load the resource from the HTTP cache without making network
    578   * requests.
    579   *
    580   * @param {nsIURI} resourceURI
    581   *   The URI of the resource to load from cache
    582   * @returns {Promise<boolean>}
    583   *   Promise that resolves to true if loaded from cache
    584   */
    585  async #tryCache(resourceURI) {
    586    let result;
    587 
    588    // Bug 1977139: Transferring nsIInputStream currently causes crashes
    589    // from in-process child actors, so only use the MozCachedOHTTPChild
    590    // if we're running outside of the parent process. If we're running in the
    591    // parent process, we can just talk to the parent actor directly.
    592    if (
    593      Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT
    594    ) {
    595      const [parentProcess] = ChromeUtils.getAllDOMProcesses();
    596      const parentActor = parentProcess.getActor("MozCachedOHTTP");
    597      result = await parentActor.tryCache(resourceURI);
    598    } else {
    599      const childActor = ChromeUtils.domProcessChild.getActor("MozCachedOHTTP");
    600      result = await childActor.sendQuery("tryCache", {
    601        uriString: resourceURI.spec,
    602      });
    603    }
    604 
    605    if (result.success) {
    606      // Stream from parent-provided inputStream
    607      const pump = this.#createInputStreamPump(result.inputStream);
    608      let headers = new Headers(result.headersObj);
    609      this.#applyContentHeaders(headers);
    610 
    611      await this.#streamFromCache(pump, result.inputStream);
    612      return true;
    613    }
    614 
    615    return false;
    616  }
    617 
    618  /**
    619   * Examines any response headers that were included with the cache entry or
    620   * network response, and sets any internal state to represent those headers.
    621   * For now, this is only the Content-Type header.
    622   *
    623   * @param {Headers} headers
    624   *   A Headers object that may have some header key value pairs included.
    625   */
    626  #applyContentHeaders(headers) {
    627    let contentTypeHeader = headers.get("Content-Type");
    628    if (contentTypeHeader) {
    629      let charSet = {};
    630      let hadCharSet = {};
    631      this.#contentType = Services.io.parseResponseContentType(
    632        contentTypeHeader,
    633        charSet,
    634        hadCharSet
    635      );
    636 
    637      if (hadCharSet.value) {
    638        this.#contentCharset = charSet.value;
    639      }
    640    }
    641  }
    642 
    643  /**
    644   * Creates an input stream pump for efficient data streaming.
    645   *
    646   * @param {nsIInputStream} inputStream
    647   *   The input stream to pump.
    648   * @returns {nsIInputStreamPump}
    649   *   The configured pump.
    650   */
    651  #createInputStreamPump(inputStream) {
    652    const pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
    653      Ci.nsIInputStreamPump
    654    );
    655    pump.init(inputStream, 0, 0, false);
    656    return pump;
    657  }
    658 
    659  /**
    660   * Streams data from cache to the client listener.
    661   *
    662   * @param {nsIInputStreamPump} pump
    663   *   The input stream pump.
    664   * @param {nsIInputStream} inputStream
    665   *   The input stream for cleanup.
    666   * @returns {Promise<boolean>}
    667   *   Promise that resolves to true on success.
    668   */
    669  #streamFromCache(pump, inputStream) {
    670    return new Promise((resolve, reject) => {
    671      const listener = this.#createCacheStreamListener(
    672        pump,
    673        inputStream,
    674        resolve
    675      );
    676 
    677      try {
    678        pump.asyncRead(listener);
    679      } catch (error) {
    680        this.#safeCloseStream(inputStream);
    681        reject(error);
    682      }
    683    });
    684  }
    685 
    686  /**
    687   * Creates a stream listener for cache data streaming.
    688   *
    689   * @param {nsIInputStreamPump} pump
    690   *   The pump for channel tracking.
    691   * @param {nsIInputStream} inputStream
    692   *   Input stream for cleanup.
    693   * @param {Function} resolve
    694   *   Promise resolve function.
    695   * @returns {object}
    696   *   Stream listener implementation.
    697   */
    698  #createCacheStreamListener(pump, inputStream, resolve) {
    699    return {
    700      onStartRequest: () => {
    701        this.#startedRequest = true;
    702        this.#pendingChannel = pump;
    703        this.#listener.onStartRequest(this);
    704      },
    705 
    706      onDataAvailable: (request, innerInputStream, offset, count) => {
    707        this.#listener.onDataAvailable(this, innerInputStream, offset, count);
    708      },
    709 
    710      onStopRequest: (request, status) => {
    711        this.#pendingChannel = null;
    712        this.#safeCloseStream(inputStream);
    713 
    714        this.#listener.onStopRequest(this, status);
    715        resolve(Components.isSuccessCode(status));
    716      },
    717    };
    718  }
    719 
    720  /**
    721   * Safely closes an input stream, ignoring errors.
    722   *
    723   * @param {nsIInputStream} stream
    724   *   The stream to close.
    725   */
    726  #safeCloseStream(stream) {
    727    try {
    728      stream.close();
    729    } catch (e) {
    730      // Ignore close errors
    731    }
    732  }
    733 
    734  /**
    735   * Loads the resource via Oblivious HTTP when it's not available in cache.
    736   * Manually writes the response to the HTTP cache for future requests.
    737   *
    738   * @param {nsIURI} resourceURI
    739   *   The URI of the resource to load via OHTTP
    740   * @param {string} host
    741   *   A host matching one of the entries in HOST_MAP
    742   * @returns {Promise<void>} Promise that resolves when OHTTP loading is complete
    743   */
    744  async #loadViaOHTTP(resourceURI, host) {
    745    const { ohttpGatewayConfig, relayURI } =
    746      await this.#protocolHandler.getOHTTPGatewayConfigAndRelayURI(host);
    747 
    748    const ohttpChannel = this.#createOHTTPChannel(
    749      relayURI,
    750      resourceURI,
    751      ohttpGatewayConfig
    752    );
    753 
    754    return this.#executeOHTTPRequest(ohttpChannel);
    755  }
    756 
    757  /**
    758   * Creates and configures an OHTTP channel.
    759   *
    760   * @param {nsIURI} relayURI
    761   *   The OHTTP relay URI.
    762   * @param {nsIURI} resourceURI
    763   *   The target resource URI.
    764   * @param {Uint8Array} ohttpConfig
    765   *   The OHTTP configuration.
    766   * @returns {nsIChannel}
    767   *   The configured OHTTP channel.
    768   */
    769  #createOHTTPChannel(relayURI, resourceURI, ohttpConfig) {
    770    ChromeUtils.releaseAssert(
    771      Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT ||
    772        Services.appinfo.remoteType ===
    773          lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE,
    774      "moz-cached-ohttp is only allowed in privileged content processes " +
    775        "or the main process"
    776    );
    777 
    778    const ohttpChannel = this.#ohttpService.newChannel(
    779      relayURI,
    780      resourceURI,
    781      ohttpConfig
    782    );
    783 
    784    // I'm not sure I love this, but in order for the privileged about content
    785    // process to make requests to the relay and not get dinged by
    786    // OpaqueResponseBlocking, we need to make the load come from the system
    787    // principal.
    788    const loadInfo = lazy.NetUtil.newChannel({
    789      uri: relayURI,
    790      loadUsingSystemPrincipal: true,
    791      contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
    792    }).loadInfo;
    793 
    794    // Copy relevant properties from our channel to the OHTTP channel
    795    ohttpChannel.loadInfo = loadInfo;
    796    ohttpChannel.loadFlags = this.#loadFlags | Ci.nsIRequest.LOAD_ANONYMOUS;
    797 
    798    if (this.#notificationCallbacks) {
    799      ohttpChannel.notificationCallbacks = this.#notificationCallbacks;
    800    }
    801 
    802    if (this.#loadGroup) {
    803      ohttpChannel.loadGroup = this.#loadGroup;
    804    }
    805 
    806    return ohttpChannel;
    807  }
    808 
    809  /**
    810   * Executes the OHTTP request with caching support.
    811   *
    812   * @param {nsIChannel} ohttpChannel
    813   *   The OHTTP channel to execute.
    814   * @returns {Promise<undefined, Error>}
    815   *   Promise that resolves when request is complete.
    816   */
    817  #executeOHTTPRequest(ohttpChannel) {
    818    this.#pendingChannel = ohttpChannel;
    819 
    820    let cachePipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
    821    cachePipe.init(
    822      true /* non-blocking input */,
    823      false /* blocking output */,
    824      0 /* segment size */,
    825      0 /* max segments */
    826    );
    827    let cacheStreamUpdate = new MessageChannel();
    828 
    829    return new Promise((resolve, reject) => {
    830      // Bug 1977139: Transferring nsIInputStream currently causes crashes
    831      // from in-process child actors, so only use the MozCachedOHTTPChild
    832      // if we're running outside of the parent process. If we're running in the
    833      // parent process, we can just talk to the parent actor directly.
    834      if (
    835        Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT
    836      ) {
    837        const [parentProcess] = ChromeUtils.getAllDOMProcesses();
    838        const parentActor = parentProcess.getActor("MozCachedOHTTP");
    839        parentActor.writeCache(
    840          ohttpChannel.URI,
    841          cachePipe.inputStream,
    842          cacheStreamUpdate.port2
    843        );
    844      } else {
    845        const childActor =
    846          ChromeUtils.domProcessChild.getActor("MozCachedOHTTP");
    847        childActor.sendAsyncMessage(
    848          "writeCache",
    849          {
    850            uriString: ohttpChannel.URI.spec,
    851            cacheInputStream: cachePipe.inputStream,
    852            cacheStreamUpdatePort: cacheStreamUpdate.port2,
    853          },
    854          [cacheStreamUpdate.port2]
    855        );
    856      }
    857 
    858      const originalListener = this.#createOHTTPResponseListener(
    859        cacheStreamUpdate.port1,
    860        cachePipe.outputStream,
    861        resolve,
    862        reject
    863      );
    864 
    865      const finalListener = this.#setupStreamTee(
    866        originalListener,
    867        cachePipe.outputStream
    868      );
    869 
    870      try {
    871        ohttpChannel.asyncOpen(finalListener);
    872      } catch (error) {
    873        this.#cleanupCacheOnError(
    874          cacheStreamUpdate.port1,
    875          cachePipe.outputStream
    876        );
    877        reject(error);
    878      }
    879    });
    880  }
    881 
    882  /**
    883   * Creates a response listener for OHTTP requests with cache handling.
    884   *
    885   * @param {MessageChannelPort} cacheStreamUpdatePort
    886   *   MessagePort for sending cache control messages to the parent actor
    887   *   for operations like setting expiration time and dooming cache entries.
    888   * @param {nsIOutputStream} cachePipeOutputStream
    889   *   Output stream of the pipe used to send response data to the cache
    890   *   writer in the parent process via the JSActor.
    891   * @param {Function} resolve
    892   *   Promise resolve function.
    893   * @param {Function} reject
    894   *   Promise reject function.
    895   * @returns {nsIStreamListener}
    896   *   Stream listener implementation
    897   */
    898  #createOHTTPResponseListener(
    899    cacheStreamUpdatePort,
    900    cachePipeOutputStream,
    901    resolve,
    902    reject
    903  ) {
    904    return {
    905      onStartRequest: request => {
    906        // Copy content type from the OHTTP response to our channel
    907        if (request instanceof Ci.nsIHttpChannel) {
    908          try {
    909            const contentType = request.getResponseHeader("content-type");
    910            if (contentType) {
    911              let headers = new Headers();
    912              headers.set("content-type", contentType);
    913              this.#applyContentHeaders(headers);
    914            }
    915          } catch (e) {
    916            // Content-Type header not available
    917          }
    918        }
    919 
    920        this.#startedRequest = true;
    921        this.#listener.onStartRequest(this);
    922        this.#processResponseHeaders(cacheStreamUpdatePort, request);
    923        this.#processCacheControl(cacheStreamUpdatePort, request);
    924      },
    925 
    926      onDataAvailable: (request, inputStream, offset, count) => {
    927        this.#listener.onDataAvailable(this, inputStream, offset, count);
    928      },
    929 
    930      onStopRequest: (request, status) => {
    931        this.#pendingChannel = null;
    932        this.#finalizeCacheEntry(
    933          cacheStreamUpdatePort,
    934          cachePipeOutputStream,
    935          status
    936        );
    937        this.#listener.onStopRequest(this, status);
    938 
    939        if (Components.isSuccessCode(status)) {
    940          resolve();
    941        } else {
    942          reject(new Error(`OHTTP request failed with status: ${status}`));
    943        }
    944      },
    945    };
    946  }
    947 
    948  /**
    949   * Sets up stream tee for cache writing if cache output stream is available.
    950   *
    951   * @param {nsIStreamListener} originalListener
    952   *   The original stream listener.
    953   * @param {nsIOutputStream} cacheOutputStream
    954   *   Cache output stream (may be null).
    955   * @returns {nsIStreamListener}
    956   *   Final listener (tee or original).
    957   */
    958  #setupStreamTee(originalListener, cacheOutputStream) {
    959    if (!cacheOutputStream) {
    960      return originalListener;
    961    }
    962 
    963    try {
    964      const tee = Cc[
    965        "@mozilla.org/network/stream-listener-tee;1"
    966      ].createInstance(Ci.nsIStreamListenerTee);
    967      tee.init(originalListener, cacheOutputStream, null);
    968      return tee;
    969    } catch (error) {
    970      console.warn(
    971        "Failed to create stream tee, proceeding without caching:",
    972        error
    973      );
    974      this.#safeCloseStream(cacheOutputStream);
    975      return originalListener;
    976    }
    977  }
    978 
    979  /**
    980   * Finalizes cache entry after response completion.
    981   *
    982   * @param {MessageChannelPort} cacheStreamUpdatePort
    983   *   MessagePort for sending cache finalization messages to the parent actor.
    984   *   Used to doom the cache entry if the response failed.
    985   * @param {nsIOutputStream} cacheOutputStream
    986   *   Output stream to close.
    987   * @param {number} status
    988   *   Response status code.
    989   */
    990  #finalizeCacheEntry(cacheStreamUpdatePort, cacheOutputStream, status) {
    991    try {
    992      if (cacheOutputStream) {
    993        cacheOutputStream.closeWithStatus(Cr.NS_BASE_STREAM_CLOSED);
    994      }
    995 
    996      if (!Components.isSuccessCode(status)) {
    997        cacheStreamUpdatePort.postMessage({ name: "DoomCacheEntry" });
    998      }
    999    } catch (error) {
   1000      console.warn("Failed to finalize cache entry:", error);
   1001    }
   1002  }
   1003 
   1004  /**
   1005   * Cleans up cache resources on error.
   1006   *
   1007   * @param {MessageChannelPort} cacheStreamUpdatePort
   1008   *   MessagePort for sending error cleanup messages to the parent actor.
   1009   *   Used to doom the cache entry when an error occurs during the request.
   1010   * @param {boolean} cacheOutputStream
   1011   */
   1012  #cleanupCacheOnError(cacheStreamUpdatePort, cacheOutputStream) {
   1013    try {
   1014      if (cacheOutputStream) {
   1015        cacheOutputStream.closeWithStatus(Cr.NS_BASE_STREAM_CLOSED);
   1016      }
   1017      cacheStreamUpdatePort.postMessage({ name: "DoomCacheEntry" });
   1018    } catch (e) {
   1019      // Ignore cleanup errors
   1020    }
   1021  }
   1022 
   1023  /**
   1024   * Notifies the listener of an error condition.
   1025   *
   1026   * @param {number} status
   1027   *   The error status code.
   1028   */
   1029  #notifyError(status) {
   1030    this.#status = status;
   1031    if (this.#listener) {
   1032      // Depending on when the error occurred, we may have already started
   1033      // the request - but if not, start it.
   1034      if (!this.#startedRequest) {
   1035        this.#listener.onStartRequest(this);
   1036      }
   1037      this.#listener.onStopRequest(this, status);
   1038    }
   1039  }
   1040 
   1041  /**
   1042   * Attempts to write the response headers into the cache entry.
   1043   *
   1044   * @param {MessageChannelPort} cacheStreamUpdatePort
   1045   *   MessagePort for sending cache response headers to the parent actor.
   1046   * @param {nsIHttpChannel} httpChannel
   1047   *   The HTTP channel with response headers.
   1048   */
   1049  #processResponseHeaders(cacheStreamUpdatePort, httpChannel) {
   1050    let headersObj = {};
   1051    // nsIHttpChannel is marked as a builtinclass, meaning that it cannot
   1052    // be implemented by script, which makes mocking out channels a pain. We
   1053    // work around this by assuming that if we're in testing mode, that we've
   1054    // been passed an nsIChannel implemented in JS that also happens to have a
   1055    // visitResponseHeaders method implemented on it.
   1056    if (this.#inTestingMode) {
   1057      httpChannel = httpChannel.wrappedJSObject;
   1058    } else if (!(httpChannel instanceof Ci.nsIHttpChannel)) {
   1059      return;
   1060    }
   1061 
   1062    httpChannel.visitResponseHeaders({
   1063      visitHeader(name, value) {
   1064        headersObj[name] = value;
   1065      },
   1066    });
   1067 
   1068    cacheStreamUpdatePort.postMessage({
   1069      name: "WriteOriginalResponseHeaders",
   1070      headersObj,
   1071    });
   1072  }
   1073 
   1074  /**
   1075   * Sets appropriate cache expiration metadata based on response headers.
   1076   *
   1077   * @param {MessageChannelPort} cacheStreamUpdatePort
   1078   *   MessagePort for sending cache metadata messages to the parent actor.
   1079   *   Used to set cache expiration time and handle cache-control directives.
   1080   * @param {nsIHttpChannel} httpChannel
   1081   *   The HTTP channel with response headers.
   1082   */
   1083  #processCacheControl(cacheStreamUpdatePort, httpChannel) {
   1084    if (!(httpChannel instanceof Ci.nsIHttpChannel)) {
   1085      return;
   1086    }
   1087 
   1088    let expirationTime = null;
   1089 
   1090    // Check for Cache-Control header first (takes precedence)
   1091    let cacheControl = null;
   1092    try {
   1093      cacheControl = httpChannel.getResponseHeader("cache-control");
   1094    } catch (e) {
   1095      // Cache-Control header not available, continue to check Expires
   1096    }
   1097 
   1098    if (cacheControl) {
   1099      let cacheControlParseResult =
   1100        Services.io.parseCacheControlHeader(cacheControl);
   1101      // Respect max-age directive
   1102      if (cacheControlParseResult.maxAge) {
   1103        expirationTime = Date.now() + cacheControlParseResult.maxAge * 1000;
   1104      } else if (
   1105        cacheControlParseResult.noCache ||
   1106        cacheControlParseResult.noStore
   1107      ) {
   1108        // Don't cache if explicitly prohibited
   1109        cacheStreamUpdatePort.postMessage({ name: "DoomCacheEntry" });
   1110        return;
   1111      }
   1112    }
   1113 
   1114    // Fallback to Expires header if Cache-Control max-age not found
   1115    if (!expirationTime) {
   1116      try {
   1117        const expires = httpChannel.getResponseHeader("expires");
   1118        if (expires) {
   1119          expirationTime = new Date(expires).getTime();
   1120        }
   1121      } catch (e) {
   1122        // Expires header not available
   1123      }
   1124    }
   1125 
   1126    // Set default expiration if no headers found (24 hours)
   1127    expirationTime ??= Date.now() + 24 * 60 * 60 * 1000; // 24 hours
   1128 
   1129    // Set the expiration time in cache metadata
   1130    cacheStreamUpdatePort.postMessage({
   1131      name: "WriteCacheExpiry",
   1132      expiry: Math.floor(expirationTime / 1000),
   1133    });
   1134  }
   1135 
   1136  QueryInterface = ChromeUtils.generateQI(["nsIChannel"]);
   1137 }