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();