switch over to commander
This commit is contained in:
		
							parent
							
								
									20c7bc977c
								
							
						
					
					
						commit
						4267955286
					
				| @ -1,687 +1,67 @@ | ||||
| #!/usr/bin/env node
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| //var PromiseA = global.Promise;
 | ||||
| var PromiseA = require('bluebird'); | ||||
| var tls = require('tls'); | ||||
| var https = require('httpolyglot'); | ||||
| var http = require('http'); | ||||
| var path = require('path'); | ||||
| var DDNS = require('ddns-cli'); | ||||
| var httpPort = 80; | ||||
| var httpsPort = 443; | ||||
| var lrPort = 35729; | ||||
| var portFallback = 8443; | ||||
| var insecurePortFallback = 4080; | ||||
| 
 | ||||
| function showError(err, port) { | ||||
|   if ('EACCES' === err.code) { | ||||
|     console.error(err); | ||||
|     console.warn("You do not have permission to use '" + port + "'."); | ||||
|     console.warn("You can probably fix that by running as Administrator or root."); | ||||
|   } | ||||
|   else if ('EADDRINUSE' === err.code) { | ||||
|     console.warn("Another server is already running on '" + port + "'."); | ||||
|     console.warn("You can probably fix that by rebooting your computer (or stopping it if you know what it is)."); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function createInsecureServer(port, _delete_me_, opts) { | ||||
|   return new PromiseA(function (realResolve) { | ||||
|     var server = http.createServer(); | ||||
| 
 | ||||
|     function resolve() { | ||||
|       realResolve(server); | ||||
|     } | ||||
| 
 | ||||
|     server.on('error', function (err) { | ||||
|       if (opts.errorInsecurePort || opts.manualInsecurePort) { | ||||
|         showError(err, port); | ||||
|         process.exit(1); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       opts.errorInsecurePort = err.toString(); | ||||
| 
 | ||||
|       return createInsecureServer(insecurePortFallback, null, opts).then(resolve); | ||||
|     }); | ||||
| 
 | ||||
|     server.on('request', opts.redirectApp); | ||||
| 
 | ||||
|     server.listen(port, function () { | ||||
|       opts.insecurePort = port; | ||||
|       resolve(); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function createServer(port, _delete_me_, content, opts) { | ||||
|   function approveDomains(params, certs, cb) { | ||||
|     // This is where you check your database and associated
 | ||||
|     // email addresses with domains and agreements and such
 | ||||
|     var domains = params.domains; | ||||
|     //var p;
 | ||||
|     console.log('approveDomains'); | ||||
|     console.log(domains); | ||||
| 
 | ||||
| 
 | ||||
|     // The domains being approved for the first time are listed in opts.domains
 | ||||
|     // Certs being renewed are listed in certs.altnames
 | ||||
|     if (certs) { | ||||
|       params.domains = certs.altnames; | ||||
|       //p = PromiseA.resolve();
 | ||||
|     } | ||||
|     else { | ||||
|       //params.email = opts.email;
 | ||||
|       if (!opts.agreeTos) { | ||||
|         console.error("You have not previously registered '" + domains + "' so you must specify --agree-tos to agree to both the Let's Encrypt and Daplie DNS terms of service."); | ||||
|         process.exit(1); | ||||
|         return; | ||||
|       } | ||||
|       params.agreeTos = opts.agreeTos; | ||||
|     } | ||||
| 
 | ||||
|     // ddns.token(params.email, domains[0])
 | ||||
|     params.email = opts.email; | ||||
|     params.refreshToken = opts.refreshToken; | ||||
|     params.challengeType = 'dns-01'; | ||||
|     params.cli = opts.argv; | ||||
| 
 | ||||
|     cb(null, { options: params, certs: certs }); | ||||
|   } | ||||
| 
 | ||||
|   return new PromiseA(function (realResolve) { | ||||
|     var app = require('../lib/app.js'); | ||||
|     var ipaddr = require('ipaddr.js'); | ||||
|     var addresses = []; | ||||
| 
 | ||||
|     Object.keys(opts.ifaces).forEach(function (ifacename) { | ||||
|       var iface = opts.ifaces[ifacename]; | ||||
|       iface.ipv4.forEach(function (ip) { | ||||
|         addresses.push(ip); | ||||
|       }); | ||||
|       iface.ipv6.forEach(function (ip) { | ||||
|         addresses.push(ip); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     addresses.sort(function (a, b) { | ||||
|       if (a.family !== b.family) { | ||||
|         return 'IPv4' === a.family ? 1 : -1; | ||||
|       } | ||||
| 
 | ||||
|       return a.address > b.address ? 1 : -1; | ||||
|     }); | ||||
| 
 | ||||
|     addresses.forEach(function (addr) { | ||||
|       addr.range = ipaddr.parse(addr.address).range(); | ||||
|     }); | ||||
| 
 | ||||
|     var Oauth3 = require('oauth3-cli'); | ||||
|     var oauth3 = Oauth3.create({ device: { hostname: opts.device } }); | ||||
|     return Oauth3.Devices.one(oauth3).then(function (device) { | ||||
|       return Oauth3.Devices.all(oauth3).then(function (devices) { | ||||
|         return { devices: devices, device: device.device || device }; | ||||
|       }); | ||||
|     }).then(function (devices) { | ||||
|       devices.device.secret = undefined; | ||||
|       console.log('devices'); | ||||
|       console.log(devices); | ||||
|       var directive = { | ||||
|         global: opts.global | ||||
|       , sites: opts.sites | ||||
|       , defaults: opts.defaults | ||||
|       , cwd: process.cwd() | ||||
|       , ifaces: opts.ifaces | ||||
|       , addresses: addresses | ||||
|       , devices: devices.devices | ||||
|       , device: devices.device | ||||
|       , net: { | ||||
|           createConnection: function (opts, cb) { | ||||
|             // opts = { host, port, data
 | ||||
|             //        , /*proprietary to tunneler*/ servername, remoteAddress, remoteFamily, remotePort
 | ||||
|             //        , secure (tls already terminated by a proxy) }
 | ||||
|             //        // http://stackoverflow.com/questions/10348906/how-to-know-if-a-request-is-http-or-https-in-node-js
 | ||||
|             // var packerStream = require('tunnel-packer').Stream;
 | ||||
|             // TODO here we will have the tls termination (or re-forward)
 | ||||
|           } | ||||
|         } | ||||
|       }; | ||||
|       var server; | ||||
|       var insecureServer; | ||||
| 
 | ||||
|       function resolve() { | ||||
|         realResolve({ | ||||
|           plainServer: insecureServer | ||||
|         , server: server | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       // returns an instance of node-letsencrypt with additional helper methods
 | ||||
|       var webrootPath = require('os').tmpdir(); | ||||
|       var leChallengeFs = require('le-challenge-fs').create({ webrootPath: webrootPath }); | ||||
|       //var leChallengeSni = require('le-challenge-sni').create({ webrootPath: webrootPath });
 | ||||
|       var leChallengeDdns = require('le-challenge-ddns').create({ ttl: 1 }); | ||||
|       var lex = require('greenlock-express').create({ | ||||
|         // set to https://acme-v01.api.letsencrypt.org/directory in production
 | ||||
|         server: opts.debug ? 'staging' : 'https://acme-v01.api.letsencrypt.org/directory' | ||||
| 
 | ||||
|       // If you wish to replace the default plugins, you may do so here
 | ||||
|       //
 | ||||
|       , challenges: { | ||||
|           'http-01': leChallengeFs | ||||
|         , 'tls-sni-01': leChallengeFs // leChallengeSni
 | ||||
|         , 'dns-01': leChallengeDdns | ||||
|         } | ||||
|       , challengeType: (opts.tunnel ? 'http-01' : 'dns-01') | ||||
|       , store: require('le-store-certbot').create({ | ||||
|           webrootPath: webrootPath | ||||
|         , configDir: path.join((opts.homedir || '~'), 'letsencrypt', 'etc') | ||||
|         , homedir: opts.homedir | ||||
|         }) | ||||
|       , webrootPath: webrootPath | ||||
| 
 | ||||
|       // You probably wouldn't need to replace the default sni handler
 | ||||
|       // See https://git.daplie.com/Daplie/le-sni-auto if you think you do
 | ||||
|       //, sni: require('le-sni-auto').create({})
 | ||||
| 
 | ||||
|       , approveDomains: approveDomains | ||||
|       }); | ||||
| 
 | ||||
|       var secureContexts = { | ||||
|         'localhost.daplie.me': null | ||||
|       }; | ||||
|       opts.httpsOptions.SNICallback = function (sni, cb ) { | ||||
|         var tlsOptions; | ||||
|         console.log('[https] sni', sni); | ||||
| 
 | ||||
|         // Static Certs
 | ||||
|         if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) { | ||||
|           // TODO implement
 | ||||
|           if (!secureContexts[sni]) { | ||||
|             tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {}); | ||||
|           } | ||||
|           if (tlsOptions) { | ||||
|             secureContexts[sni] = tls.createSecureContext(tlsOptions); | ||||
|           } | ||||
|           cb(null, secureContexts[sni]); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         // Dynamic Certs
 | ||||
|         lex.httpsOptions.SNICallback(sni, cb); | ||||
|       }; | ||||
|       server = https.createServer(opts.httpsOptions); | ||||
| 
 | ||||
|       server.on('error', function (err) { | ||||
|         if (opts.errorPort || opts.manualPort) { | ||||
|           showError(err, port); | ||||
|           process.exit(1); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         opts.errorPort = err.toString(); | ||||
| 
 | ||||
|         return createServer(portFallback, null, content, opts).then(resolve); | ||||
|       }); | ||||
| 
 | ||||
|       server.listen(port, function () { | ||||
|         opts.port = port; | ||||
|         opts.redirectOptions.port = port; | ||||
| 
 | ||||
|         if (opts.livereload) { | ||||
|           opts.lrPort = opts.lrPort || lrPort; | ||||
|           var livereload = require('livereload'); | ||||
|           var server2 = livereload.createServer({ | ||||
|             https: opts.httpsOptions | ||||
|           , port: opts.lrPort | ||||
|           , exclusions: [ 'node_modules' ] | ||||
|           }); | ||||
| 
 | ||||
|           console.info("[livereload] watching " + opts.pubdir); | ||||
|           console.warn("WARNING: If CPU usage spikes to 100% it's because too many files are being watched"); | ||||
|           // TODO create map of directories to watch from opts.sites and iterate over it
 | ||||
|           server2.watch(opts.pubdir); | ||||
|         } | ||||
| 
 | ||||
|         // if we haven't disabled insecure port
 | ||||
|         if ('false' !== opts.insecurePort) { | ||||
|           // and both ports are the default
 | ||||
|           if ((httpsPort === opts.port && httpPort === opts.insecurePort) | ||||
|             // or other case
 | ||||
|             || (httpPort !== opts.insecurePort && opts.port !== opts.insecurePort) | ||||
|           ) { | ||||
|             return createInsecureServer(opts.insecurePort, null, opts).then(function (_server) { | ||||
|               insecureServer = _server; | ||||
|               resolve(); | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         opts.insecurePort = opts.port; | ||||
|         resolve(); | ||||
|         return; | ||||
|       }); | ||||
| 
 | ||||
|       if ('function' === typeof app) { | ||||
|         app = app(directive); | ||||
|       } else if ('function' === typeof app.create) { | ||||
|         app = app.create(directive); | ||||
|       } | ||||
| 
 | ||||
|       server.on('request', function (req, res) { | ||||
|         console.log('[' + req.method + '] ' + req.url); | ||||
|         if (!req.socket.encrypted && !/\/\.well-known\/acme-challenge\//.test(req.url)) { | ||||
|           opts.redirectApp(req, res); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         if ('function' === typeof app) { | ||||
|           app(req, res); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         res.end('not ready'); | ||||
|       }); | ||||
| 
 | ||||
|       return PromiseA.resolve(app).then(function (_app) { | ||||
|         app = _app; | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| module.exports.createServer = createServer; | ||||
| 
 | ||||
| function run() { | ||||
|   var defaultServername = 'localhost.daplie.me'; | ||||
|   var minimist = require('minimist'); | ||||
|   var argv = minimist(process.argv.slice(2)); | ||||
|   var port = parseInt(argv.p || argv.port || argv._[0], 10) || httpsPort; | ||||
|   var livereload = argv.livereload; | ||||
|   var defaultWebRoot = path.normalize(argv['default-web-root'] || argv.d || argv._[1] || '.'); | ||||
|   var assetsPath = path.join(__dirname, '..', 'packages', 'assets'); | ||||
|   var content = argv.c; | ||||
|   var letsencryptHost = argv['letsencrypt-certs']; | ||||
|   var yaml = require('js-yaml'); | ||||
|   var fs = PromiseA.promisifyAll(require('fs')); | ||||
|   var configFile = argv.c || argv.conf || argv.config; | ||||
| function readConfigAndRun(args) { | ||||
|   var fs = require('fs'); | ||||
|   var path = require('path'); | ||||
|   var cwd = args.cwd || process.cwd(); | ||||
|   var text; | ||||
|   var filename; | ||||
|   var config; | ||||
|   console.log('defaultWebRoot', defaultWebRoot); | ||||
| 
 | ||||
|   try { | ||||
|     config = fs.readFileSync(configFile || 'Goldilocks.yml'); | ||||
|   } catch(e) { | ||||
|     if (configFile) { | ||||
|       console.error('Failed to read config:', e); | ||||
|       process.exit(1); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (config) { | ||||
|     try { | ||||
|       config = yaml.safeLoad(config); | ||||
|     } catch(e) { | ||||
|       console.error('Failed to parse config:', e); | ||||
|       process.exit(1); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (argv.V || argv.version || argv.v) { | ||||
|     if (argv.v) { | ||||
|       console.warn("flag -v is reserved for future use. Use -V or --version for version information."); | ||||
|     } | ||||
|     console.info('v' + require('../package.json').version); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   argv.sites = argv.sites; | ||||
| 
 | ||||
|   // letsencrypt
 | ||||
|   var httpsOptions = require('localhost.daplie.me-certificates').merge({}); | ||||
|   var secureContext; | ||||
| 
 | ||||
|   var opts = { | ||||
|     agreeTos: argv.agreeTos || argv['agree-tos'] | ||||
|   , debug: argv.debug | ||||
|   , device: argv.device | ||||
|   , provider: (argv.provider && 'false' !== argv.provider) ? argv.provider : 'oauth3.org' | ||||
|   , email: argv.email | ||||
|   , httpsOptions: { | ||||
|       key: httpsOptions.key | ||||
|     , cert: httpsOptions.cert | ||||
|     //, ca: httpsOptions.ca
 | ||||
|     } | ||||
|   , homedir: argv.homedir | ||||
|   , argv: argv | ||||
|   }; | ||||
|   var peerCa; | ||||
|   var p; | ||||
| 
 | ||||
|   opts.PromiseA = PromiseA; | ||||
|   opts.httpsOptions.SNICallback = function (sni, cb) { | ||||
|     if (!secureContext) { | ||||
|       secureContext = tls.createSecureContext(opts.httpsOptions); | ||||
|     } | ||||
|     cb(null, secureContext); | ||||
|     return; | ||||
|   }; | ||||
| 
 | ||||
|   if (letsencryptHost) { | ||||
|     // TODO remove in v3.x (aka goldilocks)
 | ||||
|     argv.key = argv.key || '/etc/letsencrypt/live/' + letsencryptHost + '/privkey.pem'; | ||||
|     argv.cert = argv.cert || '/etc/letsencrypt/live/' + letsencryptHost + '/fullchain.pem'; | ||||
|     argv.root = argv.root || argv.chain || ''; | ||||
|     argv.sites = argv.sites || letsencryptHost; | ||||
|     argv['serve-root'] = argv['serve-root'] || argv['serve-chain']; | ||||
|     // argv[express-app]
 | ||||
|   } | ||||
| 
 | ||||
|   if (argv['serve-root'] && !argv.root) { | ||||
|     console.error("You must specify bath --root to use --serve-root"); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (argv.key || argv.cert || argv.root) { | ||||
|     if (!argv.key || !argv.cert) { | ||||
|       console.error("You must specify bath --key and --cert, and optionally --root (required with serve-root)"); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (!Array.isArray(argv.root)) { | ||||
|       argv.root = [argv.root]; | ||||
|     } | ||||
| 
 | ||||
|     opts.httpsOptions.key = fs.readFileSync(argv.key); | ||||
|     opts.httpsOptions.cert = fs.readFileSync(argv.cert); | ||||
| 
 | ||||
|     // turn multiple-cert pemfile into array of cert strings
 | ||||
|     peerCa = argv.root.reduce(function (roots, fullpath) { | ||||
|       if (!fs.existsSync(fullpath)) { | ||||
|         return roots; | ||||
|       } | ||||
| 
 | ||||
|       return roots.concat(fs.readFileSync(fullpath, 'ascii') | ||||
|       .split('-----END CERTIFICATE-----') | ||||
|       .filter(function (ca) { | ||||
|         return ca.trim(); | ||||
|       }).map(function (ca) { | ||||
|         return (ca + '-----END CERTIFICATE-----').trim(); | ||||
|       })); | ||||
|     }, []); | ||||
| 
 | ||||
|     // TODO * `--verify /path/to/root.pem` require peers to present certificates from said authority
 | ||||
|     if (argv.verify) { | ||||
|       opts.httpsOptions.ca = peerCa; | ||||
|       opts.httpsOptions.requestCert = true; | ||||
|       opts.httpsOptions.rejectUnauthorized = true; | ||||
|     } | ||||
| 
 | ||||
|     if (argv['serve-root']) { | ||||
|       content = peerCa.join('\r\n'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   opts.cwd = process.cwd(); | ||||
|   opts.sites = []; | ||||
|   opts.sites._map = {}; | ||||
| 
 | ||||
|   if (argv.sites) { | ||||
|     opts._externalHost = false; | ||||
|     argv.sites.split(',').map(function (name) { | ||||
|       var nameparts = name.split('|'); | ||||
|       var servername = nameparts.shift(); | ||||
|       var modules; | ||||
| 
 | ||||
|       opts._externalHost = opts._externalHost || !/(^|\.)localhost\./.test(servername); | ||||
|       // TODO allow reverse proxy
 | ||||
|       if (!opts.sites._map[servername]) { | ||||
|         opts.sites._map[servername] =  { $id: servername, paths: [] }; | ||||
|         opts.sites._map[servername].paths._map = {}; | ||||
|         opts.sites.push(opts.sites._map[servername]); | ||||
|       } | ||||
| 
 | ||||
|       if (!nameparts.length) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (!opts.sites._map[servername].paths._map['/']) { | ||||
|         opts.sites._map[servername].paths._map['/'] = { $id: '/', modules: [] }; | ||||
|         opts.sites._map[servername].paths.push(opts.sites._map[servername].paths._map['/']); | ||||
|       } | ||||
| 
 | ||||
|       modules = opts.sites._map[servername].paths._map['/'].modules; | ||||
|       modules.push({ | ||||
|         $id: 'serve' | ||||
|       , paths: nameparts | ||||
|       }); | ||||
|       modules.push({ | ||||
|         $id: 'indexes' | ||||
|       , paths: nameparts | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   opts.groups = []; | ||||
| 
 | ||||
|   // 'packages', 'assets', 'com.daplie.caddy'
 | ||||
|   opts.global = { | ||||
|     modules: [ // TODO uh-oh we've got a mixed bag of modules (various types), a true map
 | ||||
|       { $id: 'greenlock', email: opts.email, tos: opts.tos } | ||||
|     , { $id: 'rvpn', email: opts.email, tos: opts.tos } | ||||
|     , { $id: 'content', content: content } | ||||
|     , { $id: 'livereload', on: opts.livereload } | ||||
|     , { $id: 'app', path: opts.expressApp } | ||||
|     ] | ||||
|   , paths: [ | ||||
|       { $id: '/assets/', modules: [ { $id: 'serve', paths: [ assetsPath ] } ] } | ||||
|       // TODO figure this b out
 | ||||
|     , { $id: '/.well-known/', modules: [ | ||||
|         { $id: 'serve', paths: [ path.join(assetsPath, 'well-known') ] } | ||||
|       ] } | ||||
|     ] | ||||
|   }; | ||||
|   opts.defaults = { | ||||
|     modules: [] | ||||
|   , paths: [ | ||||
|       { $id: '/', modules: [ | ||||
|         { $id: 'serve', paths: [ defaultWebRoot ] } | ||||
|       , { $id: 'indexes', paths: [ defaultWebRoot ] } | ||||
|       ] } | ||||
|     ] | ||||
|   }; | ||||
|   opts.sites.push({ | ||||
|     // greenlock: {}
 | ||||
|     $id: 'localhost.alpha.daplie.me' | ||||
|   , paths: [ | ||||
|       { $id: '/', modules: [ | ||||
|         { $id: 'serve', paths: [ path.resolve(__dirname, '..', 'admin', 'public') ] } | ||||
|       ] } | ||||
|     , { $id: '/api/', modules: [ | ||||
|         { $id: 'app', path: path.join(__dirname, 'admin') } | ||||
|       ] } | ||||
|     ] | ||||
|   }); | ||||
|   opts.sites.push({ | ||||
|     $id: 'localhost.daplie.invalid' | ||||
|   , paths: [ | ||||
|       { $id: '/', modules: [ { $id: 'serve', paths: [ path.resolve(__dirname, '..', 'admin', 'public') ] } ] } | ||||
|     , { $id: '/api/', modules: [ { $id: 'app', path: path.join(__dirname, 'admin') } ] } | ||||
|     ] | ||||
|   }); | ||||
| 
 | ||||
|   // ifaces
 | ||||
|   opts.ifaces = require('../lib/local-ip.js').find(); | ||||
| 
 | ||||
|   // TODO use arrays in all things
 | ||||
|   opts._old_server_name = opts.sites[0].$id; | ||||
|   opts.pubdir = defaultWebRoot.replace(/(:hostname|:servername).*/, ''); | ||||
| 
 | ||||
|   if (argv.p || argv.port || argv._[0]) { | ||||
|     opts.manualPort = true; | ||||
|   } | ||||
|   if (argv.t || argv.tunnel) { | ||||
|     opts.tunnel = true; | ||||
|   } | ||||
|   if (argv.i || argv['insecure-port']) { | ||||
|     opts.manualInsecurePort = true; | ||||
|   } | ||||
|   opts.insecurePort = parseInt(argv.i || argv['insecure-port'], 10) | ||||
|     || argv.i || argv['insecure-port'] | ||||
|     || httpPort | ||||
|     ; | ||||
|   opts.livereload = livereload; | ||||
| 
 | ||||
|   if (argv['express-app']) { | ||||
|     opts.expressApp = require(argv['express-app']); | ||||
|   } | ||||
| 
 | ||||
|   if (opts.email || opts._externalHost) { | ||||
|     if (!opts.agreeTos) { | ||||
|       console.warn("You may need to specify --agree-tos to agree to both the Let's Encrypt and Daplie DNS terms of service."); | ||||
|     } | ||||
|     if (!opts.email) { | ||||
|       // TODO store email in .ddnsrc.json
 | ||||
|       console.warn("You may need to specify --email to register with both the Let's Encrypt and Daplie DNS."); | ||||
|     } | ||||
|     p = DDNS.refreshToken({ | ||||
|       email: opts.email | ||||
|     , providerUrl: opts.provider | ||||
|     , silent: true | ||||
|     , homedir: opts.homedir | ||||
|     }, { | ||||
|       debug: false | ||||
|     , email: opts.argv.email | ||||
|     }).then(function (refreshToken) { | ||||
|       opts.refreshToken = refreshToken; | ||||
|     }); | ||||
|   if (args.config) { | ||||
|     text = fs.readFileSync(path.join(cwd, args.config), 'utf8'); | ||||
|   } | ||||
|   else { | ||||
|     p = PromiseA.resolve(); | ||||
|     filename = path.join(cwd, 'Goldilocks.yml'); | ||||
| 
 | ||||
|     if (fs.existsSync(filename)) { | ||||
|       text = fs.readFileSync(filename, 'utf8'); | ||||
|     } | ||||
| 
 | ||||
|   return p.then(function () { | ||||
| 
 | ||||
|   // can be changed to tunnel external port
 | ||||
|   opts.redirectOptions = { | ||||
|     port: opts.port | ||||
|   }; | ||||
|   opts.redirectApp = require('redirect-https')(opts.redirectOptions); | ||||
| 
 | ||||
|   return createServer(port, null, content, opts).then(function (servers) { | ||||
|     var p; | ||||
|     var httpsUrl; | ||||
|     var httpUrl; | ||||
|     var promise; | ||||
| 
 | ||||
|     // TODO show all sites
 | ||||
|     console.info(''); | ||||
|     console.info('Serving ' + opts.pubdir + ' at '); | ||||
|     console.info(''); | ||||
| 
 | ||||
|     // Port
 | ||||
|     httpsUrl = 'https://' + opts._old_server_name; | ||||
|     p = opts.port; | ||||
|     if (httpsPort !== p) { | ||||
|       httpsUrl += ':' + p; | ||||
|     } | ||||
|     console.info('\t' + httpsUrl); | ||||
| 
 | ||||
|     // Insecure Port
 | ||||
|     httpUrl = 'http://' + opts._old_server_name; | ||||
|     p = opts.insecurePort; | ||||
|     if (httpPort !== p) { | ||||
|       httpUrl += ':' + p; | ||||
|     } | ||||
|     console.info('\t' + httpUrl + ' (redirecting to https)'); | ||||
|     console.info(''); | ||||
| 
 | ||||
|     if (!(argv.sites && (defaultServername !== argv.sites) && !(argv.key && argv.cert))) { | ||||
|       // TODO what is this condition actually intending to test again?
 | ||||
|       // (I think it can be replaced with if (!opts._externalHost) { ... }
 | ||||
| 
 | ||||
|       promise = PromiseA.resolve(); | ||||
|     else { | ||||
|       filename = path.join(cwd, 'Goldilocks.json'); | ||||
|       if (fs.existsSync(filename)) { | ||||
|         text = fs.readFileSync(filename, 'utf8'); | ||||
|       } else { | ||||
|       console.info("Attempting to resolve external connection for '" + opts._old_server_name + "'"); | ||||
|         text = '{}'; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|         promise = require('../lib/match-ips.js').match(opts._old_server_name, opts); | ||||
|     config = JSON.parse(text); | ||||
|   } catch(e) { | ||||
|         console.warn("Upgrade to version 2.x to use automatic certificate issuance for '" + opts._old_server_name + "'"); | ||||
|         promise = PromiseA.resolve(); | ||||
|     try { | ||||
|       config = require('js-yaml').safeLoad(text); | ||||
|     } catch(e) { | ||||
|       throw new Error( | ||||
|         "Could not load '" + filename + "' as JSON nor YAML" | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|     return promise.then(function (matchingIps) { | ||||
|       if (matchingIps) { | ||||
|         if (!matchingIps.length) { | ||||
|           console.info("Neither the attached nor external interfaces match '" + opts._old_server_name + "'"); | ||||
|         } | ||||
|       } | ||||
|       opts.matchingIps = matchingIps || []; | ||||
| 
 | ||||
|       if (opts.matchingIps.length) { | ||||
|         console.info(''); | ||||
|         console.info('External IPs:'); | ||||
|         console.info(''); | ||||
|         opts.matchingIps.forEach(function (ip) { | ||||
|           if ('IPv4' === ip.family) { | ||||
|             httpsUrl = 'https://' + ip.address; | ||||
|             if (httpsPort !== opts.port) { | ||||
|               httpsUrl += ':' + opts.port; | ||||
|             } | ||||
|             console.info('\t' + httpsUrl); | ||||
|           } | ||||
|           else { | ||||
|             httpsUrl = 'https://[' + ip.address + ']'; | ||||
|             if (httpsPort !== opts.port) { | ||||
|               httpsUrl += ':' + opts.port; | ||||
|             } | ||||
|             console.info('\t' + httpsUrl); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|       else if (!opts.tunnel) { | ||||
|         console.info("External IP address does not match local IP address."); | ||||
|         console.info("Use --tunnel to allow the people of the Internet to access your server."); | ||||
|       } | ||||
| 
 | ||||
|       if (opts.tunnel) { | ||||
|         require('../lib/tunnel.js').create(opts, servers); | ||||
|       } | ||||
|       else if (opts.ddns) { | ||||
|         require('../lib/ddns.js').create(opts, servers); | ||||
|       } | ||||
| 
 | ||||
|       Object.keys(opts.ifaces).forEach(function (iname) { | ||||
|         var iface = opts.ifaces[iname]; | ||||
| 
 | ||||
|         if (iface.ipv4.length) { | ||||
|           console.info(''); | ||||
|           console.info(iname + ':'); | ||||
| 
 | ||||
|           httpsUrl = 'https://' + iface.ipv4[0].address; | ||||
|           if (httpsPort !== opts.port) { | ||||
|             httpsUrl += ':' + opts.port; | ||||
|           } | ||||
|           console.info('\t' + httpsUrl); | ||||
| 
 | ||||
|           if (iface.ipv6.length) { | ||||
|             httpsUrl = 'https://[' + iface.ipv6[0].address + ']'; | ||||
|             if (httpsPort !== opts.port) { | ||||
|               httpsUrl += ':' + opts.port; | ||||
|             } | ||||
|             console.info('\t' + httpsUrl); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       console.info(''); | ||||
|     }); | ||||
|   }); | ||||
|   }); | ||||
|   require('../lib/goldilocks.js').create(config); | ||||
| } | ||||
| 
 | ||||
| if (require.main === module) { | ||||
|   run(); | ||||
| if (process.argv.length === 2) { | ||||
|   readConfigAndRun({}); | ||||
| } | ||||
| else if (process.argv.length === 4) { | ||||
|   if ('-c' === process.argv[3] || '--config' === process.argv[3]) { | ||||
|     readConfigAndRun({ config: process.argv[4] }); | ||||
|   } | ||||
| } | ||||
| else if (process.argv.length > 2) { | ||||
|   var program = require('commander'); | ||||
| 
 | ||||
|   program | ||||
|     .version(require('package.json').version) | ||||
|     .option('--config', 'Path to config file (Goldilocks.json or Goldilocks.yml) example: --config /etc/goldilocks/Goldilocks.json') | ||||
|     .option('--tunnel [token]', 'Turn tunnel on. This will enter interactive mode for login if no token is specified.') | ||||
|     .parse(process.argv); | ||||
| 
 | ||||
|   readConfigAndRun(program); | ||||
| } | ||||
| else { | ||||
|   throw new Error("impossible number of arguments: " + process.argv.length); | ||||
| } | ||||
|  | ||||
							
								
								
									
										688
									
								
								lib/goldilocks.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										688
									
								
								lib/goldilocks.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,688 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| module.exports.create = function (config) { | ||||
|   //var PromiseA = global.Promise;
 | ||||
|   var PromiseA = require('bluebird'); | ||||
|   var tls = require('tls'); | ||||
|   var https = require('httpolyglot'); | ||||
|   var http = require('http'); | ||||
|   var path = require('path'); | ||||
|   var httpPort = 80; | ||||
|   var httpsPort = 443; | ||||
|   var lrPort = 35729; | ||||
|   var portFallback = 8443; | ||||
|   var insecurePortFallback = 4080; | ||||
| 
 | ||||
|   function showError(err, port) { | ||||
|     if ('EACCES' === err.code) { | ||||
|       console.error(err); | ||||
|       console.warn("You do not have permission to use '" + port + "'."); | ||||
|       console.warn("You can probably fix that by running as Administrator or root."); | ||||
|     } | ||||
|     else if ('EADDRINUSE' === err.code) { | ||||
|       console.warn("Another server is already running on '" + port + "'."); | ||||
|       console.warn("You can probably fix that by rebooting your computer (or stopping it if you know what it is)."); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function createInsecureServer(port, _delete_me_, opts) { | ||||
|     return new PromiseA(function (realResolve) { | ||||
|       var server = http.createServer(); | ||||
| 
 | ||||
|       function resolve() { | ||||
|         realResolve(server); | ||||
|       } | ||||
| 
 | ||||
|       server.on('error', function (err) { | ||||
|         if (opts.errorInsecurePort || opts.manualInsecurePort) { | ||||
|           showError(err, port); | ||||
|           process.exit(1); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         opts.errorInsecurePort = err.toString(); | ||||
| 
 | ||||
|         return createInsecureServer(insecurePortFallback, null, opts).then(resolve); | ||||
|       }); | ||||
| 
 | ||||
|       server.on('request', opts.redirectApp); | ||||
| 
 | ||||
|       server.listen(port, function () { | ||||
|         opts.insecurePort = port; | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function createServer(port, _delete_me_, content, opts) { | ||||
|     function approveDomains(params, certs, cb) { | ||||
|       // This is where you check your database and associated
 | ||||
|       // email addresses with domains and agreements and such
 | ||||
|       var domains = params.domains; | ||||
|       //var p;
 | ||||
|       console.log('approveDomains'); | ||||
|       console.log(domains); | ||||
| 
 | ||||
| 
 | ||||
|       // The domains being approved for the first time are listed in opts.domains
 | ||||
|       // Certs being renewed are listed in certs.altnames
 | ||||
|       if (certs) { | ||||
|         params.domains = certs.altnames; | ||||
|         //p = PromiseA.resolve();
 | ||||
|       } | ||||
|       else { | ||||
|         //params.email = opts.email;
 | ||||
|         if (!opts.agreeTos) { | ||||
|           console.error("You have not previously registered '" + domains + "' so you must specify --agree-tos to agree to both the Let's Encrypt and Daplie DNS terms of service."); | ||||
|           process.exit(1); | ||||
|           return; | ||||
|         } | ||||
|         params.agreeTos = opts.agreeTos; | ||||
|       } | ||||
| 
 | ||||
|       // ddns.token(params.email, domains[0])
 | ||||
|       params.email = opts.email; | ||||
|       params.refreshToken = opts.refreshToken; | ||||
|       params.challengeType = 'dns-01'; | ||||
|       params.cli = opts.argv; | ||||
| 
 | ||||
|       cb(null, { options: params, certs: certs }); | ||||
|     } | ||||
| 
 | ||||
|     return new PromiseA(function (realResolve) { | ||||
|       var app = require('../lib/app.js'); | ||||
|       var ipaddr = require('ipaddr.js'); | ||||
|       var addresses = []; | ||||
| 
 | ||||
|       Object.keys(opts.ifaces).forEach(function (ifacename) { | ||||
|         var iface = opts.ifaces[ifacename]; | ||||
|         iface.ipv4.forEach(function (ip) { | ||||
|           addresses.push(ip); | ||||
|         }); | ||||
|         iface.ipv6.forEach(function (ip) { | ||||
|           addresses.push(ip); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       addresses.sort(function (a, b) { | ||||
|         if (a.family !== b.family) { | ||||
|           return 'IPv4' === a.family ? 1 : -1; | ||||
|         } | ||||
| 
 | ||||
|         return a.address > b.address ? 1 : -1; | ||||
|       }); | ||||
| 
 | ||||
|       addresses.forEach(function (addr) { | ||||
|         addr.range = ipaddr.parse(addr.address).range(); | ||||
|       }); | ||||
| 
 | ||||
|       var Oauth3 = require('oauth3-cli'); | ||||
|       var oauth3 = Oauth3.create({ device: { hostname: opts.device } }); | ||||
|       return Oauth3.Devices.one(oauth3).then(function (device) { | ||||
|         return Oauth3.Devices.all(oauth3).then(function (devices) { | ||||
|           return { devices: devices, device: device.device || device }; | ||||
|         }); | ||||
|       }).then(function (devices) { | ||||
|         devices.device.secret = undefined; | ||||
|         console.log('devices'); | ||||
|         console.log(devices); | ||||
|         var directive = { | ||||
|           global: opts.global | ||||
|         , sites: opts.sites | ||||
|         , defaults: opts.defaults | ||||
|         , cwd: process.cwd() | ||||
|         , ifaces: opts.ifaces | ||||
|         , addresses: addresses | ||||
|         , devices: devices.devices | ||||
|         , device: devices.device | ||||
|         , net: { | ||||
|             createConnection: function (opts, cb) { | ||||
|               // opts = { host, port, data
 | ||||
|               //        , /*proprietary to tunneler*/ servername, remoteAddress, remoteFamily, remotePort
 | ||||
|               //        , secure (tls already terminated by a proxy) }
 | ||||
|               //        // http://stackoverflow.com/questions/10348906/how-to-know-if-a-request-is-http-or-https-in-node-js
 | ||||
|               // var packerStream = require('tunnel-packer').Stream;
 | ||||
|               // TODO here we will have the tls termination (or re-forward)
 | ||||
|               return require('net').createConnection(opts, cb); | ||||
|             } | ||||
|           } | ||||
|         }; | ||||
|         var server; | ||||
|         var insecureServer; | ||||
| 
 | ||||
|         function resolve() { | ||||
|           realResolve({ | ||||
|             plainServer: insecureServer | ||||
|           , server: server | ||||
|           }); | ||||
|         } | ||||
| 
 | ||||
|         // returns an instance of node-letsencrypt with additional helper methods
 | ||||
|         var webrootPath = require('os').tmpdir(); | ||||
|         var leChallengeFs = require('le-challenge-fs').create({ webrootPath: webrootPath }); | ||||
|         //var leChallengeSni = require('le-challenge-sni').create({ webrootPath: webrootPath });
 | ||||
|         var leChallengeDdns = require('le-challenge-ddns').create({ ttl: 1 }); | ||||
|         var lex = require('greenlock-express').create({ | ||||
|           // set to https://acme-v01.api.letsencrypt.org/directory in production
 | ||||
|           server: opts.debug ? 'staging' : 'https://acme-v01.api.letsencrypt.org/directory' | ||||
| 
 | ||||
|         // If you wish to replace the default plugins, you may do so here
 | ||||
|         //
 | ||||
|         , challenges: { | ||||
|             'http-01': leChallengeFs | ||||
|           , 'tls-sni-01': leChallengeFs // leChallengeSni
 | ||||
|           , 'dns-01': leChallengeDdns | ||||
|           } | ||||
|         , challengeType: (opts.tunnel ? 'http-01' : 'dns-01') | ||||
|         , store: require('le-store-certbot').create({ | ||||
|             webrootPath: webrootPath | ||||
|           , configDir: path.join((opts.homedir || '~'), 'letsencrypt', 'etc') | ||||
|           , homedir: opts.homedir | ||||
|           }) | ||||
|         , webrootPath: webrootPath | ||||
| 
 | ||||
|         // You probably wouldn't need to replace the default sni handler
 | ||||
|         // See https://git.daplie.com/Daplie/le-sni-auto if you think you do
 | ||||
|         //, sni: require('le-sni-auto').create({})
 | ||||
| 
 | ||||
|         , approveDomains: approveDomains | ||||
|         }); | ||||
| 
 | ||||
|         var secureContexts = { | ||||
|           'localhost.daplie.me': null | ||||
|         }; | ||||
|         opts.httpsOptions.SNICallback = function (sni, cb ) { | ||||
|           var tlsOptions; | ||||
|           console.log('[https] sni', sni); | ||||
| 
 | ||||
|           // Static Certs
 | ||||
|           if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) { | ||||
|             // TODO implement
 | ||||
|             if (!secureContexts[sni]) { | ||||
|               tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {}); | ||||
|             } | ||||
|             if (tlsOptions) { | ||||
|               secureContexts[sni] = tls.createSecureContext(tlsOptions); | ||||
|             } | ||||
|             cb(null, secureContexts[sni]); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           // Dynamic Certs
 | ||||
|           lex.httpsOptions.SNICallback(sni, cb); | ||||
|         }; | ||||
|         server = https.createServer(opts.httpsOptions); | ||||
| 
 | ||||
|         server.on('error', function (err) { | ||||
|           if (opts.errorPort || opts.manualPort) { | ||||
|             showError(err, port); | ||||
|             process.exit(1); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           opts.errorPort = err.toString(); | ||||
| 
 | ||||
|           return createServer(portFallback, null, content, opts).then(resolve); | ||||
|         }); | ||||
| 
 | ||||
|         server.listen(port, function () { | ||||
|           opts.port = port; | ||||
|           opts.redirectOptions.port = port; | ||||
| 
 | ||||
|           if (opts.livereload) { | ||||
|             opts.lrPort = opts.lrPort || lrPort; | ||||
|             var livereload = require('livereload'); | ||||
|             var server2 = livereload.createServer({ | ||||
|               https: opts.httpsOptions | ||||
|             , port: opts.lrPort | ||||
|             , exclusions: [ 'node_modules' ] | ||||
|             }); | ||||
| 
 | ||||
|             console.info("[livereload] watching " + opts.pubdir); | ||||
|             console.warn("WARNING: If CPU usage spikes to 100% it's because too many files are being watched"); | ||||
|             // TODO create map of directories to watch from opts.sites and iterate over it
 | ||||
|             server2.watch(opts.pubdir); | ||||
|           } | ||||
| 
 | ||||
|           // if we haven't disabled insecure port
 | ||||
|           if ('false' !== opts.insecurePort) { | ||||
|             // and both ports are the default
 | ||||
|             if ((httpsPort === opts.port && httpPort === opts.insecurePort) | ||||
|               // or other case
 | ||||
|               || (httpPort !== opts.insecurePort && opts.port !== opts.insecurePort) | ||||
|             ) { | ||||
|               return createInsecureServer(opts.insecurePort, null, opts).then(function (_server) { | ||||
|                 insecureServer = _server; | ||||
|                 resolve(); | ||||
|               }); | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           opts.insecurePort = opts.port; | ||||
|           resolve(); | ||||
|           return; | ||||
|         }); | ||||
| 
 | ||||
|         if ('function' === typeof app) { | ||||
|           app = app(directive); | ||||
|         } else if ('function' === typeof app.create) { | ||||
|           app = app.create(directive); | ||||
|         } | ||||
| 
 | ||||
|         server.on('request', function (req, res) { | ||||
|           console.log('[' + req.method + '] ' + req.url); | ||||
|           if (!req.socket.encrypted && !/\/\.well-known\/acme-challenge\//.test(req.url)) { | ||||
|             opts.redirectApp(req, res); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           if ('function' === typeof app) { | ||||
|             app(req, res); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           res.end('not ready'); | ||||
|         }); | ||||
| 
 | ||||
|         return PromiseA.resolve(app).then(function (_app) { | ||||
|           app = _app; | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   module.exports.createServer = createServer; | ||||
| 
 | ||||
|   function run() { | ||||
|     var defaultServername = 'localhost.daplie.me'; | ||||
|     var minimist = require('minimist'); | ||||
|     var argv = minimist(process.argv.slice(2)); | ||||
|     var port = parseInt(argv.p || argv.port || argv._[0], 10) || httpsPort; | ||||
|     var livereload = argv.livereload; | ||||
|     var defaultWebRoot = path.normalize(argv['default-web-root'] || argv.d || argv._[1] || '.'); | ||||
|     var assetsPath = path.join(__dirname, '..', 'packages', 'assets'); | ||||
|     var content = argv.c; | ||||
|     var letsencryptHost = argv['letsencrypt-certs']; | ||||
|     var yaml = require('js-yaml'); | ||||
|     var fs = PromiseA.promisifyAll(require('fs')); | ||||
|     var configFile = argv.c || argv.conf || argv.config; | ||||
|     var config; | ||||
|     var DDNS; | ||||
|     console.log('defaultWebRoot', defaultWebRoot); | ||||
| 
 | ||||
|     try { | ||||
|       config = fs.readFileSync(configFile || 'Goldilocks.yml'); | ||||
|     } catch(e) { | ||||
|       if (configFile) { | ||||
|         console.error('Failed to read config:', e); | ||||
|         process.exit(1); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (config) { | ||||
|       try { | ||||
|         config = yaml.safeLoad(config); | ||||
|       } catch(e) { | ||||
|         console.error('Failed to parse config:', e); | ||||
|         process.exit(1); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (argv.V || argv.version || argv.v) { | ||||
|       if (argv.v) { | ||||
|         console.warn("flag -v is reserved for future use. Use -V or --version for version information."); | ||||
|       } | ||||
|       console.info('v' + require('../package.json').version); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     argv.sites = argv.sites; | ||||
| 
 | ||||
|     // letsencrypt
 | ||||
|     var httpsOptions = require('localhost.daplie.me-certificates').merge({}); | ||||
|     var secureContext; | ||||
| 
 | ||||
|     var opts = { | ||||
|       agreeTos: argv.agreeTos || argv['agree-tos'] | ||||
|     , debug: argv.debug | ||||
|     , device: argv.device | ||||
|     , provider: (argv.provider && 'false' !== argv.provider) ? argv.provider : 'oauth3.org' | ||||
|     , email: argv.email | ||||
|     , httpsOptions: { | ||||
|         key: httpsOptions.key | ||||
|       , cert: httpsOptions.cert | ||||
|       //, ca: httpsOptions.ca
 | ||||
|       } | ||||
|     , homedir: argv.homedir | ||||
|     , argv: argv | ||||
|     }; | ||||
|     var peerCa; | ||||
|     var p; | ||||
| 
 | ||||
|     opts.PromiseA = PromiseA; | ||||
|     opts.httpsOptions.SNICallback = function (sni, cb) { | ||||
|       if (!secureContext) { | ||||
|         secureContext = tls.createSecureContext(opts.httpsOptions); | ||||
|       } | ||||
|       cb(null, secureContext); | ||||
|       return; | ||||
|     }; | ||||
| 
 | ||||
|     if (letsencryptHost) { | ||||
|       // TODO remove in v3.x (aka goldilocks)
 | ||||
|       argv.key = argv.key || '/etc/letsencrypt/live/' + letsencryptHost + '/privkey.pem'; | ||||
|       argv.cert = argv.cert || '/etc/letsencrypt/live/' + letsencryptHost + '/fullchain.pem'; | ||||
|       argv.root = argv.root || argv.chain || ''; | ||||
|       argv.sites = argv.sites || letsencryptHost; | ||||
|       argv['serve-root'] = argv['serve-root'] || argv['serve-chain']; | ||||
|       // argv[express-app]
 | ||||
|     } | ||||
| 
 | ||||
|     if (argv['serve-root'] && !argv.root) { | ||||
|       console.error("You must specify bath --root to use --serve-root"); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (argv.key || argv.cert || argv.root) { | ||||
|       if (!argv.key || !argv.cert) { | ||||
|         console.error("You must specify bath --key and --cert, and optionally --root (required with serve-root)"); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (!Array.isArray(argv.root)) { | ||||
|         argv.root = [argv.root]; | ||||
|       } | ||||
| 
 | ||||
|       opts.httpsOptions.key = fs.readFileSync(argv.key); | ||||
|       opts.httpsOptions.cert = fs.readFileSync(argv.cert); | ||||
| 
 | ||||
|       // turn multiple-cert pemfile into array of cert strings
 | ||||
|       peerCa = argv.root.reduce(function (roots, fullpath) { | ||||
|         if (!fs.existsSync(fullpath)) { | ||||
|           return roots; | ||||
|         } | ||||
| 
 | ||||
|         return roots.concat(fs.readFileSync(fullpath, 'ascii') | ||||
|         .split('-----END CERTIFICATE-----') | ||||
|         .filter(function (ca) { | ||||
|           return ca.trim(); | ||||
|         }).map(function (ca) { | ||||
|           return (ca + '-----END CERTIFICATE-----').trim(); | ||||
|         })); | ||||
|       }, []); | ||||
| 
 | ||||
|       // TODO * `--verify /path/to/root.pem` require peers to present certificates from said authority
 | ||||
|       if (argv.verify) { | ||||
|         opts.httpsOptions.ca = peerCa; | ||||
|         opts.httpsOptions.requestCert = true; | ||||
|         opts.httpsOptions.rejectUnauthorized = true; | ||||
|       } | ||||
| 
 | ||||
|       if (argv['serve-root']) { | ||||
|         content = peerCa.join('\r\n'); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     opts.cwd = process.cwd(); | ||||
|     opts.sites = []; | ||||
|     opts.sites._map = {}; | ||||
| 
 | ||||
|     if (argv.sites) { | ||||
|       opts._externalHost = false; | ||||
|       argv.sites.split(',').map(function (name) { | ||||
|         var nameparts = name.split('|'); | ||||
|         var servername = nameparts.shift(); | ||||
|         var modules; | ||||
| 
 | ||||
|         opts._externalHost = opts._externalHost || !/(^|\.)localhost\./.test(servername); | ||||
|         // TODO allow reverse proxy
 | ||||
|         if (!opts.sites._map[servername]) { | ||||
|           opts.sites._map[servername] =  { $id: servername, paths: [] }; | ||||
|           opts.sites._map[servername].paths._map = {}; | ||||
|           opts.sites.push(opts.sites._map[servername]); | ||||
|         } | ||||
| 
 | ||||
|         if (!nameparts.length) { | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         if (!opts.sites._map[servername].paths._map['/']) { | ||||
|           opts.sites._map[servername].paths._map['/'] = { $id: '/', modules: [] }; | ||||
|           opts.sites._map[servername].paths.push(opts.sites._map[servername].paths._map['/']); | ||||
|         } | ||||
| 
 | ||||
|         modules = opts.sites._map[servername].paths._map['/'].modules; | ||||
|         modules.push({ | ||||
|           $id: 'serve' | ||||
|         , paths: nameparts | ||||
|         }); | ||||
|         modules.push({ | ||||
|           $id: 'indexes' | ||||
|         , paths: nameparts | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     opts.groups = []; | ||||
| 
 | ||||
|     // 'packages', 'assets', 'com.daplie.caddy'
 | ||||
|     opts.global = { | ||||
|       modules: [ // TODO uh-oh we've got a mixed bag of modules (various types), a true map
 | ||||
|         { $id: 'greenlock', email: opts.email, tos: opts.tos } | ||||
|       , { $id: 'rvpn', email: opts.email, tos: opts.tos } | ||||
|       , { $id: 'content', content: content } | ||||
|       , { $id: 'livereload', on: opts.livereload } | ||||
|       , { $id: 'app', path: opts.expressApp } | ||||
|       ] | ||||
|     , paths: [ | ||||
|         { $id: '/assets/', modules: [ { $id: 'serve', paths: [ assetsPath ] } ] } | ||||
|         // TODO figure this b out
 | ||||
|       , { $id: '/.well-known/', modules: [ | ||||
|           { $id: 'serve', paths: [ path.join(assetsPath, 'well-known') ] } | ||||
|         ] } | ||||
|       ] | ||||
|     }; | ||||
|     opts.defaults = { | ||||
|       modules: [] | ||||
|     , paths: [ | ||||
|         { $id: '/', modules: [ | ||||
|           { $id: 'serve', paths: [ defaultWebRoot ] } | ||||
|         , { $id: 'indexes', paths: [ defaultWebRoot ] } | ||||
|         ] } | ||||
|       ] | ||||
|     }; | ||||
|     opts.sites.push({ | ||||
|       // greenlock: {}
 | ||||
|       $id: 'localhost.alpha.daplie.me' | ||||
|     , paths: [ | ||||
|         { $id: '/', modules: [ | ||||
|           { $id: 'serve', paths: [ path.resolve(__dirname, '..', 'admin', 'public') ] } | ||||
|         ] } | ||||
|       , { $id: '/api/', modules: [ | ||||
|           { $id: 'app', path: path.join(__dirname, 'admin') } | ||||
|         ] } | ||||
|       ] | ||||
|     }); | ||||
|     opts.sites.push({ | ||||
|       $id: 'localhost.daplie.invalid' | ||||
|     , paths: [ | ||||
|         { $id: '/', modules: [ { $id: 'serve', paths: [ path.resolve(__dirname, '..', 'admin', 'public') ] } ] } | ||||
|       , { $id: '/api/', modules: [ { $id: 'app', path: path.join(__dirname, 'admin') } ] } | ||||
|       ] | ||||
|     }); | ||||
| 
 | ||||
|     // ifaces
 | ||||
|     opts.ifaces = require('../lib/local-ip.js').find(); | ||||
| 
 | ||||
|     // TODO use arrays in all things
 | ||||
|     opts._old_server_name = opts.sites[0].$id; | ||||
|     opts.pubdir = defaultWebRoot.replace(/(:hostname|:servername).*/, ''); | ||||
| 
 | ||||
|     if (argv.p || argv.port || argv._[0]) { | ||||
|       opts.manualPort = true; | ||||
|     } | ||||
|     if (argv.t || argv.tunnel) { | ||||
|       opts.tunnel = true; | ||||
|     } | ||||
|     if (argv.i || argv['insecure-port']) { | ||||
|       opts.manualInsecurePort = true; | ||||
|     } | ||||
|     opts.insecurePort = parseInt(argv.i || argv['insecure-port'], 10) | ||||
|       || argv.i || argv['insecure-port'] | ||||
|       || httpPort | ||||
|       ; | ||||
|     opts.livereload = livereload; | ||||
| 
 | ||||
|     if (argv['express-app']) { | ||||
|       opts.expressApp = require(argv['express-app']); | ||||
|     } | ||||
| 
 | ||||
|     if (opts.email || opts._externalHost) { | ||||
|       if (!opts.agreeTos) { | ||||
|         console.warn("You may need to specify --agree-tos to agree to both the Let's Encrypt and Daplie DNS terms of service."); | ||||
|       } | ||||
|       if (!opts.email) { | ||||
|         // TODO store email in .ddnsrc.json
 | ||||
|         console.warn("You may need to specify --email to register with both the Let's Encrypt and Daplie DNS."); | ||||
|       } | ||||
|       DDNS = require('ddns-cli'); | ||||
|       p = DDNS.refreshToken({ | ||||
|         email: opts.email | ||||
|       , providerUrl: opts.provider | ||||
|       , silent: true | ||||
|       , homedir: opts.homedir | ||||
|       }, { | ||||
|         debug: false | ||||
|       , email: opts.argv.email | ||||
|       }).then(function (refreshToken) { | ||||
|         opts.refreshToken = refreshToken; | ||||
|       }); | ||||
|     } | ||||
|     else { | ||||
|       p = PromiseA.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     return p.then(function () { | ||||
| 
 | ||||
|     // can be changed to tunnel external port
 | ||||
|     opts.redirectOptions = { | ||||
|       port: opts.port | ||||
|     }; | ||||
|     opts.redirectApp = require('redirect-https')(opts.redirectOptions); | ||||
| 
 | ||||
|     return createServer(port, null, content, opts).then(function (servers) { | ||||
|       var p; | ||||
|       var httpsUrl; | ||||
|       var httpUrl; | ||||
|       var promise; | ||||
| 
 | ||||
|       // TODO show all sites
 | ||||
|       console.info(''); | ||||
|       console.info('Serving ' + opts.pubdir + ' at '); | ||||
|       console.info(''); | ||||
| 
 | ||||
|       // Port
 | ||||
|       httpsUrl = 'https://' + opts._old_server_name; | ||||
|       p = opts.port; | ||||
|       if (httpsPort !== p) { | ||||
|         httpsUrl += ':' + p; | ||||
|       } | ||||
|       console.info('\t' + httpsUrl); | ||||
| 
 | ||||
|       // Insecure Port
 | ||||
|       httpUrl = 'http://' + opts._old_server_name; | ||||
|       p = opts.insecurePort; | ||||
|       if (httpPort !== p) { | ||||
|         httpUrl += ':' + p; | ||||
|       } | ||||
|       console.info('\t' + httpUrl + ' (redirecting to https)'); | ||||
|       console.info(''); | ||||
| 
 | ||||
|       if (!(argv.sites && (defaultServername !== argv.sites) && !(argv.key && argv.cert))) { | ||||
|         // TODO what is this condition actually intending to test again?
 | ||||
|         // (I think it can be replaced with if (!opts._externalHost) { ... }
 | ||||
| 
 | ||||
|         promise = PromiseA.resolve(); | ||||
|       } else { | ||||
|         console.info("Attempting to resolve external connection for '" + opts._old_server_name + "'"); | ||||
|         try { | ||||
|           promise = require('../lib/match-ips.js').match(opts._old_server_name, opts); | ||||
|         } catch(e) { | ||||
|           console.warn("Upgrade to version 2.x to use automatic certificate issuance for '" + opts._old_server_name + "'"); | ||||
|           promise = PromiseA.resolve(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return promise.then(function (matchingIps) { | ||||
|         if (matchingIps) { | ||||
|           if (!matchingIps.length) { | ||||
|             console.info("Neither the attached nor external interfaces match '" + opts._old_server_name + "'"); | ||||
|           } | ||||
|         } | ||||
|         opts.matchingIps = matchingIps || []; | ||||
| 
 | ||||
|         if (opts.matchingIps.length) { | ||||
|           console.info(''); | ||||
|           console.info('External IPs:'); | ||||
|           console.info(''); | ||||
|           opts.matchingIps.forEach(function (ip) { | ||||
|             if ('IPv4' === ip.family) { | ||||
|               httpsUrl = 'https://' + ip.address; | ||||
|               if (httpsPort !== opts.port) { | ||||
|                 httpsUrl += ':' + opts.port; | ||||
|               } | ||||
|               console.info('\t' + httpsUrl); | ||||
|             } | ||||
|             else { | ||||
|               httpsUrl = 'https://[' + ip.address + ']'; | ||||
|               if (httpsPort !== opts.port) { | ||||
|                 httpsUrl += ':' + opts.port; | ||||
|               } | ||||
|               console.info('\t' + httpsUrl); | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|         else if (!opts.tunnel) { | ||||
|           console.info("External IP address does not match local IP address."); | ||||
|           console.info("Use --tunnel to allow the people of the Internet to access your server."); | ||||
|         } | ||||
| 
 | ||||
|         if (opts.tunnel) { | ||||
|           require('../lib/tunnel.js').create(opts, servers); | ||||
|         } | ||||
|         else if (opts.ddns) { | ||||
|           require('../lib/ddns.js').create(opts, servers); | ||||
|         } | ||||
| 
 | ||||
|         Object.keys(opts.ifaces).forEach(function (iname) { | ||||
|           var iface = opts.ifaces[iname]; | ||||
| 
 | ||||
|           if (iface.ipv4.length) { | ||||
|             console.info(''); | ||||
|             console.info(iname + ':'); | ||||
| 
 | ||||
|             httpsUrl = 'https://' + iface.ipv4[0].address; | ||||
|             if (httpsPort !== opts.port) { | ||||
|               httpsUrl += ':' + opts.port; | ||||
|             } | ||||
|             console.info('\t' + httpsUrl); | ||||
| 
 | ||||
|             if (iface.ipv6.length) { | ||||
|               httpsUrl = 'https://[' + iface.ipv6[0].address + ']'; | ||||
|               if (httpsPort !== opts.port) { | ||||
|                 httpsUrl += ':' + opts.port; | ||||
|               } | ||||
|               console.info('\t' + httpsUrl); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|         console.info(''); | ||||
|       }); | ||||
|     }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   run(); | ||||
| }; | ||||
| @ -40,6 +40,7 @@ | ||||
|   "dependencies": { | ||||
|     "bluebird": "^3.4.6", | ||||
|     "body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1", | ||||
|     "commander": "^2.9.0", | ||||
|     "daplie-tunnel": "git+https://git.daplie.com/Daplie/daplie-cli-tunnel.git#master", | ||||
|     "ddns-cli": "git+https://git.daplie.com/Daplie/node-ddns-client.git#master", | ||||
|     "express": "git+https://github.com/expressjs/express.git#4.x", | ||||
| @ -49,7 +50,7 @@ | ||||
|     "httpolyglot": "^0.1.1", | ||||
|     "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", | ||||
|     "ipify": "^1.1.0", | ||||
|     "js-yaml": "^3.8.1", | ||||
|     "js-yaml": "^3.8.3", | ||||
|     "le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master", | ||||
|     "le-challenge-fs": "git+https://git.daplie.com/Daplie/le-challenge-webroot.git#master", | ||||
|     "le-challenge-sni": "^2.0.1", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user