Bug 1386427 - Part 3: Add `storage` and `pkcs11` NativeManifest types r=kmag
authorTomislav Jovanovic <tomica@gmail.com>
Sat, 16 Sep 2017 05:30:13 +0200
changeset 431659 603d09a85dd63adb4a8f3927b7d8dacc440437d0
parent 431658 660f2242c95f7804448f4d09e3d030e6eb1d9d06
child 431660 a439e2ac43058989193daa201b764b96821bd8fa
push id7785
push userryanvm@gmail.com
push dateThu, 21 Sep 2017 13:39:55 +0000
treeherdermozilla-beta@06d4034a8a03 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1386427
milestone57.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 1386427 - Part 3: Add `storage` and `pkcs11` NativeManifest types r=kmag MozReview-Commit-ID: 62MoqNLTxic
toolkit/components/extensions/NativeManifests.jsm
toolkit/components/extensions/NativeMessaging.jsm
toolkit/components/extensions/extensions-toolkit.manifest
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/schemas/native_host_manifest.json
toolkit/components/extensions/schemas/native_manifest.json
toolkit/components/extensions/schemas/runtime.json
toolkit/components/extensions/test/xpcshell/head_native_messaging.js
toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
toolkit/components/extensions/test/xpcshell/test_native_manifests.js
--- a/toolkit/components/extensions/NativeManifests.jsm
+++ b/toolkit/components/extensions/NativeManifests.jsm
@@ -13,113 +13,128 @@ Cu.import("resource://gre/modules/XPCOMU
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   Schemas: "resource://gre/modules/Schemas.jsm",
   Services: "resource://gre/modules/Services.jsm",
   WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
 });
 
-const HOST_MANIFEST_SCHEMA = "chrome://extensions/content/schemas/native_host_manifest.json";
-const VALID_APPLICATION = /^\w+(\.\w+)*$/;
+const DASHED = AppConstants.platform === "linux";
 
-const REGPATH = "Software\\Mozilla\\NativeMessagingHosts";
+// Supported native manifest types, with platform-specific slugs.
+const TYPES = {
+  stdio: DASHED ? "native-messaging-hosts" : "NativeMessagingHosts",
+  storage: DASHED ? "managed-storage" : "ManagedStorage",
+  pkcs11: DASHED ? "pkcs11-modules" : "PKCS11Modules",
+};
+
+const NATIVE_MANIFEST_SCHEMA = "chrome://extensions/content/schemas/native_manifest.json";
+
+const REGPATH = "Software\\Mozilla";
 
 this.NativeManifests = {
   _initializePromise: null,
   _lookup: null,
 
   init() {
     if (!this._initializePromise) {
       let platform = AppConstants.platform;
       if (platform == "win") {
         this._lookup = this._winLookup;
       } else if (platform == "macosx" || platform == "linux") {
         let dirs = [
           Services.dirsvc.get("XREUserNativeManifests", Ci.nsIFile).path,
           Services.dirsvc.get("XRESysNativeManifests", Ci.nsIFile).path,
         ];
-        this._lookup = (application, context) => this._tryPaths(application, dirs, context);
+        this._lookup = (type, name, context) => this._tryPaths(type, name, dirs, context);
       } else {
-        throw new Error(`Native messaging is not supported on ${AppConstants.platform}`);
+        throw new Error(`Native manifests are not supported on ${AppConstants.platform}`);
       }
-      this._initializePromise = Schemas.load(HOST_MANIFEST_SCHEMA);
+      this._initializePromise = Schemas.load(NATIVE_MANIFEST_SCHEMA);
     }
     return this._initializePromise;
   },
 
-  _winLookup(application, context) {
+  _winLookup(type, name, context) {
     const REGISTRY = Ci.nsIWindowsRegKey;
-    let regPath = `${REGPATH}\\${application}`;
+    let regPath = `${REGPATH}\\${TYPES[type]}\\${name}`;
     let path = WindowsRegistry.readRegKey(REGISTRY.ROOT_KEY_CURRENT_USER,
                                           regPath, "", REGISTRY.WOW64_64);
     if (!path) {
       path = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
                                         regPath, "", REGISTRY.WOW64_64);
     }
     if (!path) {
       return null;
     }
-    return this._tryPath(path, application, context)
+    return this._tryPath(type, path, name, context)
       .then(manifest => manifest ? {path, manifest} : null);
   },
 
-  _tryPath(path, application, context) {
+  _tryPath(type, path, name, context) {
     return Promise.resolve()
       .then(() => OS.File.read(path, {encoding: "utf-8"}))
       .then(data => {
         let manifest;
         try {
           manifest = JSON.parse(data);
         } catch (ex) {
-          let msg = `Error parsing native host manifest ${path}: ${ex.message}`;
-          Cu.reportError(msg);
+          Cu.reportError(`Error parsing native manifest ${path}: ${ex.message}`);
           return null;
         }
 
-        let normalized = Schemas.normalize(manifest, "manifest.NativeHostManifest", context);
+        let normalized = Schemas.normalize(manifest, "manifest.NativeManifest", context);
         if (normalized.error) {
           Cu.reportError(normalized.error);
           return null;
         }
         manifest = normalized.value;
-        if (manifest.name != application) {
-          let msg = `Native host manifest ${path} has name property ${manifest.name} (expected ${application})`;
-          Cu.reportError(msg);
+
+        if (manifest.type !== type) {
+          Cu.reportError(`Native manifest ${path} has type property ${manifest.type} (expected ${type})`);
           return null;
         }
-        return normalized.value;
+        if (manifest.name !== name) {
+          Cu.reportError(`Native manifest ${path} has name property ${manifest.name} (expected ${name})`);
+          return null;
+        }
+        if (manifest.allowed_extensions &&
+            !manifest.allowed_extensions.includes(context.extension.id)) {
+          Cu.reportError(`This extension does not have permission to use native manifest ${path}`);
+          return null;
+        }
+
+        return manifest;
       }).catch(ex => {
         if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
           return null;
         }
         throw ex;
       });
   },
 
-  async _tryPaths(application, dirs, context) {
+  async _tryPaths(type, name, dirs, context) {
     for (let dir of dirs) {
-      let path = OS.Path.join(dir, `${application}.json`);
-      let manifest = await this._tryPath(path, application, context);
+      let path = OS.Path.join(dir, TYPES[type], `${name}.json`);
+      let manifest = await this._tryPath(type, path, name, context);
       if (manifest) {
         return {path, manifest};
       }
     }
     return null;
   },
 
   /**
-   * Search for a valid native host manifest for the given application name.
+   * Search for a valid native manifest of the given type and name.
    * The directories searched and rules for manifest validation are all
-   * detailed in the native messaging documentation.
+   * detailed in the Native Manifests documentation.
    *
-   * @param {string} application The name of the applciation to search for.
+   * @param {string} type The type, one of: "pkcs11", "stdio" or "storage".
+   * @param {string} name The name of the manifest to search for.
    * @param {object} context A context object as expected by Schemas.normalize.
    * @returns {object} The contents of the validated manifest, or null if
-   *                   no valid manifest can be found for this application.
+   *                   no valid manifest can be found for this type and name.
    */
-  lookupApplication(application, context) {
-    if (!VALID_APPLICATION.test(application)) {
-      throw new Error(`Invalid application "${application}"`);
-    }
-    return this.init().then(() => this._lookup(application, context));
+  lookupManifest(type, name, context) {
+    return this.init().then(() => this._lookup(type, name, context));
   },
 };
--- a/toolkit/components/extensions/NativeMessaging.jsm
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -61,22 +61,22 @@ this.NativeApp = class extends EventEmit
     this.context.callOnClose(this);
 
     this.proc = null;
     this.readPromise = null;
     this.sendQueue = [];
     this.writePromise = null;
     this.sentDisconnect = false;
 
-    this.startupPromise = NativeManifests.lookupApplication(application, context)
+    this.startupPromise = NativeManifests.lookupManifest("stdio", application, context)
       .then(hostInfo => {
-        // Put the two errors together to not leak information about whether a native
+        // Report a generic error to not leak information about whether a native
         // application is installed to addons that do not have the right permission.
-        if (!hostInfo || !hostInfo.manifest.allowed_extensions.includes(context.extension.id)) {
-          throw new context.cloneScope.Error(`This extension does not have permission to use native application ${application} (or the application is not installed)`);
+        if (!hostInfo) {
+          throw new context.cloneScope.Error(`No such native application ${application}`);
         }
 
         let command = hostInfo.manifest.path;
         if (AppConstants.platform == "win") {
           // OS.Path.join() ignores anything before the last absolute path
           // it sees, so if command is already absolute, it remains unchanged
           // here.  If it is relative, we get the proper absolute path here.
           command = OS.Path.join(OS.Path.dirname(hostInfo.path), command);
--- a/toolkit/components/extensions/extensions-toolkit.manifest
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -4,14 +4,14 @@ category webextension-modules toolkit ch
 category webextension-scripts a-toolkit chrome://extensions/content/ext-toolkit.js
 category webextension-scripts b-tabs-base chrome://extensions/content/ext-tabs-base.js
 
 category webextension-scripts-content toolkit chrome://extensions/content/ext-c-toolkit.js
 category webextension-scripts-devtools toolkit chrome://extensions/content/ext-c-toolkit.js
 category webextension-scripts-addon toolkit chrome://extensions/content/ext-c-toolkit.js
 
 category webextension-schemas events chrome://extensions/content/schemas/events.json
-category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
+category webextension-schemas native_manifest chrome://extensions/content/schemas/native_manifest.json
 category webextension-schemas types chrome://extensions/content/schemas/types.json
 
 
 component {21f9819e-4cdf-49f9-85a0-850af91a5058} extension-process-script.js
 contract @mozilla.org/webextensions/extension-process-script;1 {21f9819e-4cdf-49f9-85a0-850af91a5058}
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -17,17 +17,17 @@ toolkit.jar:
     content/extensions/schemas/extension_protocol_handlers.json
     content/extensions/schemas/i18n.json
 #ifndef ANDROID
     content/extensions/schemas/identity.json
 #endif
     content/extensions/schemas/idle.json
     content/extensions/schemas/management.json
     content/extensions/schemas/manifest.json
-    content/extensions/schemas/native_host_manifest.json
+    content/extensions/schemas/native_manifest.json
     content/extensions/schemas/notifications.json
     content/extensions/schemas/permissions.json
     content/extensions/schemas/proxy.json
     content/extensions/schemas/privacy.json
     content/extensions/schemas/runtime.json
     content/extensions/schemas/storage.json
     content/extensions/schemas/test.json
     content/extensions/schemas/theme.json
rename from toolkit/components/extensions/schemas/native_host_manifest.json
rename to toolkit/components/extensions/schemas/native_manifest.json
--- a/toolkit/components/extensions/schemas/native_host_manifest.json
+++ b/toolkit/components/extensions/schemas/native_manifest.json
@@ -1,37 +1,61 @@
 [
   {
     "namespace": "manifest",
     "types": [
       {
-        "id": "NativeHostManifest",
-        "type": "object",
-        "description": "Represents a native host manifest file",
-        "properties": {
-          "name": {
-            "type": "string",
-            "pattern": "^\\w+(\\.\\w+)*$"
-          },
-          "description": {
-            "type": "string"
+        "id": "NativeManifest",
+        "description": "Represents a native manifest file",
+        "choices": [
+          {
+            "type": "object",
+            "properties": {
+              "name": {
+                "type": "string",
+                "pattern": "^\\w+(\\.\\w+)*$"
+              },
+              "description": {
+                "type": "string"
+              },
+              "path": {
+                "type": "string"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "pkcs11", "stdio"
+                ]
+              },
+              "allowed_extensions": {
+                "type": "array",
+                "minItems": 1,
+                "items": {
+                  "$ref": "manifest.ExtensionID"
+                }
+              }
+            }
           },
-          "path": {
-            "type": "string"
-          },
-          "type": {
-            "type": "string",
-            "enum": [
-              "stdio"
-            ]
-          },
-          "allowed_extensions": {
-            "type": "array",
-            "minItems": 1,
-            "items": {
-              "$ref": "manifest.ExtensionID"
+          {
+            "type": "object",
+            "properties": {
+              "name": {
+                "$ref": "manifest.ExtensionID"
+              },
+              "description": {
+                "type": "string"
+              },
+              "data": {
+                "type": "object"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "storage"
+                ]
+              }
             }
           }
-        }
+        ]
       }
     ]
   }
 ]
--- a/toolkit/components/extensions/schemas/runtime.json
+++ b/toolkit/components/extensions/schemas/runtime.json
@@ -313,16 +313,17 @@
       {
         "name": "connectNative",
         "type": "function",
         "description": "Connects to a native application in the host machine.",
         "permissions": ["nativeMessaging"],
         "parameters": [
           {
             "type": "string",
+            "pattern": "^\\w+(\\.\\w+)*$",
             "name": "application",
             "description": "The name of the registered application to connect to."
           }
         ],
         "returns": {
           "$ref": "Port",
           "description": "Port through which messages can be sent and received with the application"
         }
@@ -365,17 +366,18 @@
         "type": "function",
         "description": "Send a single message to a native application.",
         "permissions": ["nativeMessaging"],
         "async": "responseCallback",
         "parameters": [
           {
             "name": "application",
             "description": "The name of the native messaging host.",
-            "type": "string"
+            "type": "string",
+            "pattern": "^\\w+(\\.\\w+)*$"
           },
           {
             "name": "message",
             "description": "The message that will be passed to the native messaging host.",
             "type": "any"
           },
           {
             "type": "function",
--- a/toolkit/components/extensions/test/xpcshell/head_native_messaging.js
+++ b/toolkit/components/extensions/test/xpcshell/head_native_messaging.js
@@ -15,27 +15,29 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 let {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm", {});
 
 
 // It's important that we use a space in this directory name to make sure we
 // correctly handle executing batch files with spaces in their path.
 let tmpDir = FileUtils.getDir("TmpD", ["Native Messaging"]);
 tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
+const TYPE_SLUG = AppConstants.platform === "linux" ? "native-messaging-hosts" : "NativeMessagingHosts";
+OS.File.makeDir(OS.Path.join(tmpDir.path, TYPE_SLUG));
+
 do_register_cleanup(() => {
   tmpDir.remove(true);
 });
 
 function getPath(filename) {
-  return OS.Path.join(tmpDir.path, filename);
+  return OS.Path.join(tmpDir.path, TYPE_SLUG, filename);
 }
 
 const ID = "native@tests.mozilla.org";
 
-
 async function setupHosts(scripts) {
   const PERMS = {unixMode: 0o755};
 
   const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
   const pythonPath = await Subprocess.pathSearch(env.get("PYTHON"));
 
   async function writeManifest(script, scriptPath, path) {
     let body = `#!${pythonPath} -u\n${script.script}`;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
@@ -403,17 +403,17 @@ add_task(async function test_ext_permiss
 
 // Test that an extension that is not listed in allowed_extensions for
 // a native application cannot use that application.
 add_task(async function test_app_permission() {
   function background() {
     let port = browser.runtime.connectNative("echo");
     port.onDisconnect.addListener(msgPort => {
       browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
-      browser.test.assertEq("This extension does not have permission to use native application echo (or the application is not installed)", port.error && port.error.message);
+      browser.test.assertEq("No such native application echo", port.error && port.error.message);
       browser.test.sendMessage("result", "disconnected");
     });
     port.onMessage.addListener(msg => {
       browser.test.sendMessage("result", "message");
     });
     port.postMessage({test: "test"});
   }
 
@@ -454,17 +454,17 @@ add_task(async function test_child_proce
   });
 
   await extension.startup();
 
   let msg = await extension.awaitMessage("result");
   equal(msg.args.length, 3, "Received two command line arguments");
   equal(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest");
   equal(msg.args[2], ID, "Second command line argument is the ID of the calling extension");
-  equal(msg.cwd.replace(/^\/private\//, "/"), tmpDir.path,
+  equal(msg.cwd.replace(/^\/private\//, "/"), OS.Path.join(tmpDir.path, TYPE_SLUG),
         "Working directory is the directory containing the native appliation");
 
   let exitPromise = waitForSubprocessExit();
   await extension.unload();
   await exitPromise;
 });
 
 add_task(async function test_stderr() {
--- a/toolkit/components/extensions/test/xpcshell/test_native_manifests.js
+++ b/toolkit/components/extensions/test/xpcshell/test_native_manifests.js
@@ -19,27 +19,32 @@ if (AppConstants.platform == "win") {
     registry.shutdown();
   });
 }
 
 const REGPATH = "Software\\Mozilla\\NativeMessagingHosts";
 
 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
 
-let dir = FileUtils.getDir("TmpD", ["NativeMessaging"]);
+const TYPE_SLUG = AppConstants.platform === "linux" ? "native-messaging-hosts" : "NativeMessagingHosts";
+
+let dir = FileUtils.getDir("TmpD", ["NativeManifests"]);
 dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
 let userDir = dir.clone();
 userDir.append("user");
 userDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
 let globalDir = dir.clone();
 globalDir.append("global");
 globalDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
+OS.File.makeDir(OS.Path.join(userDir.path, TYPE_SLUG));
+OS.File.makeDir(OS.Path.join(globalDir.path, TYPE_SLUG));
+
 let dirProvider = {
   getFile(property) {
     if (property == "XREUserNativeManifests") {
       return userDir.clone();
     } else if (property == "XRESysNativeManifests") {
       return globalDir.clone();
     }
     return null;
@@ -70,16 +75,19 @@ add_task(async function setup() {
   }
   notEqual(PYTHON, null, "Found a suitable python interpreter");
 });
 
 let global = this;
 
 // Test of NativeManifests.lookupApplication() begin here...
 let context = {
+  extension: {
+    id: "extension@tests.mozilla.org",
+  },
   url: null,
   jsonStringify(...args) { return JSON.stringify(...args); },
   cloneScope: global,
   logError() {},
   preprocessors: {},
   callOnClose: () => {},
   forgetOnClose: () => {},
 };
@@ -104,25 +112,25 @@ let templateManifest = {
   name: "test",
   description: "this is only a test",
   path: "/bin/cat",
   type: "stdio",
   allowed_extensions: ["extension@tests.mozilla.org"],
 };
 
 function lookupApplication(app, ctx) {
-  return NativeManifests.lookupApplication(app, ctx);
+  return NativeManifests.lookupManifest("stdio", app, ctx);
 }
 
 add_task(async function test_nonexistent_manifest() {
   let result = await lookupApplication("test", context);
   equal(result, null, "lookupApplication returns null for non-existent application");
 });
 
-const USER_TEST_JSON = OS.Path.join(userDir.path, "test.json");
+const USER_TEST_JSON = OS.Path.join(userDir.path, TYPE_SLUG, "test.json");
 
 add_task(async function test_good_manifest() {
   await writeManifest(USER_TEST_JSON, templateManifest);
   if (registry) {
     registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
                       `${REGPATH}\\test`, "", USER_TEST_JSON);
   }
 
@@ -184,17 +192,17 @@ add_task(async function test_invalid_typ
 add_task(async function test_no_allowed_extensions() {
   let manifest = Object.assign({}, templateManifest);
   manifest.allowed_extensions = [];
   await writeManifest(USER_TEST_JSON, manifest);
   let result = await lookupApplication("test", context);
   equal(result, null, "lookupApplication ignores manifest with no allowed_extensions");
 });
 
-const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, "test.json");
+const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, TYPE_SLUG, "test.json");
 let globalManifest = Object.assign({}, templateManifest);
 globalManifest.description = "This manifest is from the systemwide directory";
 
 add_task(async function good_manifest_system_dir() {
   await OS.File.remove(USER_TEST_JSON);
   await writeManifest(GLOBAL_TEST_JSON, globalManifest);
   if (registry) {
     registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
@@ -240,31 +248,31 @@ while True:
         signal.pause()
     msglen = struct.unpack('@I', rawlen)[0]
     msg = sys.stdin.read(msglen)
 
     sys.stdout.write(struct.pack('@I', msglen))
     sys.stdout.write(msg)
   `;
 
-  let scriptPath = OS.Path.join(userDir.path, "wontdie.py");
-  let manifestPath = OS.Path.join(userDir.path, "wontdie.json");
+  let scriptPath = OS.Path.join(userDir.path, TYPE_SLUG, "wontdie.py");
+  let manifestPath = OS.Path.join(userDir.path, TYPE_SLUG, "wontdie.json");
 
   const ID = "native@tests.mozilla.org";
   let manifest = {
     name: "wontdie",
     description: "test async shutdown of native apps",
     type: "stdio",
     allowed_extensions: [ID],
   };
 
   if (AppConstants.platform == "win") {
     await OS.File.writeAtomic(scriptPath, SCRIPT);
 
-    let batPath = OS.Path.join(userDir.path, "wontdie.bat");
+    let batPath = OS.Path.join(userDir.path, TYPE_SLUG, "wontdie.bat");
     let batBody = `@ECHO OFF\n${PYTHON} -u "${scriptPath}" %*\n`;
     await OS.File.writeAtomic(batPath, batBody);
     await OS.File.setPermissions(batPath, {unixMode: 0o755});
 
     manifest.path = batPath;
     await writeManifest(manifestPath, manifest);
 
     registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,