goldilocks.js/bin/goldilocks.js

231 lines
6.2 KiB
JavaScript
Executable File

#!/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' };
config.tunnel = args.tunnel || config.tunnel;
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 <file>', '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 <email>', "(Re)set default email to use for Daplie, Let's Encrypt, ACME, etc.")
.option('--debug', "Enable debug output")
.parse(process.argv);
readEnv(program);