Bug 1270356 Part 2: Implement parsing and validation of native host manifests r?kmag
MozReview-Commit-ID: 3aXlBAgV4ti
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"