tor-browser

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

Manifest.sys.mjs (8157B)


      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 /*
      6 * Manifest.sys.mjs is the top level api for managing installed web applications
      7 * https://www.w3.org/TR/appmanifest/
      8 *
      9 * It is used to trigger the installation of a web application via .install()
     10 * and to access the manifest data (including icons).
     11 *
     12 * TODO:
     13 *  - Trigger appropriate app installed events
     14 */
     15 
     16 import { ManifestObtainer } from "resource://gre/modules/ManifestObtainer.sys.mjs";
     17 
     18 import { ManifestIcons } from "resource://gre/modules/ManifestIcons.sys.mjs";
     19 
     20 const lazy = {};
     21 
     22 ChromeUtils.defineESModuleGetters(lazy, {
     23  JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
     24 });
     25 
     26 /**
     27 * Generates an hash for the given string.
     28 *
     29 * Note: The generated hash is returned in base64 form.  Mind the fact base64
     30 * is case-sensitive if you are going to reuse this code.
     31 */
     32 function generateHash(aString, hashAlg) {
     33  const cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance(
     34    Ci.nsICryptoHash
     35  );
     36  cryptoHash.init(hashAlg);
     37  const stringStream = Cc[
     38    "@mozilla.org/io/string-input-stream;1"
     39  ].createInstance(Ci.nsIStringInputStream);
     40  stringStream.setByteStringData(aString);
     41  cryptoHash.updateFromStream(stringStream, -1);
     42  // base64 allows the '/' char, but we can't use it for filenames.
     43  return cryptoHash.finish(true).replace(/\//g, "-");
     44 }
     45 
     46 /**
     47 * Trims the query parameters from a uri.
     48 *
     49 * @param {nsIURI} uri
     50 *
     51 * @returns {string} The url as a string, without any query or hash/ref bits.
     52 */
     53 function stripQuery(uri) {
     54  return uri.mutate().setQuery("").setRef("").finalize().spec;
     55 }
     56 
     57 // Folder in which we store the manifest files
     58 const MANIFESTS_DIR = PathUtils.join(PathUtils.profileDir, "manifests");
     59 
     60 // We maintain a list of scopes for installed webmanifests so we can determine
     61 // whether a given url is within the scope of a previously installed manifest
     62 const MANIFESTS_FILE = "manifest-scopes.json";
     63 
     64 /**
     65 * Manifest object
     66 */
     67 
     68 class Manifest {
     69  constructor(browser, manifestUrl) {
     70    this._manifestUrl = manifestUrl;
     71    // The key for this is the manifests URL that is required to be unique.
     72    // However arbitrary urls are not safe file paths so lets hash it.
     73    const filename =
     74      generateHash(manifestUrl, Ci.nsICryptoHash.SHA256) + ".json";
     75    this._path = PathUtils.join(MANIFESTS_DIR, filename);
     76    this.browser = browser;
     77  }
     78 
     79  /**
     80   * See Bug 1871109
     81   * This function is called at the beginning of initialize() to check if a given
     82   * manifest has MD5 based filename, if so we remove it and migrate the content to
     83   * a new file with SHA256 based name.
     84   * This is done due to security concern, as MD5 is an outdated hashing algorithm and
     85   * shouldn't be used anymore
     86   */
     87  async removeMD5BasedFilename() {
     88    const filenameMD5 =
     89      generateHash(this._manifestUrl, Ci.nsICryptoHash.MD5) + ".json";
     90    const MD5Path = PathUtils.join(MANIFESTS_DIR, filenameMD5);
     91    try {
     92      await IOUtils.copy(MD5Path, this._path, { noOverwrite: true });
     93    } catch (error) {
     94      // we are ignoring the failures returned from copy as it should not stop us from
     95      // installing a new manifest
     96    }
     97 
     98    // Remove the old MD5 based file unconditionally to ensure it's no longer used
     99    try {
    100      await IOUtils.remove(MD5Path);
    101    } catch {
    102      // ignore the error in case MD5 based file does not exist
    103    }
    104  }
    105 
    106  get browser() {
    107    return this._browser;
    108  }
    109 
    110  set browser(aBrowser) {
    111    this._browser = aBrowser;
    112  }
    113 
    114  async initialize() {
    115    await this.removeMD5BasedFilename();
    116    this._store = new lazy.JSONFile({ path: this._path, saveDelayMs: 100 });
    117    await this._store.load();
    118  }
    119 
    120  async prefetch(browser) {
    121    const manifestData = await ManifestObtainer.browserObtainManifest(browser);
    122    const icon = await ManifestIcons.browserFetchIcon(
    123      browser,
    124      manifestData,
    125      192
    126    );
    127    const data = {
    128      installed: false,
    129      manifest: manifestData,
    130      cached_icon: icon,
    131    };
    132    return data;
    133  }
    134 
    135  async install() {
    136    const manifestData = await ManifestObtainer.browserObtainManifest(
    137      this._browser
    138    );
    139    this._store.data = {
    140      installed: true,
    141      manifest: manifestData,
    142    };
    143    Manifests.manifestInstalled(this);
    144    this._store.saveSoon();
    145  }
    146 
    147  async icon(expectedSize) {
    148    if ("cached_icon" in this._store.data) {
    149      return this._store.data.cached_icon;
    150    }
    151    const icon = await ManifestIcons.browserFetchIcon(
    152      this._browser,
    153      this._store.data.manifest,
    154      expectedSize
    155    );
    156    // Cache the icon so future requests do not go over the network
    157    this._store.data.cached_icon = icon;
    158    this._store.saveSoon();
    159    return icon;
    160  }
    161 
    162  get scope() {
    163    const scope =
    164      this._store.data.manifest.scope || this._store.data.manifest.start_url;
    165    return stripQuery(Services.io.newURI(scope));
    166  }
    167 
    168  get name() {
    169    return (
    170      this._store.data.manifest.short_name ||
    171      this._store.data.manifest.name ||
    172      this._store.data.manifest.short_url
    173    );
    174  }
    175 
    176  get url() {
    177    return this._manifestUrl;
    178  }
    179 
    180  get installed() {
    181    return (this._store.data && this._store.data.installed) || false;
    182  }
    183 
    184  get start_url() {
    185    return this._store.data.manifest.start_url;
    186  }
    187 
    188  get path() {
    189    return this._path;
    190  }
    191 }
    192 
    193 /*
    194 * Manifests maintains the list of installed manifests
    195 */
    196 export var Manifests = {
    197  async _initialize() {
    198    if (this._readyPromise) {
    199      return this._readyPromise;
    200    }
    201 
    202    // Prevent multiple initializations
    203    this._readyPromise = (async () => {
    204      // Make sure the manifests have the folder needed to save into
    205      await IOUtils.makeDirectory(MANIFESTS_DIR, { ignoreExisting: true });
    206 
    207      // Ensure any existing scope data we have about manifests is loaded
    208      this._path = PathUtils.join(PathUtils.profileDir, MANIFESTS_FILE);
    209      this._store = new lazy.JSONFile({ path: this._path });
    210      await this._store.load();
    211 
    212      // If we don't have any existing data, initialize empty
    213      if (!this._store.data.hasOwnProperty("scopes")) {
    214        this._store.data.scopes = new Map();
    215      }
    216    })();
    217 
    218    // Cache the Manifest objects creates as they are references to files
    219    // and we do not want multiple file handles
    220    this.manifestObjs = new Map();
    221    return this._readyPromise;
    222  },
    223 
    224  // When a manifest is installed, we save its scope so we can determine if
    225  // future visits fall within this manifests scope
    226  manifestInstalled(manifest) {
    227    this._store.data.scopes[manifest.scope] = manifest.url;
    228    this._store.saveSoon();
    229  },
    230 
    231  // Given a url, find if it is within an installed manifests scope and if so
    232  // return that manifests url
    233  findManifestUrl(url) {
    234    for (let scope in this._store.data.scopes) {
    235      if (url.startsWith(scope)) {
    236        return this._store.data.scopes[scope];
    237      }
    238    }
    239    return null;
    240  },
    241 
    242  // Get the manifest given a url, or if not look for a manifest that is
    243  // tied to the current page
    244  async getManifest(browser, manifestUrl) {
    245    // Ensure we have all started up
    246    if (!this._readyPromise) {
    247      await this._initialize();
    248    }
    249 
    250    // If the client does not already know its manifestUrl, we take the
    251    // url of the client and see if it matches the scope of any installed
    252    // manifests
    253    if (!manifestUrl) {
    254      const url = stripQuery(browser.currentURI);
    255      manifestUrl = this.findManifestUrl(url);
    256    }
    257 
    258    // No matches so no manifest
    259    if (manifestUrl === null) {
    260      return null;
    261    }
    262 
    263    // If we have already created this manifest return cached
    264    if (this.manifestObjs.has(manifestUrl)) {
    265      const manifest = this.manifestObjs.get(manifestUrl);
    266      if (manifest.browser !== browser) {
    267        manifest.browser = browser;
    268      }
    269      return manifest;
    270    }
    271 
    272    // Otherwise create a new manifest object
    273    const manifest = new Manifest(browser, manifestUrl);
    274    this.manifestObjs.set(manifestUrl, manifest);
    275    await manifest.initialize();
    276    return manifest;
    277  },
    278 };