Bug 1386427 - Part 3: Add `storage` and `pkcs11` NativeManifest types draft
authorTomislav Jovanovic <tomica@gmail.com>
Sat, 16 Sep 2017 05:30:13 +0200
changeset 667984 fb75d92336b18d0ffe97659a0da893dddabf3382
parent 667983 d6ebf123ea4c42094f255caf32f9c40942f5ba52
child 667985 244b473fd75d9a5bbf13ae0d01a549193cd61ed6
push id80910
push userbmo:tomica@gmail.com
push dateThu, 21 Sep 2017 00:19:34 +0000
bugs1386427
milestone57.0a1
Bug 1386427 - Part 3: Add `storage` and `pkcs11` NativeManifest types 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,