goldilocks.js/serve.js

368 lines
10 KiB
JavaScript
Raw Normal View History

2015-06-24 21:45:54 +00:00
#!/usr/bin/env node
2015-06-24 21:36:17 +00:00
'use strict';
2016-10-07 04:40:16 +00:00
//var PromiseA = global.Promise;
var PromiseA = require('bluebird');
2016-10-11 23:20:37 +00:00
var tls = require('tls');
2016-10-06 23:09:21 +00:00
var https = require('httpolyglot');
var http = require('http');
var fs = require('fs');
2015-06-24 21:36:17 +00:00
var path = require('path');
2016-10-06 23:09:21 +00:00
var httpPort = 80;
var httpsPort = 443;
var lrPort = 35729;
2016-09-13 23:08:08 +00:00
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 comupter (or stopping it if you know what it is).");
}
}
2015-06-24 21:36:17 +00:00
function createInsecureServer(port, pubdir, opts) {
2016-10-11 16:58:18 +00:00
return new PromiseA(function (realResolve) {
2016-09-13 23:08:08 +00:00
var server = http.createServer();
2016-10-11 16:58:18 +00:00
function resolve() {
realResolve(server);
}
2016-09-13 23:08:08 +00:00
server.on('error', function (err) {
if (opts.errorInsecurePort || opts.manualInsecurePort) {
showError(err, port);
process.exit(1);
return;
}
2016-09-13 23:08:08 +00:00
opts.errorInsecurePort = err.toString();
2016-09-13 23:08:08 +00:00
return createInsecureServer(insecurePortFallback, pubdir, opts).then(resolve);
});
2016-10-11 16:58:18 +00:00
server.on('request', opts.redirectApp);
2016-09-13 23:08:08 +00:00
server.listen(port, function () {
opts.insecurePort = port;
resolve();
});
});
}
function createServer(port, pubdir, content, opts) {
2016-10-11 16:58:18 +00:00
return new PromiseA(function (realResolve) {
2016-10-07 19:26:53 +00:00
var server = https.createServer(opts.httpsOptions);
2016-09-13 23:08:08 +00:00
var app = require('./app');
var directive = { public: pubdir, content: content, livereload: opts.livereload
, servername: opts.servername, expressApp: opts.expressApp };
2016-10-11 16:58:18 +00:00
var insecureServer;
function resolve() {
realResolve({
plainServer: insecureServer
, server: server
});
}
2016-09-13 23:08:08 +00:00
server.on('error', function (err) {
if (opts.errorPort || opts.manualPort) {
showError(err, port);
process.exit(1);
return;
}
2015-12-06 06:43:33 +00:00
2016-09-13 23:08:08 +00:00
opts.errorPort = err.toString();
2015-12-06 06:43:33 +00:00
2016-09-13 23:08:08 +00:00
return createServer(portFallback, pubdir, content, opts).then(resolve);
});
2015-06-24 21:36:17 +00:00
2016-09-13 23:08:08 +00:00
server.listen(port, function () {
opts.port = port;
2016-10-11 16:58:18 +00:00
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: [ '.hg', '.git', '.svn', 'node_modules' ]
});
console.info("[livereload] watching " + pubdir);
console.warn("WARNING: If CPU usage spikes to 100% it's because too many files are being watched");
server2.watch(pubdir);
}
2016-09-09 05:10:04 +00:00
2016-10-06 23:09:21 +00:00
if ('false' !== opts.insecurePort && httpPort !== opts.insecurePort) {
2016-10-11 16:58:18 +00:00
return createInsecureServer(opts.insecurePort, pubdir, opts).then(function (_server) {
insecureServer = _server;
resolve();
});
2016-09-13 23:08:08 +00:00
} else {
2016-10-07 04:28:05 +00:00
opts.insecurePort = opts.port;
2016-09-13 23:08:08 +00:00
resolve();
}
});
2016-09-09 05:10:04 +00:00
2016-09-13 23:08:08 +00:00
if ('function' === typeof app) {
app = app(directive);
} else if ('function' === typeof app.create) {
app = app.create(directive);
}
2016-09-09 05:10:04 +00:00
2016-09-13 23:08:08 +00:00
server.on('request', function (req, res) {
2016-10-06 23:09:21 +00:00
if (!req.socket.encrypted) {
2016-10-11 16:58:18 +00:00
opts.redirectApp(req, res);
2016-10-06 23:09:21 +00:00
return;
}
2016-09-13 23:08:08 +00:00
if ('function' === typeof app) {
app(req, res);
return;
2016-09-09 05:10:04 +00:00
}
2015-06-24 21:36:17 +00:00
2016-09-13 23:08:08 +00:00
res.end('not ready');
});
2015-06-24 21:36:17 +00:00
2016-09-13 23:08:08 +00:00
return PromiseA.resolve(app).then(function (_app) {
app = _app;
});
2015-06-24 21:36:17 +00:00
});
}
module.exports.createServer = createServer;
function run() {
2016-09-13 23:08:08 +00:00
var defaultServername = 'localhost.daplie.com';
2015-06-24 21:36:17 +00:00
var minimist = require('minimist');
var argv = minimist(process.argv.slice(2));
2016-10-06 23:09:21 +00:00
var port = parseInt(argv.p || argv.port || argv._[0], 10) || httpsPort;
2015-12-06 06:43:33 +00:00
var livereload = argv.livereload;
2015-06-24 21:36:17 +00:00
var pubdir = path.resolve(argv.d || argv._[1] || process.cwd());
2015-06-30 23:11:01 +00:00
var content = argv.c;
var letsencryptHost = argv['letsencrypt-certs'];
2016-10-07 21:28:46 +00:00
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;
}
2016-09-13 23:08:08 +00:00
// letsencrypt
2016-10-07 19:26:53 +00:00
var httpsOptions = require('localhost.daplie.com-certificates').merge({});
2016-10-07 18:00:21 +00:00
var secureContext;
2016-09-13 23:08:08 +00:00
2016-10-07 17:36:28 +00:00
var opts = {
agreeTos: argv.agreeTos || argv['agree-tos']
, debug: argv.debug
, email: argv.email
, httpsOptions: {
2016-10-07 19:26:53 +00:00
key: httpsOptions.key
, cert: httpsOptions.cert
//, ca: httpsOptions.ca
2016-10-07 19:29:50 +00:00
}
2016-10-07 17:36:28 +00:00
, argv: argv
};
2015-07-13 23:46:44 +00:00
var peerCa;
2016-10-07 17:36:28 +00:00
opts.httpsOptions.SNICallback = function (servername, cb) {
2016-10-07 18:00:21 +00:00
if (!secureContext) {
secureContext = tls.createSecureContext(opts.httpsOptions);
}
cb(null, secureContext);
2016-09-13 23:08:08 +00:00
return;
};
if (letsencryptHost) {
argv.key = argv.key || '/etc/letsencrypt/live/' + letsencryptHost + '/privkey.pem';
2015-07-13 23:46:44 +00:00
argv.cert = argv.cert || '/etc/letsencrypt/live/' + letsencryptHost + '/fullchain.pem';
2015-12-11 05:30:54 +00:00
argv.root = argv.root || argv.chain || '';
argv.servername = argv.servername || letsencryptHost;
2015-07-13 23:46:44 +00:00
argv['serve-root'] = argv['serve-root'] || argv['serve-chain'];
2016-08-06 18:34:15 +00:00
// argv[express-app]
}
2015-07-13 23:46:44 +00:00
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;
}
2015-07-13 23:46:44 +00:00
if (!Array.isArray(argv.root)) {
argv.root = [argv.root];
}
2016-10-07 17:36:28 +00:00
opts.httpsOptions.key = fs.readFileSync(argv.key);
opts.httpsOptions.cert = fs.readFileSync(argv.cert);
2015-07-13 23:46:44 +00:00
// turn multiple-cert pemfile into array of cert strings
2015-07-13 23:46:44 +00:00
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();
}));
}, []);
2015-07-13 23:46:44 +00:00
// TODO * `--verify /path/to/root.pem` require peers to present certificates from said authority
if (argv.verify) {
2016-10-07 17:36:28 +00:00
opts.httpsOptions.ca = peerCa;
opts.httpsOptions.requestCert = true;
opts.httpsOptions.rejectUnauthorized = true;
2015-07-13 23:46:44 +00:00
}
2015-12-11 05:30:54 +00:00
if (argv['serve-root']) {
content = peerCa.join('\r\n');
}
}
2016-09-13 23:08:08 +00:00
opts.servername = defaultServername;
if (argv.servername) {
opts.servername = argv.servername;
}
2016-09-13 23:08:08 +00:00
if (argv.p || argv.port || argv._[0]) {
opts.manualPort = true;
}
if (argv.i || argv['insecure-port']) {
opts.manualInsecurePort = true;
}
2016-10-06 23:09:21 +00:00
opts.insecurePort = parseInt(argv.i || argv['insecure-port'], 10)
|| argv.i || argv['insecure-port']
|| httpPort
;
2015-12-06 06:43:33 +00:00
opts.livereload = livereload;
2016-08-06 18:34:15 +00:00
if (argv['express-app']) {
opts.expressApp = require(path.resolve(process.cwd(), argv['express-app']));
}
2016-10-11 16:58:18 +00:00
// can be changed to tunnel external port
opts.redirectOptions = {
port: opts.port
};
opts.redirectApp = require('redirect-https')(opts.redirectOptions);
2016-09-13 23:08:08 +00:00
return createServer(port, pubdir, content, opts).then(function () {
var msg;
var p;
var httpsUrl;
var promise;
// Port
msg = 'Serving ' + pubdir + ' at ';
httpsUrl = 'https://' + opts.servername;
p = opts.port;
2016-10-06 23:09:21 +00:00
if (httpsPort !== p) {
2016-09-13 23:08:08 +00:00
httpsUrl += ':' + p;
}
console.info('');
console.info(msg);
console.info('');
console.info('\t' + httpsUrl);
// Insecure Port
p = '';
2016-10-06 23:09:21 +00:00
if (httpPort !== p) {
2016-09-13 23:08:08 +00:00
p = ':' + opts.insecurePort;
}
msg = '\thttp://' + opts.servername + p + ' (redirecting to https)';
console.info(msg);
console.info('');
if (!(argv.servername && defaultServername !== argv.servername && !(argv.key && argv.cert))) {
// ifaces
opts.ifaces = require('./local-ip.js').find();
promise = PromiseA.resolve();
} else {
console.info("Attempting to resolve external connection for '" + argv.servername + "'");
try {
promise = require('./match-ips.js').match(argv.servername, opts);
} catch(e) {
console.warn("Upgrade to version 2.x to use automatic certificate issuance for '" + argv.servername + "'");
promise = PromiseA.resolve();
}
}
return promise.then(function (matchingIps) {
if (matchingIps) {
if (!matchingIps.length) {
2016-10-07 17:23:43 +00:00
console.info("Neither the attached nor external interfaces match '" + argv.servername + "'");
2016-09-13 23:08:08 +00:00
}
}
2016-10-07 04:28:05 +00:00
opts.matchingIps = matchingIps || [];
2016-09-13 23:08:08 +00:00
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;
2016-10-06 23:09:21 +00:00
if (httpsPort !== opts.port) {
2016-09-13 23:08:08 +00:00
httpsUrl += ':' + opts.port;
}
console.info('\t' + httpsUrl);
}
else {
httpsUrl = 'https://[' + ip.address + ']';
2016-10-06 23:09:21 +00:00
if (httpsPort !== opts.port) {
2016-09-13 23:08:08 +00:00
httpsUrl += ':' + opts.port;
}
console.info('\t' + httpsUrl);
}
});
}
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;
2016-10-06 23:09:21 +00:00
if (httpsPort !== opts.port) {
2016-09-13 23:08:08 +00:00
httpsUrl += ':' + opts.port;
}
console.info('\t' + httpsUrl);
if (iface.ipv6.length) {
2016-10-07 04:40:16 +00:00
httpsUrl = 'https://[' + iface.ipv6[0].address + ']';
if (httpsPort !== opts.port) {
httpsUrl += ':' + opts.port;
}
2016-09-13 23:08:08 +00:00
console.info('\t' + httpsUrl);
}
}
});
console.info('');
});
});
2015-06-24 21:36:17 +00:00
}
if (require.main === module) {
run();
}