tor-browser

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

MozCachedOHTTPParent.sys.mjs (6598B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
      9  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     10 });
     11 
     12 // These match the serialization scheme used for header key/value pairs in
     13 // nsHttpHeaderArray::FlattenOriginalHeader.
     14 const RESPONSE_HEADER_KEY_VALUE_DELIMETER = ": ";
     15 const RESPONSE_HEADER_DELIMETER = "\r\n";
     16 const RESPONSE_HEADER_METADATA_ELEMENT = "original-response-headers";
     17 
     18 /**
     19 * Parent process JSActor for handling cache lookups for moz-cachged-ohttp
     20 * protocol. This actor handles cache operations that require parent process
     21 * privileges.
     22 */
     23 export class MozCachedOHTTPParent extends JSProcessActorParent {
     24  receiveMessage(message) {
     25    if (
     26      this.manager.remoteType !== null &&
     27      this.manager.remoteType !== lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
     28    ) {
     29      return Promise.reject(new Error("Process type mismatch"));
     30    }
     31 
     32    switch (message.name) {
     33      case "tryCache": {
     34        let uri = Services.io.newURI(message.data.uriString);
     35        return this.tryCache(uri);
     36      }
     37      case "writeCache": {
     38        let uri = Services.io.newURI(message.data.uriString);
     39        return this.writeCache(
     40          uri,
     41          message.data.cacheInputStream,
     42          message.data.cacheStreamUpdatePort
     43        );
     44      }
     45    }
     46 
     47    return Promise.reject(new Error(`Unknown message: ${message.name}`));
     48  }
     49 
     50  /**
     51   * Attempts to load the resource from the HTTP cache without making network
     52   * requests. On cache miss, provides an output stream for writing to cache.
     53   *
     54   * @param {nsIURI} resourceURI
     55   *   The URI of the resource to load from cache
     56   * @returns {Promise<object>}
     57   *   Promise that resolves to result object with success flag and data
     58   */
     59  async tryCache(resourceURI) {
     60    try {
     61      const cacheEntry = await this.#openCacheEntry(
     62        resourceURI,
     63        Ci.nsICacheStorage.OPEN_READONLY
     64      );
     65 
     66      if (!cacheEntry.dataSize) {
     67        throw new Error("Cache entry is empty.");
     68      }
     69 
     70      let headersStrings = cacheEntry
     71        .getMetaDataElement(RESPONSE_HEADER_METADATA_ELEMENT)
     72        .split(RESPONSE_HEADER_DELIMETER);
     73      let headersObj = {};
     74      for (let headersString of headersStrings) {
     75        let delimeterIndex = headersString.indexOf(
     76          RESPONSE_HEADER_KEY_VALUE_DELIMETER
     77        );
     78        let key = headersString.substring(0, delimeterIndex);
     79        let value = headersString.substring(
     80          delimeterIndex + RESPONSE_HEADER_KEY_VALUE_DELIMETER.length
     81        );
     82        headersObj[key] = value;
     83      }
     84 
     85      // Cache hit - return input stream for reading
     86      const inputStream = cacheEntry.openInputStream(0);
     87 
     88      return {
     89        success: true,
     90        inputStream,
     91        headersObj,
     92      };
     93    } catch (e) {
     94      // Cache miss or error - proceed without caching
     95      return { success: false };
     96    }
     97  }
     98 
     99  /**
    100   * Writes resource data to the HTTP cache. Opens a cache entry for the
    101   * specified URI and copies data from the input stream to the cache, handling
    102   * cache control messages for expiration and entry management.
    103   *
    104   * @param {nsIURI} resourceURI
    105   *   The URI of the resource to cache
    106   * @param {nsIInputStream} cacheInputStream
    107   *   Input stream containing the resource data to cache
    108   * @param {MessageChannelPort} cacheStreamUpdatePort
    109   *   MessagePort for receiving cache control messages:
    110   *   - "DoomCacheEntry": Remove cache entry (on error/no-cache)
    111   *   - "WriteCacheExpiry": Set cache expiration time
    112   * @returns {Promise<undefined>}
    113   *   Promise that resolves when caching is complete
    114   */
    115  async writeCache(resourceURI, cacheInputStream, cacheStreamUpdatePort) {
    116    let cacheEntry;
    117    let outputStream;
    118 
    119    try {
    120      cacheEntry = await this.#openCacheEntry(
    121        resourceURI,
    122        Ci.nsICacheStorage.OPEN_NORMALLY
    123      );
    124      outputStream = cacheEntry.openOutputStream(0, -1);
    125    } catch (e) {
    126      return;
    127    }
    128 
    129    cacheStreamUpdatePort.onmessage = msg => {
    130      switch (msg.data.name) {
    131        case "DoomCacheEntry": {
    132          cacheEntry.asyncDoom(null);
    133          break;
    134        }
    135        case "WriteOriginalResponseHeaders": {
    136          let headers = new Headers(msg.data.headersObj);
    137          let headersStrings = [];
    138          for (let [key, value] of headers.entries()) {
    139            headersStrings.push(
    140              `${key}${RESPONSE_HEADER_KEY_VALUE_DELIMETER}${value}`
    141            );
    142          }
    143          cacheEntry.setMetaDataElement(
    144            RESPONSE_HEADER_METADATA_ELEMENT,
    145            headersStrings.join(RESPONSE_HEADER_DELIMETER)
    146          );
    147          break;
    148        }
    149        case "WriteCacheExpiry": {
    150          cacheEntry.setExpirationTime(msg.data.expiry);
    151          break;
    152        }
    153      }
    154    };
    155    try {
    156      await new Promise(resolve => {
    157        lazy.NetUtil.asyncCopy(cacheInputStream, outputStream, writeResult => {
    158          if (!Components.isSuccessCode(writeResult)) {
    159            console.error(
    160              "Failed to cache moz-cached-ohttp resource with result: ",
    161              writeResult
    162            );
    163          }
    164 
    165          resolve();
    166        });
    167      });
    168    } finally {
    169      cacheStreamUpdatePort.onmessage = null;
    170    }
    171  }
    172 
    173  /**
    174   * Opens a cache entry for the specified URI.
    175   *
    176   * @param {nsIURI} uri
    177   *   The URI to open cache entry for.
    178   * @param {number} openFlags
    179   *   Cache storage open flags.
    180   * @returns {Promise<nsICacheEntry|null>}
    181   *   Promise that resolves to cache entry or null if not available.
    182   */
    183  async #openCacheEntry(uri, openFlags) {
    184    const lci = Services.loadContextInfo.anonymous;
    185    const storage = Services.cache2.diskCacheStorage(lci);
    186 
    187    // For read-only access, check existence first
    188    if (
    189      openFlags === Ci.nsICacheStorage.OPEN_READONLY &&
    190      !storage.exists(uri, "")
    191    ) {
    192      throw new Error("Cache entry does not exist.");
    193    }
    194 
    195    return new Promise((resolve, reject) => {
    196      storage.asyncOpenURI(uri, "", openFlags, {
    197        onCacheEntryCheck: () => Ci.nsICacheEntryOpenCallback.ENTRY_WANTED,
    198        onCacheEntryAvailable: (entry, _isNew, status) => {
    199          if (Components.isSuccessCode(status)) {
    200            resolve(entry);
    201          } else {
    202            reject(new Error(`Cache entry operation failed: ${status}`));
    203          }
    204        },
    205      });
    206    });
    207  }
    208 }