2019-03-12 09:34:24 -07:00
/ * T h i s S o u r c e C o d e F o r m i s s u b j e c t t o t h e t e r m s o f t h e M o z i l l a P u b l i c
* 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 , "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 ) {
2020-03-12 10:42:04 -07:00
# ifdef MOZ _WIDGET _GTK
let platform = "linux" ;
# elif XP _WIN
let platform = "win" ;
# elif XP _MACOSX
let platform = "macosx" ;
# elif MOZ _WIDGET _ANDROID
let platform = "android" ;
# elif XP _LINUX
let platform = "linux" ;
# else
let platform = "other" ;
# endif
2019-03-12 09:34:24 -07:00
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 {
2020-03-12 10:42:04 -07:00
throw new Error ( ` Native messaging is not supported on ${ platform } ` ) ;
2019-03-12 09:34:24 -07:00
}
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 ;
2020-03-12 10:42:04 -07:00
# if XP _WIN
2019-03-12 09:34:24 -07:00
// 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 ) ;
2020-03-12 10:42:04 -07:00
# endif
2019-03-12 09:34:24 -07:00
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 ) ;