import * as child_process from 'child_process' ; import * as jsyaml from 'js-yaml' ; import * as mkdirp from 'mkdirp' ; import * as path from 'path' ; import * as file from '../infrastructure/file' ; import * as logging from '../infrastructure/logging' ; import { ShadowsocksAccessKey , ShadowsocksServer } from '../model/shadowsocks_server' ; export class OutlineShadowsocksServer implements ShadowsocksServer { private ssProcess : child_process . ChildProcess ; private ipCountryFilename = '' ; private isReplayProtectionEnabled = false ; constructor ( private readonly binaryFilename , private readonly configFilename , private readonly verbose , private readonly metricsLocation ) { } enableCountryMetrics ( ipCountryFilename ) { this . ipCountryFilename = ipCountryFilename ; return this ; } enableReplayProtection ( ) { this . isReplayProtectionEnabled = true ; return this ; } update ( keys ) { return this . writeConfigFile ( keys ) . then ( ( ) => { if ( ! this . ssProcess ) { this . start ( ) ; return Promise . resolve ( ) ; } else { this . ssProcess . kill ( 'SIGHUP' ) ; } } ) ; } private writeConfigFile ( keys ) { return new Promise ( ( resolve , reject ) => { const keysJson = { keys : [ ] as ShadowsocksAccessKey [ ] } ; for ( const key of keys ) { if ( ! isAeadCipher ( key . cipher ) ) { logging . error ( `Cipher ${ key . cipher } for access key ${ key . id } is not supported: use an AEAD cipher instead.` ) ; continue ; } keysJson . keys . push ( key ) ; } mkdirp . sync ( path . dirname ( this . configFilename ) ) ; try { file . atomicWriteFileSync ( this . configFilename , jsyaml . safeDump ( keysJson , { sortKeys : true } ) ) ; resolve ( ) ; } catch ( error ) { reject ( error ) ; } } ) ; } private start ( ) { const commandArguments = [ '-config' , this . configFilename , '-metrics' , this . metricsLocation ] ; if ( this . ipCountryFilename ) { commandArguments . push ( '-ip_country_db' , this . ipCountryFilename ) ; } if ( this . verbose ) { commandArguments . push ( '-verbose' ) ; } if ( this . isReplayProtectionEnabled ) { commandArguments . push ( '--replay_history=10000' ) ; } this . ssProcess = child_process . spawn ( this . binaryFilename , commandArguments ) ; this . ssProcess . on ( 'error' , ( error ) => { logging . error ( `Error spawning outline-ss-server: ${ error }` ) ; } ) ; this . ssProcess . on ( 'exit' , ( code , signal ) => { logging . info ( `outline-ss-server has exited with error. Code: ${ code }, Signal: ${ signal }` ) ; logging . info ( `Restarting` ) ; this . start ( ) ; } ) ; this . ssProcess . stdout . pipe ( process . stdout ) ; this . ssProcess . stderr . pipe ( process . stderr ) ; } } function isAeadCipher ( cipherAlias ) { cipherAlias = cipherAlias . toLowerCase ( ) ; return cipherAlias . endsWith ( 'gcm' ) || cipherAlias . endsWith ( 'poly1305' ) ; }