toolkit/components/remotepagemanager/MessagePort.jsm
author Dorel Luca <dluca@mozilla.com>
Thu, 25 Oct 2018 17:52:59 +0300
changeset 491342 5e471c480b8a6ba4939a6c159d86a687a24bb5d8
parent 491333 d9940451c3734f9de2f074005d81ef0e6ca4a3ec
child 491353 cdccbbffdd8d367847768aad9f5297ca694a6050
permissions -rw-r--r--
Backed out 5 changesets (bug 1501286) for browser-chrome failures in browser/components/uitour/test/browser_trackingProtection.js Backed out changeset cac0d4aa79d1 (bug 1501286) Backed out changeset 578d395f555e (bug 1501286) Backed out changeset d9940451c373 (bug 1501286) Backed out changeset ca6c3139bdd2 (bug 1501286) Backed out changeset 93d6f4d54f78 (bug 1501286)

/* 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/. */

"use strict";

var EXPORTED_SYMBOLS = ["MessagePort", "MessageListener"];

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "AsyncPrefs",
  "resource://gre/modules/AsyncPrefs.jsm");
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
  "resource://gre/modules/PrivateBrowsingUtils.jsm");

/*
 * Used for all kinds of permissions checks which requires explicit
 * whitelisting of specific permissions granted through RPM.
 *
 * Please note that prefs that one wants to update need to be
 * whitelisted within AsyncPrefs.jsm.
 */
let RPMAccessManager = {
  accessMap: {
    "about:privatebrowsing": {
      // "sendAsyncMessage": handled within AboutPrivateBrowsingHandler.jsm
      // "setBoolPref": handled within AsyncPrefs.jsm and uses the prefs
      //                ["browser.contentblocking.enabled",
      //                 "privacy.trackingprotection.pbmode.enabled"],
      "getBoolPref": ["browser.contentblocking.enabled",
                      "browser.contentblocking.ui.enabled",
                      "privacy.trackingprotection.enabled",
                      "privacy.trackingprotection.pbmode.enabled"],
      "getFormatURLPref": ["privacy.trackingprotection.introURL",
                           "app.support.baseURL"],
      "isWindowPrivate": ["yes"],
    },
  },

  checkAllowAccess(aPrincipal, aFeature, aValue) {
    // if there is no content principal; deny access
    if (!aPrincipal || !aPrincipal.URI) {
      return false;
    }
    let uri = aPrincipal.URI.asciiSpec;

    // check if there is an entry for that requestying URI in the accessMap;
    // if not, deny access.
    let accessMapForURI = this.accessMap[uri];
    if (!accessMapForURI) {
      Cu.reportError("RPMAccessManager does not allow access to Feature: " + aFeature + " for: " + uri);
      return false;
    }

    // check if the feature is allowed to be accessed for that URI;
    // if not, deny access.
    let accessMapForFeature = accessMapForURI[aFeature];
    if (!accessMapForFeature) {
      Cu.reportError("RPMAccessManager does not allow access to Feature: " + aFeature + " for: " + uri);
      return false;
    }

    // if the actual value is in the whitelist for that feature;
    // allow access
    if (accessMapForFeature.includes(aValue)) {
      return true;
    }

    // otherwise deny access
    Cu.reportError("RPMAccessManager does not allow access to Feature: " + aFeature + " for: " + uri);
    return false;
  },
};

function MessageListener() {
  this.listeners = new Map();
}

MessageListener.prototype = {
  keys() {
    return this.listeners.keys();
  },

  has(name) {
    return this.listeners.has(name);
  },

  callListeners(message) {
    let listeners = this.listeners.get(message.name);
    if (!listeners) {
      return;
    }

    for (let listener of listeners.values()) {
      try {
        listener(message);
      } catch (e) {
        Cu.reportError(e);
      }
    }
  },

  addMessageListener(name, callback) {
    if (!this.listeners.has(name))
      this.listeners.set(name, new Set([callback]));
    else
      this.listeners.get(name).add(callback);
  },

  removeMessageListener(name, callback) {
    if (!this.listeners.has(name))
      return;

    this.listeners.get(name).delete(callback);
  },
};

/*
 * A message port sits on each side of the process boundary for every remote
 * page. Each has a port ID that is unique to the message manager it talks
 * through.
 *
 * We roughly implement the same contract as nsIMessageSender and
 * nsIMessageListenerManager
 */
function MessagePort(messageManager, portID) {
  this.messageManager = messageManager;
  this.portID = portID;
  this.destroyed = false;
  this.listener = new MessageListener();

  this.message = this.message.bind(this);
  this.messageManager.addMessageListener("RemotePage:Message", this.message);
}

MessagePort.prototype = {
  messageManager: null,
  portID: null,
  destroyed: null,
  listener: null,
  _browser: null,
  remotePort: null,

  // Called when the message manager used to connect to the other process has
  // changed, i.e. when a tab is detached.
  swapMessageManager(messageManager) {
    this.messageManager.removeMessageListener("RemotePage:Message", this.message);

    this.messageManager = messageManager;

    this.messageManager.addMessageListener("RemotePage:Message", this.message);
  },

  /* Adds a listener for messages. Many callbacks can be registered for the
   * same message if necessary. An attempt to register the same callback for the
   * same message twice will be ignored. When called the callback is passed an
   * object with these properties:
   *   target: This message port
   *   name:   The message name
   *   data:   Any data sent with the message
   */
  addMessageListener(name, callback) {
    if (this.destroyed) {
      throw new Error("Message port has been destroyed");
    }

    this.listener.addMessageListener(name, callback);
  },

  /*
   * Removes a listener for messages.
   */
  removeMessageListener(name, callback) {
    if (this.destroyed) {
      throw new Error("Message port has been destroyed");
    }

    this.listener.removeMessageListener(name, callback);
  },

  // Sends a message asynchronously to the other process
  sendAsyncMessage(name, data = null) {
    if (this.destroyed) {
      throw new Error("Message port has been destroyed");
    }

    this.messageManager.sendAsyncMessage("RemotePage:Message", {
      portID: this.portID,
      name,
      data,
    });
  },

  // Called to destroy this port
  destroy() {
    try {
      // This can fail in the child process if the tab has already been closed
      this.messageManager.removeMessageListener("RemotePage:Message", this.message);
    } catch (e) { }
    this.messageManager = null;
    this.destroyed = true;
    this.portID = null;
    this.listener = null;
  },

  getBoolPref(aPref) {
    let principal = this.window.document.nodePrincipal;
    if (!RPMAccessManager.checkAllowAccess(principal, "getBoolPref", aPref)) {
      throw new Error("RPMAccessManager does not allow access to getBoolPref");
    }
    return Services.prefs.getBoolPref(aPref);
  },

  setBoolPref(aPref, aVal) {
    return new this.window.Promise(function(resolve) {
      AsyncPrefs.set(aPref, aVal).then(function() {
        resolve();
      });
    });
  },

  getFormatURLPref(aFormatURL) {
    let principal = this.window.document.nodePrincipal;
    if (!RPMAccessManager.checkAllowAccess(principal, "getFormatURLPref", aFormatURL)) {
      throw new Error("RPMAccessManager does not allow access to getFormatURLPref");
    }
    return Services.urlFormatter.formatURLPref(aFormatURL);
  },

  isWindowPrivate() {
    let principal = this.window.document.nodePrincipal;
    if (!RPMAccessManager.checkAllowAccess(principal, "isWindowPrivate", "yes")) {
      throw new Error("RPMAccessManager does not allow access to isWindowPrivate");
    }
    return PrivateBrowsingUtils.isContentWindowPrivate(this.window);
  },
};