Bug 1270356 Part 2: Implement parsing and validation of native host manifests r?kmag draft
authorAndrew Swan <aswan@mozilla.com>
Tue, 17 May 2016 15:17:52 -0700
changeset 368000 bb6aed73c2a0c326740bb12dd893f92a1880a0b3
parent 366398 ece183f543df0a5d961d94478f94ea2426142f57
child 368446 36ce4a2db5ea592c3eeb93ea21259abee8c97947
push id18409
push useraswan@mozilla.com
push dateTue, 17 May 2016 22:24:42 +0000
reviewerskmag
bugs1270356
milestone49.0a1
Bug 1270356 Part 2: Implement parsing and validation of native host manifests r?kmag MozReview-Commit-ID: 3aXlBAgV4ti
toolkit/components/extensions/NativeMessaging.jsm
toolkit/components/extensions/moz.build
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/schemas/native_host_manifest.json
toolkit/components/extensions/test/xpcshell/test_native_messaging.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -0,0 +1,109 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["HostManifestManager"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+                                  "resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+
+const HOST_MANIFEST_SCHEMA = "chrome://extensions/content/schemas/native_host_manifest.json";
+const VALID_APPLICATION = /^\w+(\.\w+)*$/;
+
+this.HostManifestManager = {
+  _initializePromise: null,
+  _lookup: null,
+
+  init() {
+    if (!this._initializePromise) {
+      let platform = AppConstants.platform;
+      if (platform == "win") {
+        throw new Error("Windows not yet implemented (bug 1270359)");
+      } else if (platform == "macosx" || platform == "linux") {
+        let dirs = [
+          Services.dirsvc.get("XREUserNativeMessaging", Ci.nsIFile).path,
+          Services.dirsvc.get("XRESysNativeMessaging", Ci.nsIFile).path,
+        ];
+        this._lookup = (application, context) => this._tryPaths(application, dirs, context);
+      } else {
+        throw new Error(`Native messaging is not supported on ${AppConstants.platform}`);
+      }
+      this._initializePromise = Schemas.load(HOST_MANIFEST_SCHEMA);
+    }
+    return this._initializePromise;
+  },
+
+  _tryPath(path, application, 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);
+          return null;
+        }
+
+        let normalized = Schemas.normalize(manifest, "manifest.NativeHostManifest", 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);
+          return null;
+        }
+        return normalized.value;
+      }).catch(ex => {
+        if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+          return null;
+        }
+        throw ex;
+      });
+  },
+
+  _tryPaths: Task.async(function* (application, dirs, context) {
+    for (let dir of dirs) {
+      let path = OS.Path.join(dir, `${application}.json`);
+      let manifest = yield this._tryPath(path, application, context);
+      if (manifest) {
+        return {path, manifest};
+      }
+    }
+    return null;
+  }),
+
+  /**
+   * Search for a valid native host manifest for the given application name.
+   * The directories searched and rules for manifest validation are all
+   * detailed in the native messaging documentation.
+   *
+   * @param {string} application The name of the applciation 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.
+   */
+  lookupApplication(application, context) {
+    if (!VALID_APPLICATION.test(application)) {
+      throw new context.cloneScope.Error(`Invalid application "${application}"`);
+    }
+    return this.init().then(() => this._lookup(application, context));
+  },
+};
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -6,16 +6,17 @@
 
 EXTRA_JS_MODULES += [
     'Extension.jsm',
     'ExtensionContent.jsm',
     'ExtensionManagement.jsm',
     'ExtensionStorage.jsm',
     'ExtensionUtils.jsm',
     'MessageChannel.jsm',
+    'NativeMessaging.jsm',
     'Schemas.jsm',
 ]
 
 DIRS += ['schemas']
 
 JAR_MANIFESTS += ['jar.mn']
 
 MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -7,14 +7,15 @@ toolkit.jar:
     content/extensions/schemas/alarms.json
     content/extensions/schemas/cookies.json
     content/extensions/schemas/downloads.json
     content/extensions/schemas/extension.json
     content/extensions/schemas/extension_types.json
     content/extensions/schemas/i18n.json
     content/extensions/schemas/idle.json
     content/extensions/schemas/manifest.json
+    content/extensions/schemas/native_host_manifest.json
     content/extensions/schemas/notifications.json
     content/extensions/schemas/runtime.json
     content/extensions/schemas/storage.json
     content/extensions/schemas/test.json
     content/extensions/schemas/web_navigation.json
     content/extensions/schemas/web_request.json
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/native_host_manifest.json
@@ -0,0 +1,37 @@
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "id": "NativeHostManifest",
+        "type": "object",
+        "description": "Represents a native host manifest file",
+        "properties": {
+          "name": {
+            "type": "string",
+            "pattern": "^\\w+(\\.\\w+)*$"
+          },
+          "description": {
+            "type": "string"
+          },
+          "path": {
+            "type": "string"
+          },
+          "type": {
+            "type": "string",
+            "enum": [
+              "stdio"
+            ]
+          },
+          "allowed_extensions": {
+            "type": "array",
+            "minItems": 1,
+            "items": {
+              "$ref": "manifest.ExtensionID"
+            }
+          }
+        }
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
@@ -0,0 +1,165 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+/* global OS */
+Cu.import("resource://gre/modules/osfile.jsm");
+
+/* global HostManifestManager */
+Cu.import("resource://gre/modules/NativeMessaging.jsm");
+
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
+
+let dir = FileUtils.getDir("TmpD", ["NativeMessaging"]);
+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);
+
+let dirProvider = {
+  getFile(property) {
+    if (property == "XREUserNativeMessaging") {
+      return userDir.clone();
+    } else if (property == "XRESysNativeMessaging") {
+      return globalDir.clone();
+    }
+    return null;
+  },
+};
+
+Services.dirsvc.registerProvider(dirProvider);
+
+do_register_cleanup(() => {
+  Services.dirsvc.unregisterProvider(dirProvider);
+  dir.remove(true);
+});
+
+function writeManifest(path, manifest) {
+  if (typeof manifest != "string") {
+    manifest = JSON.stringify(manifest);
+  }
+  return OS.File.writeAtomic(path, manifest);
+}
+
+add_task(function* setup() {
+  yield Schemas.load(BASE_SCHEMA);
+});
+
+// Test of HostManifestManager.lookupApplication() begin here...
+
+let context = {
+  url: null,
+  logError() {},
+  preprocessors: {},
+};
+
+let templateManifest = {
+  name: "test",
+  description: "this is only a test",
+  path: "/bin/cat",
+  type: "stdio",
+  allowed_extensions: ["extension@tests.mozilla.org"],
+};
+
+add_task(function* test_nonexistent_manifest() {
+  let result = yield HostManifestManager.lookupApplication("test", context);
+  equal(result, null, "lookupApplication returns null for non-existent application");
+});
+
+const USER_TEST_JSON = OS.Path.join(userDir.path, "test.json");
+
+add_task(function* test_good_manifest() {
+  yield writeManifest(USER_TEST_JSON, templateManifest);
+  let result = yield HostManifestManager.lookupApplication("test", context);
+  notEqual(result, null, "lookupApplication finds a good manifest");
+  equal(result.path, USER_TEST_JSON, "lookupApplication returns the correct path");
+  deepEqual(result.manifest, templateManifest, "lookupApplication returns the manifest contents");
+});
+
+add_task(function* test_invalid_json() {
+  yield writeManifest(USER_TEST_JSON, "this is not valid json");
+  let result = yield HostManifestManager.lookupApplication("test", context);
+  equal(result, null, "lookupApplication ignores bad json");
+});
+
+add_task(function* test_invalid_name() {
+  let manifest = Object.assign({}, templateManifest);
+  manifest.name = "../test";
+  yield writeManifest(USER_TEST_JSON, manifest);
+  let result = yield HostManifestManager.lookupApplication("test", context);
+  equal(result, null, "lookupApplication ignores an invalid name");
+});
+
+add_task(function* test_name_mismatch() {
+  let manifest = Object.assign({}, templateManifest);
+  manifest.name = "not test";
+  yield writeManifest(USER_TEST_JSON, manifest);
+  let result = yield HostManifestManager.lookupApplication("test", context);
+  equal(result, null, "lookupApplication ignores mistmatch between json filename and name property");
+});
+
+add_task(function* test_missing_props() {
+  const PROPS = [
+    "name",
+    "description",
+    "path",
+    "type",
+    "allowed_extensions",
+  ];
+  for (let prop of PROPS) {
+    let manifest = Object.assign({}, templateManifest);
+    delete manifest[prop];
+
+    yield writeManifest(USER_TEST_JSON, manifest);
+    let result = yield HostManifestManager.lookupApplication("test", context);
+    equal(result, null, `lookupApplication ignores missing ${prop}`);
+  }
+});
+
+add_task(function* test_invalid_type() {
+  let manifest = Object.assign({}, templateManifest);
+  manifest.type = "bogus";
+  yield writeManifest(USER_TEST_JSON, manifest);
+  let result = yield HostManifestManager.lookupApplication("test", context);
+  equal(result, null, "lookupApplication ignores invalid type");
+});
+
+add_task(function* test_no_allowed_extensions() {
+  let manifest = Object.assign({}, templateManifest);
+  manifest.allowed_extensions = [];
+  yield writeManifest(USER_TEST_JSON, manifest);
+  let result = yield HostManifestManager.lookupApplication("test", context);
+  equal(result, null, "lookupApplication ignores manifest with no allowed_extensions");
+});
+
+const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, "test.json");
+let globalManifest = Object.assign({}, templateManifest);
+globalManifest.description = "This manifest is from the systemwide directory";
+
+add_task(function* good_manifest_system_dir() {
+  yield OS.File.remove(USER_TEST_JSON);
+  yield writeManifest(GLOBAL_TEST_JSON, globalManifest);
+
+  let result = yield HostManifestManager.lookupApplication("test", context);
+  notEqual(result, null, "lookupApplication finds a manifest in the system-wide directory");
+  equal(result.path, GLOBAL_TEST_JSON, "lookupApplication returns path in the system-wide directory");
+  deepEqual(result.manifest, globalManifest, "lookupApplication returns manifest contents from the system-wide directory");
+});
+
+add_task(function* test_user_dir_precedence() {
+  yield writeManifest(USER_TEST_JSON, templateManifest);
+  // test.json is still in the global directory from the previous test
+
+  let result = yield HostManifestManager.lookupApplication("test", context);
+  notEqual(result, null, "lookupApplication finds a manifest when entries exist in both user-specific and system-wide directories");
+  equal(result.path, USER_TEST_JSON, "lookupApplication returns the user-specific path when user-specific and system-wide entries both exist");
+  deepEqual(result.manifest, templateManifest, "lookupApplication returns user-specific manifest contents with user-specific and system-wide entries both exist");
+});
+
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -8,8 +8,11 @@ skip-if = toolkit == 'gonk' || appname =
 [test_csp_validator.js]
 [test_locale_data.js]
 [test_locale_converter.js]
 [test_ext_contexts.js]
 [test_ext_json_parser.js]
 [test_ext_manifest_content_security_policy.js]
 [test_ext_schemas.js]
 [test_getAPILevelForWindow.js]
+[test_native_messaging.js]
+# Re-enable for Windows with bug 1270359.
+skip-if = os != "mac" && os != "linux"