mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-13 05:15:45 +00:00
Bug 1270356 Part 2: Implement parsing and validation of native host manifests r=kmag
MozReview-Commit-ID: 3aXlBAgV4ti --HG-- extra : rebase_source : ca0bb5ec8e93ef806d2c3a662f4863400595c0fa
This commit is contained in:
parent
1b1d9f5e45
commit
74021d63c0
109
toolkit/components/extensions/NativeMessaging.jsm
Normal file
109
toolkit/components/extensions/NativeMessaging.jsm
Normal file
@ -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));
|
||||
},
|
||||
};
|
@ -11,6 +11,7 @@ EXTRA_JS_MODULES += [
|
||||
'ExtensionStorage.jsm',
|
||||
'ExtensionUtils.jsm',
|
||||
'MessageChannel.jsm',
|
||||
'NativeMessaging.jsm',
|
||||
'Schemas.jsm',
|
||||
]
|
||||
|
||||
|
@ -12,6 +12,7 @@ toolkit.jar:
|
||||
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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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");
|
||||
});
|
||||
|
@ -13,3 +13,6 @@ skip-if = toolkit == 'gonk' || appname == "thunderbird"
|
||||
[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"
|
||||
|
Loading…
Reference in New Issue
Block a user