const Generator = require('yeoman-generator'); const chalk = require('chalk'); const wrap = require('wrap-ansi'); const html2ansi = require('./lib/html2ansi.js'); const fs = require('fs'); const validator = require('validator'); const path = require("path"); const coinstring = require('coinstring'); const name = require('./lib/name.js'); const Archive = require('./lib/archive.js'); const ApiKey = require('./lib/apikey.js'); const Cert = require('./lib/cert.js'); const htpasswd = require( './lib/htpasswd.js') const featureChoices = require('./features.json'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars const userRegexp = /^[a-zA-Z0-9\._\-]+$/; const reset = '\u001B8\u001B[u'; const clear = '\u001Bc'; const configFileVersion='0.1.0'; const defaultAPIProperties = ` # Stats can: action_getblockchaininfo=stats # Watcher can: action_watch=watcher action_unwatch=watcher action_watchxpub=watcher action_unwatchxpubbyxpub=watcher action_unwatchxpubbylabel=watcher action_getactivewatchesbyxpub=watcher action_getactivewatchesbylabel=watcher action_getactivexpubwatches=watcher action_watchtxid=watcher action_getactivewatches=watcher action_getbestblockhash=watcher action_getbestblockinfo=watcher action_getblockinfo=watcher action_gettransaction=watcher action_ln_getinfo=watcher action_ln_create_invoice=watcher action_ln_getconnectionstring=watcher action_ln_decodebolt11=watcher # Spender can do what the watcher can do, plus: action_getbalance=spender action_getbalancebyxpub=spender action_getbalancebyxpublabel=spender action_getnewaddress=spender action_spend=spender action_addtobatch=spender action_batchspend=spender action_deriveindex=spender action_derivepubpath=spender action_ln_pay=spender action_ln_newaddr=spender action_ots_stamp=spender action_ots_getfile=spender action_ln_getinvoice=spender action_ln_decodebolt11=spender action_ln_connectfund=spender # Admin can do what the spender can do, plus: # Should be called from inside the Docker network only: action_conf=internal action_newblock=internal action_executecallbacks=internal action_ots_backoffice=internal `; const prefix = function() { return chalk.green('Cyphernode')+': '; }; let prompters = []; fs.readdirSync(path.join(__dirname, "prompters")).forEach(function(file) { prompters.push(require(path.join(__dirname, "prompters",file))); }); const sleep = function(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } const easeOutCubic = function(t, b, c, d) { return c*((t=t/d-1)*t*t+1)+b; } const splash = async function() { let frames = []; fs.readdirSync(path.join(__dirname,'splash')).forEach(function(file) { frames.push(fs.readFileSync(path.join(__dirname,'splash',file))); }); const frame0 = frames[0]; const frame0lines = frame0.toString().split('\n'); const frame0lineCount = frame0lines.length; const steps = 10; process.stdout.write(clear); await sleep(150); for( let i=0; i<=steps; i++ ) { const pos = easeOutCubic( i, 0, frame0lineCount, steps ) | 0; process.stdout.write(reset); for( let l=frame0lineCount-pos; l { this.props = Object.assign(this.props, props); }); } async configuring() { if( this.props.gatekeeper_recreatekeys || this.props.gatekeeper_keys.configEntries.length===0 ) { const apikey = new ApiKey(); let configEntries = []; let clientInformation = []; apikey.setId('000'); apikey.setGroups(['stats']); await apikey.randomiseKey(); configEntries.push(apikey.getConfigEntry()); clientInformation.push(apikey.getClientInformation()); apikey.setId('001'); apikey.setGroups(['stats','watcher']); await apikey.randomiseKey(); configEntries.push(apikey.getConfigEntry()); clientInformation.push(apikey.getClientInformation()); apikey.setId('002'); apikey.setGroups(['stats','watcher','spender']); await apikey.randomiseKey(); configEntries.push(apikey.getConfigEntry()); clientInformation.push(apikey.getClientInformation()); apikey.setId('003'); apikey.setGroups(['stats','watcher','spender','admin']); await apikey.randomiseKey(); configEntries.push(apikey.getConfigEntry()); clientInformation.push(apikey.getClientInformation()); this.props.gatekeeper_keys = { configEntries: configEntries, clientInformation: clientInformation } } if( this.props.gatekeeper_recreatecert || !this.props.gatekeeper_sslcert || !this.props.gatekeeper_sslkey ) { delete this.props.gatekeeper_recreatecert; const cert = new Cert(); console.log(chalk.bold.green( '☕ Generating gatekeeper cert. This may take a while ☕' )); try { const cns = (this.props.gatekeeper_cns||'').split(',').map(e=>e.trim().toLowerCase()).filter(e=>!!e); const result = await cert.create(cns); if( result.code === 0 ) { this.props.gatekeeper_sslkey = result.key.toString(); this.props.gatekeeper_sslcert = result.cert.toString(); // Total array of cns, used to create Cyphernode's URLs this.props.cns = [] result.cns.forEach(e => { this.props.cns.push(e) }) } else { console.log(chalk.bold.red( 'error! Gatekeeper cert was not created' )); } } catch( err ) { console.log(chalk.bold.red( 'error! Gatekeeper cert was not created' )); } } delete this.props.gatekeeper_recreatekeys; } async writing() { this._resolveConfigConflicts() const configJsonString = JSON.stringify(this.props, null, 4); const archive = new Archive( this.destinationPath('config.7z'), this.configurationPassword ); if( !await archive.writeEntry( 'config.json', configJsonString ) ) { console.log(chalk.bold.red( 'error! Config archive was not written' )); } const pathProps = [ 'gatekeeper_datapath', 'traefik_datapath', 'proxy_datapath', 'bitcoin_datapath', 'lightning_datapath', 'otsclient_datapath' ]; for( let pathProp of pathProps ) { if( this.props[pathProp] === '_custom' ) { this.props[pathProp] = this.props[pathProp+'_custom'] || ''; } } for( let m of prompters ) { const name = m.name(); for( let t of m.templates(this.props) ) { const p = path.join(name,t); this.fs.copyTpl( this.templatePath(p), this.destinationPath(p), this.props ); } } if( this.props.gatekeeper_keys && this.props.gatekeeper_keys.clientInformation ) { if( this.gatekeeper_clientkeyspassword !== this.props.gatekeeper_clientkeyspassword && fs.existsSync(this.destinationPath('client.7z')) ) { fs.unlinkSync( this.destinationPath('client.7z') ); } const archive = new Archive( this.destinationPath('client.7z'), this.props.gatekeeper_clientkeyspassword ); if( !await archive.writeEntry( 'keys.txt', this.props.gatekeeper_keys.clientInformation.join('\n') ) ) { console.log(chalk.bold.red( 'error! Client gatekeeper key archive was not written' )); } if( !await archive.writeEntry( 'cacert.pem', this.props.gatekeeper_sslcert ) ) { console.log(chalk.bold.red( 'error! Client gatekeeper key archive was not written' )); } } fs.writeFileSync(path.join('/data', 'exitStatus.sh'), 'EXIT_STATUS=0'); } install() { } /* some utils */ _resolveConfigConflicts() { if( this.props.features && this.props.features.length && this.props.features.indexOf('lightning') !== -1 ) { this.props.bitcoin_prune = false; delete this.props.bitcoin_prune_size; } } _assignConfigDefaults() { this.props = Object.assign( { features: [], enablehelp: true, net: 'testnet', xpub: '', derivation_path: '0/n', installer_mode: 'docker', devmode: false, devregistry: false, run_as_different_user: true, username: 'cyphernode', docker_mode: 'compose', bitcoin_rpcuser: 'bitcoin', bitcoin_rpcpassword: 'CHANGEME', bitcoin_uacomment: '', bitcoin_prune: false, bitcoin_prune_size: 550, bitcoin_datapath: '', bitcoin_node_ip: '', bitcoin_mode: 'internal', bitcoin_expose: false, lightning_expose: true, gatekeeper_port: 2009, gatekeeper_apiproperties: defaultAPIProperties, gatekeeper_ipwhitelist: '', gatekeeper_keys: { configEntries: [], clientInformation: [] }, gatekeeper_sslcert: '', gatekeeper_sslkey: '', gatekeeper_cns: process.env['DEFAULT_CERT_HOSTNAME'] || '', gatekeeper_datapath: '', proxy_datapath: '', lightning_implementation: 'c-lightning', lightning_external_ip: '', lightning_datapath: '', lightning_nodename: name.generate(), lightning_nodecolor: '', otsclient_datapath: '', traefik_datapath: '', installer_cleanup: false, default_username: process.env.DEFAULT_USER || '', gatekeeper_version: process.env.GATEKEEPER_VERSION || 'latest', proxy_version: process.env.PROXY_VERSION || 'latest', notifier_version: process.env.NOTIFIER_VERSION || 'latest', proxycron_version: process.env.PROXYCRON_VERSION || 'latest', pycoin_version: process.env.PYCOIN_VERSION || 'latest', otsclient_version: process.env.OTSCLIENT_VERSION || 'latest', bitcoin_version: process.env.BITCOIN_VERSION || 'latest', lightning_version: process.env.LIGHTNING_VERSION || 'latest', sparkwallet_version: process.env.SPARKWALLET_VERSION || 'standalone' }, this.props ); } _isChecked( name, value ) { return this.props && this.props[name] && this.props[name].indexOf(value) != -1 ; } _getDefault( name ) { return this.props && this.props[name]; } _optional(input,validator) { if( input === undefined || input === null || input === '' ) { return true; } return validator(input); } _ipOrFQDNValidator( host ) { host = (host+"").trim(); if( !(validator.isIP(host) || validator.isFQDN(host)) ) { throw new Error( 'No IP address or fully qualified domain name' ) } return true; } _xkeyValidator( xpub ) { // TOOD: check for version if( !coinstring.isValid( xpub ) ) { throw new Error('Not an extended key.'); } return true; } _pathValidator( p ) { return true; } _derivationPathValidator( path ) { return true; } _colorValidator(color) { if( !validator.isHexadecimal(color) ) { throw new Error('Not a hex color.'); } return true; } _lightningNodeNameValidator(name) { if( !name || name.length > 32 ) { throw new Error('Please enter anything shorter than 32 characters'); } return true; } _notEmptyValidator( path ) { if( !path ) { throw new Error('Please enter something'); } return true; } _usernameValidator( user ) { if( !userRegexp.test( user ) ) { throw new Error('Choose a valid username'); } return true; } _UACommentValidator( comment ) { if( !uaCommentRegexp.test( comment ) ) { throw new Error('Unsafe characters in UA comment. Please use only a-z, A-Z, 0-9, SPACE and .,:_?@'); } return true; } _trimFilter( input ) { return (input+"").trim(); } _featureChoices() { return this.featureChoices; } _getHelp( topic ) { if( !this.props.enablehelp || !this.help ) { return ''; } const helpText = this.help[topic] || this.help['__default__']; if( !helpText ||helpText === '' ) { return ''; } return "\n\n"+wrap( html2ansi(helpText),82 )+"\n\n"; } };