diff options
Diffstat (limited to 'toolkit/components/webextensions/NativeMessaging.jsm')
-rw-r--r-- | toolkit/components/webextensions/NativeMessaging.jsm | 443 |
1 files changed, 0 insertions, 443 deletions
diff --git a/toolkit/components/webextensions/NativeMessaging.jsm b/toolkit/components/webextensions/NativeMessaging.jsm deleted file mode 100644 index 3d8658a3f..000000000 --- a/toolkit/components/webextensions/NativeMessaging.jsm +++ /dev/null @@ -1,443 +0,0 @@ -/* 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", "NativeApp"]; -/* globals NativeApp */ - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {}); - -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", - "resource://gre/modules/AsyncShutdown.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild", - "resource://gre/modules/ExtensionChild.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Subprocess", - "resource://gre/modules/Subprocess.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout", - "resource://gre/modules/Timer.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", - "resource://gre/modules/Timer.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry", - "resource://gre/modules/WindowsRegistry.jsm"); - -const HOST_MANIFEST_SCHEMA = "chrome://extensions/content/schemas/native_host_manifest.json"; -const VALID_APPLICATION = /^\w+(\.\w+)*$/; - -// For a graceful shutdown (i.e., when the extension is unloaded or when it -// explicitly calls disconnect() on a native port), how long we give the native -// application to exit before we start trying to kill it. (in milliseconds) -const GRACEFUL_SHUTDOWN_TIME = 3000; - -// Hard limits on maximum message size that can be read/written -// These are defined in the native messaging documentation, note that -// the write limit is imposed by the "wire protocol" in which message -// boundaries are defined by preceding each message with its length as -// 4-byte unsigned integer so this is the largest value that can be -// represented. Good luck generating a serialized message that large, -// the practical write limit is likely to be dictated by available memory. -const MAX_READ = 1024 * 1024; -const MAX_WRITE = 0xffffffff; - -// Preferences that can lower the message size limits above, -// used for testing the limits. -const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes"; -const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes"; - -const REGPATH = "Software\\Mozilla\\NativeMessagingHosts"; - -this.HostManifestManager = { - _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("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; - }, - - _winLookup(application, context) { - const REGISTRY = Ci.nsIWindowsRegKey; - let regPath = `${REGPATH}\\${application}`; - 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) - .then(manifest => manifest ? {path, manifest} : null); - }, - - _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 Error(`Invalid application "${application}"`); - } - return this.init().then(() => this._lookup(application, context)); - }, -}; - -this.NativeApp = class extends EventEmitter { - /** - * @param {BaseContext} context The context that initiated the native app. - * @param {string} application The identifier of the native app. - */ - constructor(context, application) { - super(); - - this.context = context; - this.name = application; - - // We want a close() notification when the window is destroyed. - this.context.callOnClose(this); - - this.proc = null; - this.readPromise = null; - this.sendQueue = []; - this.writePromise = null; - this.sentDisconnect = false; - - this.startupPromise = HostManifestManager.lookupApplication(application, context) - .then(hostInfo => { - // Put the two errors together 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)`); - } - - 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); - } - - let subprocessOpts = { - command: command, - arguments: [hostInfo.path], - workdir: OS.Path.dirname(command), - stderr: "pipe", - }; - return Subprocess.call(subprocessOpts); - }).then(proc => { - this.startupPromise = null; - this.proc = proc; - this._startRead(); - this._startWrite(); - this._startStderrRead(); - }).catch(err => { - this.startupPromise = null; - Cu.reportError(err instanceof Error ? err : err.message); - this._cleanup(err); - }); - } - - /** - * Open a connection to a native messaging host. - * - * @param {BaseContext} context The context associated with the port. - * @param {nsIMessageSender} messageManager The message manager used to send - * and receive messages from the port's creator. - * @param {string} portId A unique internal ID that identifies the port. - * @param {object} sender The object describing the creator of the connection - * request. - * @param {string} application The name of the native messaging host. - */ - static onConnectNative(context, messageManager, portId, sender, application) { - let app = new NativeApp(context, application); - let port = new ExtensionChild.Port(context, messageManager, [Services.mm], "", portId, sender, sender); - app.once("disconnect", (what, err) => port.disconnect(err)); - - /* eslint-disable mozilla/balanced-listeners */ - app.on("message", (what, msg) => port.postMessage(msg)); - /* eslint-enable mozilla/balanced-listeners */ - - port.registerOnMessage(msg => app.send(msg)); - port.registerOnDisconnect(msg => app.close()); - } - - /** - * @param {BaseContext} context The scope from where `message` originates. - * @param {*} message A message from the extension, meant for a native app. - * @returns {ArrayBuffer} An ArrayBuffer that can be sent to the native app. - */ - static encodeMessage(context, message) { - message = context.jsonStringify(message); - let buffer = new TextEncoder().encode(message).buffer; - if (buffer.byteLength > NativeApp.maxWrite) { - throw new context.cloneScope.Error("Write too big"); - } - return buffer; - } - - // A port is definitely "alive" if this.proc is non-null. But we have - // to provide a live port object immediately when connecting so we also - // need to consider a port alive if proc is null but the startupPromise - // is still pending. - get _isDisconnected() { - return (!this.proc && !this.startupPromise); - } - - _startRead() { - if (this.readPromise) { - throw new Error("Entered _startRead() while readPromise is non-null"); - } - this.readPromise = this.proc.stdout.readUint32() - .then(len => { - if (len > NativeApp.maxRead) { - throw new this.context.cloneScope.Error(`Native application tried to send a message of ${len} bytes, which exceeds the limit of ${NativeApp.maxRead} bytes.`); - } - return this.proc.stdout.readJSON(len); - }).then(msg => { - this.emit("message", msg); - this.readPromise = null; - this._startRead(); - }).catch(err => { - if (err.errorCode != Subprocess.ERROR_END_OF_FILE) { - Cu.reportError(err instanceof Error ? err : err.message); - } - this._cleanup(err); - }); - } - - _startWrite() { - if (this.sendQueue.length == 0) { - return; - } - - if (this.writePromise) { - throw new Error("Entered _startWrite() while writePromise is non-null"); - } - - let buffer = this.sendQueue.shift(); - let uintArray = Uint32Array.of(buffer.byteLength); - - this.writePromise = Promise.all([ - this.proc.stdin.write(uintArray.buffer), - this.proc.stdin.write(buffer), - ]).then(() => { - this.writePromise = null; - this._startWrite(); - }).catch(err => { - Cu.reportError(err.message); - this._cleanup(err); - }); - } - - _startStderrRead() { - let proc = this.proc; - let app = this.name; - Task.spawn(function* () { - let partial = ""; - while (true) { - let data = yield proc.stderr.readString(); - if (data.length == 0) { - // We have hit EOF, just stop reading - if (partial) { - Services.console.logStringMessage(`stderr output from native app ${app}: ${partial}`); - } - break; - } - - let lines = data.split(/\r?\n/); - lines[0] = partial + lines[0]; - partial = lines.pop(); - - for (let line of lines) { - Services.console.logStringMessage(`stderr output from native app ${app}: ${line}`); - } - } - }); - } - - send(msg) { - if (this._isDisconnected) { - throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port"); - } - if (Cu.getClassName(msg, true) != "ArrayBuffer") { - // This error cannot be triggered by extensions; it indicates an error in - // our implementation. - throw new Error("The message to the native messaging host is not an ArrayBuffer"); - } - - let buffer = msg; - - if (buffer.byteLength > NativeApp.maxWrite) { - throw new this.context.cloneScope.Error("Write too big"); - } - - this.sendQueue.push(buffer); - if (!this.startupPromise && !this.writePromise) { - this._startWrite(); - } - } - - // Shut down the native application and also signal to the extension - // that the connect has been disconnected. - _cleanup(err) { - this.context.forgetOnClose(this); - - let doCleanup = () => { - // Set a timer to kill the process gracefully after one timeout - // interval and kill it forcefully after two intervals. - let timer = setTimeout(() => { - this.proc.kill(GRACEFUL_SHUTDOWN_TIME); - }, GRACEFUL_SHUTDOWN_TIME); - - let promise = Promise.all([ - this.proc.stdin.close() - .catch(err => { - if (err.errorCode != Subprocess.ERROR_END_OF_FILE) { - throw err; - } - }), - this.proc.wait(), - ]).then(() => { - this.proc = null; - clearTimeout(timer); - }); - - AsyncShutdown.profileBeforeChange.addBlocker( - `Native Messaging: Wait for application ${this.name} to exit`, - promise); - - promise.then(() => { - AsyncShutdown.profileBeforeChange.removeBlocker(promise); - }); - - return promise; - }; - - if (this.proc) { - doCleanup(); - } else if (this.startupPromise) { - this.startupPromise.then(doCleanup); - } - - if (!this.sentDisconnect) { - this.sentDisconnect = true; - if (err && err.errorCode == Subprocess.ERROR_END_OF_FILE) { - err = null; - } - this.emit("disconnect", err); - } - } - - // Called from Context when the extension is shut down. - close() { - this._cleanup(); - } - - sendMessage(msg) { - let responsePromise = new Promise((resolve, reject) => { - this.once("message", (what, msg) => { resolve(msg); }); - this.once("disconnect", (what, err) => { reject(err); }); - }); - - let result = this.startupPromise.then(() => { - this.send(msg); - return responsePromise; - }); - - result.then(() => { - this._cleanup(); - }, () => { - // Prevent the response promise from being reported as an - // unchecked rejection if the startup promise fails. - responsePromise.catch(() => {}); - - this._cleanup(); - }); - - return result; - } -}; - -XPCOMUtils.defineLazyPreferenceGetter(NativeApp, "maxRead", PREF_MAX_READ, MAX_READ); -XPCOMUtils.defineLazyPreferenceGetter(NativeApp, "maxWrite", PREF_MAX_WRITE, MAX_WRITE); |