Bug 1322235: Part 5 - Add an ExtensionPolicyService singleton class to track active extension policies. r=billm,mixedpuppy
authorKris Maglione <maglione.k@gmail.com>
Tue, 23 May 2017 19:15:10 -0700
changeset 410370 02ce614daf1999405cd4e63d60ab2b6ebd4e4048
parent 410369 19d92b19910f3ce9bfa2b2874d17635fe323080b
child 410371 35e67c38d3cd93cc43f4665849847959f7d97656
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm, mixedpuppy
bugs1322235
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1322235: Part 5 - Add an ExtensionPolicyService singleton class to track active extension policies. r=billm,mixedpuppy Bill, can you please review the binding changes? Shane, can you please review the policy service? This is the first step to making extension policy data directly available to C++ code without any COM overhead. It tracks the set of currently active extensions, and how they map to add-on IDs and URIs. MozReview-Commit-ID: 9Z61AXFll3P
dom/base/nsGkAtomList.h
dom/webidl/WebExtensionPolicy.webidl
toolkit/components/extensions/ExtensionPolicyService.cpp
toolkit/components/extensions/ExtensionPolicyService.h
toolkit/components/extensions/WebExtensionPolicy.cpp
toolkit/components/extensions/WebExtensionPolicy.h
toolkit/components/extensions/moz.build
toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -2005,16 +2005,17 @@ GK_ATOM(ondevicelight, "ondevicelight")
 
 // MediaDevices device change event
 GK_ATOM(ondevicechange, "ondevicechange")
 
 // HTML element attributes that only exposed to XBL and chrome content
 GK_ATOM(mozinputrangeignorepreventdefault, "mozinputrangeignorepreventdefault")
 
 // WebExtensions
+GK_ATOM(moz_extension, "moz-extension")
 GK_ATOM(http, "http")
 GK_ATOM(https, "https")
 
 //---------------------------------------------------------------------------
 // Special atoms
 //---------------------------------------------------------------------------
 
 // Node types
--- a/dom/webidl/WebExtensionPolicy.webidl
+++ b/dom/webidl/WebExtensionPolicy.webidl
@@ -93,16 +93,42 @@ interface WebExtensionPolicy {
    */
   DOMString localize(DOMString unlocalizedText);
 
   /**
    * Returns the moz-extension: URL for the given path.
    */
   [Throws]
   DOMString getURL(optional DOMString path = "");
+
+
+  /**
+   * Returns the list of currently active extension policies.
+   */
+  static sequence<WebExtensionPolicy> getActiveExtensions();
+
+  /**
+   * Returns the currently-active policy for the extension with the given ID,
+   * or null if no policy is active for that ID.
+   */
+  static WebExtensionPolicy? getByID(DOMString id);
+
+  /**
+   * Returns the currently-active policy for the extension with the given
+   * moz-extension: hostname, or null if no policy is active for that
+   * hostname.
+   */
+  static WebExtensionPolicy? getByHostname(ByteString hostname);
+
+  /**
+   * Returns the currently-active policy for the extension extension URI, or
+   * null if the URI is not an extension URI, or no policy is currently active
+   * for it.
+   */
+  static WebExtensionPolicy? getByURI(URI uri);
 };
 
 dictionary WebExtensionInit {
   required DOMString id;
 
   required ByteString mozExtensionHostname;
 
   required DOMString baseURL;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionPolicyService.cpp
@@ -0,0 +1,122 @@
+/* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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/. */
+
+#include "mozilla/ExtensionPolicyService.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Preferences.h"
+#include "nsGkAtoms.h"
+
+namespace mozilla {
+
+using namespace extensions;
+
+#define DEFAULT_BASE_CSP \
+    "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; " \
+    "object-src 'self' https://* moz-extension: blob: filesystem:;"
+
+#define DEFAULT_DEFAULT_CSP \
+    "script-src 'self'; object-src 'self';"
+
+
+/*****************************************************************************
+ * ExtensionPolicyService
+ *****************************************************************************/
+
+/* static */ ExtensionPolicyService&
+ExtensionPolicyService::GetSingleton()
+{
+  static RefPtr<ExtensionPolicyService> sExtensionPolicyService;
+
+  if (MOZ_UNLIKELY(!sExtensionPolicyService)) {
+    sExtensionPolicyService = new ExtensionPolicyService();
+    ClearOnShutdown(&sExtensionPolicyService);
+  }
+  return *sExtensionPolicyService.get();
+}
+
+
+WebExtensionPolicy*
+ExtensionPolicyService::GetByURL(const URLInfo& aURL)
+{
+  if (aURL.Scheme() == nsGkAtoms::moz_extension) {
+    return GetByHost(aURL.Host());
+  }
+  return nullptr;
+}
+
+void
+ExtensionPolicyService::GetAll(nsTArray<RefPtr<WebExtensionPolicy>>& aResult)
+{
+  for (auto iter = mExtensions.Iter(); !iter.Done(); iter.Next()) {
+    aResult.AppendElement(iter.Data());
+  }
+}
+
+bool
+ExtensionPolicyService::RegisterExtension(WebExtensionPolicy& aPolicy)
+{
+  bool ok = (!GetByID(aPolicy.Id()) &&
+             !GetByHost(aPolicy.MozExtensionHostname()));
+  MOZ_ASSERT(ok);
+
+  if (!ok) {
+    return false;
+  }
+
+  mExtensions.Put(aPolicy.Id(), &aPolicy);
+  mExtensionHosts.Put(aPolicy.MozExtensionHostname(), &aPolicy);
+  return true;
+}
+
+bool
+ExtensionPolicyService::UnregisterExtension(WebExtensionPolicy& aPolicy)
+{
+  bool ok = (GetByID(aPolicy.Id()) == &aPolicy &&
+             GetByHost(aPolicy.MozExtensionHostname()) == &aPolicy);
+  MOZ_ASSERT(ok);
+
+  if (!ok) {
+    return false;
+  }
+
+  mExtensions.Remove(aPolicy.Id());
+  mExtensionHosts.Remove(aPolicy.MozExtensionHostname());
+  return true;
+}
+
+
+void
+ExtensionPolicyService::BaseCSP(nsAString& aBaseCSP) const
+{
+  nsresult rv;
+
+  rv = Preferences::GetString("extensions.webextensions.base-content-security-policy", &aBaseCSP);
+  if (NS_FAILED(rv)) {
+    aBaseCSP.AssignLiteral(DEFAULT_BASE_CSP);
+  }
+}
+
+void
+ExtensionPolicyService::DefaultCSP(nsAString& aDefaultCSP) const
+{
+  nsresult rv;
+
+  rv = Preferences::GetString("extensions.webextensions.default-content-security-policy", &aDefaultCSP);
+  if (NS_FAILED(rv)) {
+    aDefaultCSP.AssignLiteral(DEFAULT_DEFAULT_CSP);
+  }
+}
+
+NS_IMPL_CYCLE_COLLECTION(ExtensionPolicyService, mExtensions, mExtensionHosts)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionPolicyService)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionPolicyService)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionPolicyService)
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionPolicyService.h
@@ -0,0 +1,69 @@
+/* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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/. */
+
+#ifndef mozilla_ExtensionPolicyService_h
+#define mozilla_ExtensionPolicyService_h
+
+#include "mozilla/extensions/WebExtensionPolicy.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsHashKeys.h"
+#include "nsIAtom.h"
+#include "nsISupports.h"
+#include "nsPointerHashKeys.h"
+#include "nsRefPtrHashtable.h"
+
+namespace mozilla {
+
+using extensions::WebExtensionPolicy;
+
+class ExtensionPolicyService final : public nsISupports
+{
+public:
+  NS_DECL_CYCLE_COLLECTION_CLASS(ExtensionPolicyService)
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+
+  static ExtensionPolicyService& GetSingleton();
+
+  WebExtensionPolicy*
+  GetByID(const nsIAtom* aAddonId)
+  {
+    return mExtensions.GetWeak(aAddonId);
+  }
+
+  WebExtensionPolicy* GetByID(const nsAString& aAddonId)
+  {
+    nsCOMPtr<nsIAtom> atom = NS_AtomizeMainThread(aAddonId);
+    return GetByID(atom);
+  }
+
+  WebExtensionPolicy* GetByURL(const extensions::URLInfo& aURL);
+
+  WebExtensionPolicy* GetByHost(const nsACString& aHost) const
+  {
+    return mExtensionHosts.GetWeak(aHost);
+  }
+
+  void GetAll(nsTArray<RefPtr<WebExtensionPolicy>>& aResult);
+
+  bool RegisterExtension(WebExtensionPolicy& aPolicy);
+  bool UnregisterExtension(WebExtensionPolicy& aPolicy);
+
+  void BaseCSP(nsAString& aDefaultCSP) const;
+  void DefaultCSP(nsAString& aDefaultCSP) const;
+
+protected:
+  virtual ~ExtensionPolicyService() = default;
+
+private:
+  ExtensionPolicyService() = default;
+
+  nsRefPtrHashtable<nsPtrHashKey<const nsIAtom>, WebExtensionPolicy> mExtensions;
+  nsRefPtrHashtable<nsCStringHashKey, WebExtensionPolicy> mExtensionHosts;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ExtensionPolicyService_h
--- a/toolkit/components/extensions/WebExtensionPolicy.cpp
+++ b/toolkit/components/extensions/WebExtensionPolicy.cpp
@@ -54,16 +54,22 @@ class EscapeHTML final : public nsAdopti
 {
 public:
   explicit EscapeHTML(const nsACString& str)
     : nsAdoptingCString(nsEscapeHTML(str.BeginReading()))
   {}
 };
 
 
+static inline ExtensionPolicyService&
+EPS()
+{
+  return ExtensionPolicyService::GetSingleton();
+}
+
 static nsISubstitutingProtocolHandler*
 Proto()
 {
   static nsCOMPtr<nsISubstitutingProtocolHandler> sHandler;
 
   if (MOZ_UNLIKELY(!sHandler)) {
     nsCOMPtr<nsIIOService> ios = do_GetIOService();
     MOZ_RELEASE_ASSERT(ios);
@@ -96,16 +102,20 @@ WebExtensionPolicy::WebExtensionPolicy(G
   , mHostPermissions(aInit.mAllowedOrigins)
 {
   mWebAccessiblePaths.AppendElements(aInit.mWebAccessibleResources);
 
   if (!aInit.mBackgroundScripts.IsNull()) {
     mBackgroundScripts.SetValue().AppendElements(aInit.mBackgroundScripts.Value());
   }
 
+  if (mContentSecurityPolicy.IsVoid()) {
+    EPS().DefaultCSP(mContentSecurityPolicy);
+  }
+
   nsresult rv = NS_NewURI(getter_AddRefs(mBaseURI), aInit.mBaseURL);
   if (NS_FAILED(rv)) {
     aRv.Throw(rv);
   }
 }
 
 already_AddRefed<WebExtensionPolicy>
 WebExtensionPolicy::Constructor(GlobalObject& aGlobal,
@@ -115,16 +125,42 @@ WebExtensionPolicy::Constructor(GlobalOb
   RefPtr<WebExtensionPolicy> policy = new WebExtensionPolicy(aGlobal, aInit, aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
   return policy.forget();
 }
 
 
+/* static */ void
+WebExtensionPolicy::GetActiveExtensions(dom::GlobalObject& aGlobal,
+                                        nsTArray<RefPtr<WebExtensionPolicy>>& aResults)
+{
+  EPS().GetAll(aResults);
+}
+
+/* static */ already_AddRefed<WebExtensionPolicy>
+WebExtensionPolicy::GetByID(dom::GlobalObject& aGlobal, const nsAString& aID)
+{
+  return do_AddRef(EPS().GetByID(aID));
+}
+
+/* static */ already_AddRefed<WebExtensionPolicy>
+WebExtensionPolicy::GetByHostname(dom::GlobalObject& aGlobal, const nsACString& aHostname)
+{
+  return do_AddRef(EPS().GetByHost(aHostname));
+}
+
+/* static */ already_AddRefed<WebExtensionPolicy>
+WebExtensionPolicy::GetByURI(dom::GlobalObject& aGlobal, nsIURI* aURI)
+{
+  return do_AddRef(EPS().GetByURL(aURI));
+}
+
+
 void
 WebExtensionPolicy::SetActive(bool aActive, ErrorResult& aRv)
 {
   if (aActive == mActive) {
     return;
   }
 
   bool ok = aActive ? Enable() : Disable();
@@ -134,26 +170,35 @@ WebExtensionPolicy::SetActive(bool aActi
   }
 }
 
 bool
 WebExtensionPolicy::Enable()
 {
   MOZ_ASSERT(!mActive);
 
+  if (!EPS().RegisterExtension(*this)) {
+    return false;
+  }
+
   Unused << Proto()->SetSubstitution(MozExtensionHostname(), mBaseURI);
 
   mActive = true;
   return true;
 }
 
 bool
 WebExtensionPolicy::Disable()
 {
   MOZ_ASSERT(mActive);
+  MOZ_ASSERT(EPS().GetByID(Id()) == this);
+
+  if (!EPS().UnregisterExtension(*this)) {
+    return false;
+  }
 
   Unused << Proto()->SetSubstitution(MozExtensionHostname(), nullptr);
 
   mActive = false;
   return true;
 }
 
 void
--- a/toolkit/components/extensions/WebExtensionPolicy.h
+++ b/toolkit/components/extensions/WebExtensionPolicy.h
@@ -106,16 +106,29 @@ public:
     mPermissions = new AtomSet(aPermissions);
   }
 
 
   bool Active() const { return mActive; }
   void SetActive(bool aActive, ErrorResult& aRv);
 
 
+  static void
+  GetActiveExtensions(dom::GlobalObject& aGlobal, nsTArray<RefPtr<WebExtensionPolicy>>& aResults);
+
+  static already_AddRefed<WebExtensionPolicy>
+  GetByID(dom::GlobalObject& aGlobal, const nsAString& aID);
+
+  static already_AddRefed<WebExtensionPolicy>
+  GetByHostname(dom::GlobalObject& aGlobal, const nsACString& aHostname);
+
+  static already_AddRefed<WebExtensionPolicy>
+  GetByURI(dom::GlobalObject& aGlobal, nsIURI* aURI);
+
+
   nsISupports* GetParentObject() const { return mParent; }
 
   virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
 
 protected:
   virtual ~WebExtensionPolicy() = default;
 
 private:
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -40,23 +40,28 @@ TESTING_JS_MODULES += [
     'ExtensionXPCShellUtils.jsm',
 ]
 
 DIRS += [
     'schemas',
     'webrequest',
 ]
 
+EXPORTS.mozilla = [
+    'ExtensionPolicyService.h',
+]
+
 EXPORTS.mozilla.extensions = [
     'MatchGlob.h',
     'MatchPattern.h',
     'WebExtensionPolicy.h',
 ]
 
 UNIFIED_SOURCES += [
+    'ExtensionPolicyService.cpp',
     'MatchPattern.cpp',
     'WebExtensionPolicy.cpp',
 ]
 
 FINAL_LIBRARY = 'xul'
 
 
 JAR_MANIFESTS += ['jar.mn']
--- a/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js
+++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js
@@ -70,9 +70,71 @@ add_task(async function test_WebExtensin
   ok(policy.isPathWebAccessible("/foo/bar"), "Web-accessible glob should be web-accessible");
   ok(policy.isPathWebAccessible("/bar.baz"), "Web-accessible path should be web-accessible");
   ok(!policy.isPathWebAccessible("/bar.baz/quux"), "Non-web-accessible path should not be web-accessible");
 
 
   // Localization
 
   equal(policy.localize("foo"), "<foo>", "Localization callback should work as expected");
+
+
+  // Protocol and lookups.
+
+  let proto = Services.io.getProtocolHandler("moz-extension", uuid).QueryInterface(Ci.nsISubstitutingProtocolHandler);
+
+  deepEqual(WebExtensionPolicy.getActiveExtensions(), [], "Should have no active extensions");
+  equal(WebExtensionPolicy.getByID(id), null, "ID lookup should not return extension when not active");
+  equal(WebExtensionPolicy.getByHostname(uuid), null, "Hostname lookup should not return extension when not active");
+  Assert.throws(() => proto.resolveURI(mozExtURI), /NS_ERROR_NOT_AVAILABLE/,
+                "URL should not resolve when not active");
+
+  policy.active = true;
+  equal(policy.active, true, "Active attribute should be updated");
+
+  let exts = WebExtensionPolicy.getActiveExtensions();
+  equal(exts.length, 1, "Should have one active extension");
+  equal(exts[0], policy, "Should have the correct active extension");
+
+  equal(WebExtensionPolicy.getByID(id), policy, "ID lookup should return extension when active");
+  equal(WebExtensionPolicy.getByHostname(uuid), policy, "Hostname lookup should return extension when active");
+
+  equal(proto.resolveURI(mozExtURI), baseURL, "URL should resolve correctly while active");
+
+  policy.active = false;
+  equal(policy.active, false, "Active attribute should be updated");
+
+  deepEqual(WebExtensionPolicy.getActiveExtensions(), [], "Should have no active extensions");
+  equal(WebExtensionPolicy.getByID(id), null, "ID lookup should not return extension when not active");
+  equal(WebExtensionPolicy.getByHostname(uuid), null, "Hostname lookup should not return extension when not active");
+  Assert.throws(() => proto.resolveURI(mozExtURI), /NS_ERROR_NOT_AVAILABLE/,
+                "URL should not resolve when not active");
+
+
+  // Conflicting policies.
+
+  // This asserts in debug builds, so only test in non-debug builds.
+  if (!AppConstants.DEBUG) {
+    policy.active = true;
+
+    let attrs = [{id, uuid},
+                 {id, uuid: "d916886c-cfdf-482e-b7b1-d7f5b0facfa5"},
+                 {id: "foo@quux", uuid}];
+
+    // eslint-disable-next-line no-shadow
+    for (let {id, uuid} of attrs) {
+      let policy2 = new WebExtensionPolicy({
+        id,
+        mozExtensionHostname: uuid,
+        baseURL: "file://bar/",
+
+        localizeCallback() {},
+
+        allowedOrigins: new MatchPatternSet([]),
+      });
+
+      Assert.throws(() => { policy2.active = true; }, /NS_ERROR_UNEXPECTED/,
+                    `Should not be able to activate conflicting policy: ${id} ${uuid}`);
+    }
+
+    policy.active = false;
+  }
 });