#!/usr/bin/env node 'use strict'; var cluster = require('cluster'); if (!cluster.isMaster) { require('../lib/worker.js'); return; } function run(config) { // TODO spin up multiple workers // TODO use greenlock-cluster function work() { var worker = cluster.fork(); worker.on('exit', work).on('online', function () { console.log('[worker]', worker.id, 'online'); // Worker is listening worker.send(config); }); } console.log('config.tcp.bind', config.tcp.bind); work(); } function readConfigAndRun(args) { var fs = require('fs'); var path = require('path'); var cwd = args.cwd || process.cwd(); var text; var filename; var config; if (args.config) { filename = path.resolve(cwd, args.config); text = fs.readFileSync(filename, 'utf8'); } else { filename = path.resolve(cwd, 'goldilocks.yml'); if (fs.existsSync(filename)) { text = fs.readFileSync(filename, 'utf8'); } else { filename = path.resolve(cwd, 'goldilocks.json'); if (fs.existsSync(filename)) { text = fs.readFileSync(filename, 'utf8'); } else { text = '{}'; } } } try { config = JSON.parse(text); } catch(e) { try { config = require('js-yaml').safeLoad(text); // blank config file if ('undefined' === typeof config) { config = {}; } } catch(e) { throw new Error( "Could not load '" + filename + "' as JSON nor YAML" ); } } var recase = require('recase').create({}); config = recase.camelCopy(config); config.debug = config.debug || args.debug; if (!config.dns) { config.dns = { modules: [{ name: 'proxy', port: 3053 }] }; } // Use Object.assign to add any properties needed but not defined in the mdns config. // It will first copy the defaults into an empty object, then copy any real config over that. var mdnsDefaults = { port: 5353, broadcast: '224.0.0.251', ttl: 300 }; config.mdns = Object.assign({}, mdnsDefaults, config.mdns || {}); if (!config.tcp) { config.tcp = {}; } if (!config.http) { config.http = { modules: [{ name: 'proxy', domains: ['*'], port: 3000 }] }; } if (!config.tls) { config.tls = {}; } if (!config.tls.acme && (args.email || args.agreeTos)) { config.tls.acme = {}; } if (typeof args.agreeTos === 'string') { config.tls.acme.approvedDomains = args.agreeTos.split(','); } if (args.email) { config.email = args.email; config.tls.acme.email = args.email; } // maybe this should not go in config... but be ephemeral in some way? if (args.cwd) { config.cwd = args.cwd; } if (!config.cwd) { config.cwd = process.cwd(); } var ipaddr = require('ipaddr.js'); var addresses = []; var ifaces = require('../lib/local-ip.js').find(); Object.keys(ifaces).forEach(function (ifacename) { var iface = 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(); }); // TODO maybe move to config.state.addresses (?) config.addresses = addresses; config.device = { hostname: 'daplien-pod' }; var PromiseA = require('bluebird'); var tcpProm, dnsProm; if (config.tcp.bind) { tcpProm = PromiseA.resolve(); } else { tcpProm = new PromiseA(function (resolve, reject) { require('../lib/check-ports').checkTcpPorts(function (failed, bound) { config.tcp.bind = Object.keys(bound); if (config.tcp.bind.length) { resolve(); } else { reject(failed); } }); }); } if (config.dns.bind) { dnsProm = PromiseA.resolve(); } else { dnsProm = new PromiseA(function (resolve) { require('../lib/check-ports').checkUdpPorts(function (failed, bound) { var ports = Object.keys(bound); if (ports.length === 0) { // I don't think we want to prevent the rest of the app from running in // this case like we do for TCP, so don't call reject. console.warn('could not bind to the desired ports for DNS'); Object.keys(failed).forEach(function (key) { console.log('[error bind]', key, failed[key].code); }); } else if (ports.length === 1) { config.dns.bind = parseInt(ports[0], 10); } else { config.dns.bind = ports.map(function (numStr) { return parseInt(numStr, 10); }); } resolve(); }); }); } PromiseA.all([tcpProm, dnsProm]) .then(function () { run(config); }) .catch(function (failed) { console.warn("could not bind to the desired ports"); Object.keys(failed).forEach(function (key) { console.log('[error bind]', key, failed[key].code); }); }); } function readEnv(args) { // TODO try { if (process.env.GOLDILOCKS_HOME) { process.chdir(process.env.GOLDILOCKS_HOME); } } catch (err) {} var env = { tunnel: process.env.GOLDILOCKS_TUNNEL_TOKEN || process.env.GOLDILOCKS_TUNNEL && true , email: process.env.GOLDILOCKS_EMAIL , cwd: process.env.GOLDILOCKS_HOME || process.cwd() , debug: process.env.GOLDILOCKS_DEBUG && true }; readConfigAndRun(Object.assign({}, env, args)); } var program = require('commander'); program .version(require('../package.json').version) .option('--agree-tos [url1,url2]', "agree to all Terms of Service for Daplie, Let's Encrypt, etc (or specific URLs only)") .option('-c --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.') .option('--email ', "(Re)set default email to use for Daplie, Let's Encrypt, ACME, etc.") .option('--debug', "Enable debug output") .parse(process.argv); readEnv(program);