393 lines
12 KiB
JavaScript
393 lines
12 KiB
JavaScript
|
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();
|