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:
Andrew Swan 2016-05-17 15:17:52 -07:00
parent 1b1d9f5e45
commit 74021d63c0
6 changed files with 316 additions and 0 deletions

View 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));
},
};

View File

@ -11,6 +11,7 @@ EXTRA_JS_MODULES += [
'ExtensionStorage.jsm',
'ExtensionUtils.jsm',
'MessageChannel.jsm',
'NativeMessaging.jsm',
'Schemas.jsm',
]

View File

@ -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

View File

@ -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"
}
}
}
}
]
}
]

View File

@ -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");
});

View File

@ -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"