WIP merging walnut, serve-https, and stunnel.js
This commit is contained in:
parent
4267955286
commit
67aa28aece
|
@ -1,10 +1,32 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
'use strict';
|
'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.ports', config.tcp.ports);
|
||||||
|
work();
|
||||||
|
}
|
||||||
|
|
||||||
function readConfigAndRun(args) {
|
function readConfigAndRun(args) {
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var cwd = args.cwd || process.cwd();
|
var cwd = args.cwd;
|
||||||
var text;
|
var text;
|
||||||
var filename;
|
var filename;
|
||||||
var config;
|
var config;
|
||||||
|
@ -13,13 +35,13 @@ function readConfigAndRun(args) {
|
||||||
text = fs.readFileSync(path.join(cwd, args.config), 'utf8');
|
text = fs.readFileSync(path.join(cwd, args.config), 'utf8');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
filename = path.join(cwd, 'Goldilocks.yml');
|
filename = path.join(cwd, 'goldilocks.yml');
|
||||||
|
|
||||||
if (fs.existsSync(filename)) {
|
if (fs.existsSync(filename)) {
|
||||||
text = fs.readFileSync(filename, 'utf8');
|
text = fs.readFileSync(filename, 'utf8');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
filename = path.join(cwd, 'Goldilocks.json');
|
filename = path.join(cwd, 'goldilocks.json');
|
||||||
if (fs.existsSync(filename)) {
|
if (fs.existsSync(filename)) {
|
||||||
text = fs.readFileSync(filename, 'utf8');
|
text = fs.readFileSync(filename, 'utf8');
|
||||||
} else {
|
} else {
|
||||||
|
@ -39,16 +61,75 @@ function readConfigAndRun(args) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!config.tcp) {
|
||||||
|
config.tcp = {};
|
||||||
|
}
|
||||||
|
if (!config.http) {
|
||||||
|
config.http = {};
|
||||||
|
}
|
||||||
|
if (!config.tls) {
|
||||||
|
config.tls = {
|
||||||
|
agreeTos: args.agreeTos || args.agree || args['agree-tos']
|
||||||
|
, servernames: (args.servernames||'').split(',').filter(Boolean).map(function (str) { return str.toLowerCase(); })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (args.email) {
|
||||||
|
config.email = args.email;
|
||||||
|
config.tls.email = args.email;
|
||||||
|
}
|
||||||
|
|
||||||
require('../lib/goldilocks.js').create(config);
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.tcp.ports) {
|
||||||
|
run(config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require('../lib/check-ports.js').checkPorts(config, function (failed, bound) {
|
||||||
|
config.tcp.ports = Object.keys(bound);
|
||||||
|
|
||||||
|
if (!config.tcp.ports.length) {
|
||||||
|
console.warn("could not bind to the desired ports");
|
||||||
|
Object.keys(failed).forEach(function (key) {
|
||||||
|
console.log('[error bind]', key, failed[key].code);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
run(config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEnv(args) {
|
||||||
|
// TODO
|
||||||
|
var env = {
|
||||||
|
tunnel: process.env.GOLDILOCKS_TUNNEL_TOKEN || process.env.GOLDILOCKS_TUNNEL && true
|
||||||
|
, email: process.env.GOLDILOCKS_EMAIL
|
||||||
|
, cwd: process.env.GOLDILOCKS_HOME
|
||||||
|
, debug: process.env.GOLDILOCKS_DEBUG && true
|
||||||
|
};
|
||||||
|
args.cwd = args.cwd || env.cwd;
|
||||||
|
Object.keys(env).forEach(function (key) {
|
||||||
|
if ('undefined' === typeof args[key]) {
|
||||||
|
args[key] = env[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
readConfigAndRun(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.argv.length === 2) {
|
if (process.argv.length === 2) {
|
||||||
readConfigAndRun({});
|
readEnv({ cwd: process.cwd() });
|
||||||
}
|
}
|
||||||
else if (process.argv.length === 4) {
|
else if (process.argv.length === 4) {
|
||||||
if ('-c' === process.argv[3] || '--config' === process.argv[3]) {
|
if ('-c' === process.argv[3] || '--config' === process.argv[3]) {
|
||||||
readConfigAndRun({ config: process.argv[4] });
|
readEnv({ config: process.argv[4] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (process.argv.length > 2) {
|
else if (process.argv.length > 2) {
|
||||||
|
@ -56,11 +137,15 @@ else if (process.argv.length > 2) {
|
||||||
|
|
||||||
program
|
program
|
||||||
.version(require('package.json').version)
|
.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('--config', 'Path to config file (Goldilocks.json or Goldilocks.yml) example: --config /etc/goldilocks/Goldilocks.json')
|
.option('--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('--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);
|
.parse(process.argv);
|
||||||
|
|
||||||
readConfigAndRun(program);
|
program.cwd = process.cwd();
|
||||||
|
readEnv(program);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error("impossible number of arguments: " + process.argv.length);
|
throw new Error("impossible number of arguments: " + process.argv.length);
|
||||||
|
|
38
lib/app.js
38
lib/app.js
|
@ -1,6 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = function (opts) {
|
module.exports = function (deps, conf) {
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
//var finalhandler = require('finalhandler');
|
//var finalhandler = require('finalhandler');
|
||||||
var serveStatic = require('serve-static');
|
var serveStatic = require('serve-static');
|
||||||
|
@ -11,7 +11,7 @@ module.exports = function (opts) {
|
||||||
|
|
||||||
var serveStaticMap = {};
|
var serveStaticMap = {};
|
||||||
var serveIndexMap = {};
|
var serveIndexMap = {};
|
||||||
var content = opts.content;
|
var content = conf.content;
|
||||||
//var server;
|
//var server;
|
||||||
var serveInit;
|
var serveInit;
|
||||||
var app;
|
var app;
|
||||||
|
@ -106,7 +106,7 @@ module.exports = function (opts) {
|
||||||
}
|
}
|
||||||
, recase: require('recase').create({})
|
, recase: require('recase').create({})
|
||||||
, request: request
|
, request: request
|
||||||
, options: opts
|
, options: conf
|
||||||
, api: {
|
, api: {
|
||||||
// TODO move loopback to oauth3.api('tunnel:loopback')
|
// TODO move loopback to oauth3.api('tunnel:loopback')
|
||||||
loopback: function (deps, session, opts2) {
|
loopback: function (deps, session, opts2) {
|
||||||
|
@ -184,7 +184,7 @@ module.exports = function (opts) {
|
||||||
, stunneld: result.tunnelUrl
|
, stunneld: result.tunnelUrl
|
||||||
// we'll provide faux networking and pipe as we please
|
// we'll provide faux networking and pipe as we please
|
||||||
, services: { https: { '*': 443 }, http: { '*': 80 }, smtp: { '*': 25}, smtps: { '*': 587 /*also 465/starttls*/ } /*, ssh: { '*': 22 }*/ }
|
, services: { https: { '*': 443 }, http: { '*': 80 }, smtp: { '*': 25}, smtps: { '*': 587 /*also 465/starttls*/ } /*, ssh: { '*': 22 }*/ }
|
||||||
, net: opts.net
|
, net: conf.net
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tun) {
|
if (tun) {
|
||||||
|
@ -199,7 +199,7 @@ module.exports = function (opts) {
|
||||||
|
|
||||||
if (!tun) {
|
if (!tun) {
|
||||||
tun = stunnel.connect(opts3);
|
tun = stunnel.connect(opts3);
|
||||||
opts.tun = true;
|
conf.tun = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
/*
|
/*
|
||||||
|
@ -214,14 +214,10 @@ module.exports = function (opts) {
|
||||||
|
|
||||||
app = express();
|
app = express();
|
||||||
|
|
||||||
if (!opts.sites) {
|
var Sites = {
|
||||||
opts.sites = [];
|
add: function (sitesMap, site) {
|
||||||
}
|
if (!sitesMap[site.$id]) {
|
||||||
opts.sites._map = {};
|
sitesMap[site.$id] = site;
|
||||||
opts.sites.forEach(function (site) {
|
|
||||||
|
|
||||||
if (!opts.sites._map[site.$id]) {
|
|
||||||
opts.sites._map[site.$id] = site;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!site.paths) {
|
if (!site.paths) {
|
||||||
|
@ -245,6 +241,17 @@ module.exports = function (opts) {
|
||||||
path.modules._map[module.$id] = module;
|
path.modules._map[module.$id] = module;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var opts = conf.http;
|
||||||
|
if (!opts.sites) {
|
||||||
|
opts.sites = [];
|
||||||
|
}
|
||||||
|
opts.sites._map = {};
|
||||||
|
opts.sites.forEach(function (site) {
|
||||||
|
|
||||||
|
Sites.add(opts.sites._map, site);
|
||||||
});
|
});
|
||||||
|
|
||||||
function mapMap(el, i, arr) {
|
function mapMap(el, i, arr) {
|
||||||
|
@ -277,6 +284,7 @@ module.exports = function (opts) {
|
||||||
path.modules._map = {};
|
path.modules._map = {};
|
||||||
path.modules.forEach(mapMap);
|
path.modules.forEach(mapMap);
|
||||||
});
|
});
|
||||||
|
|
||||||
return app.use('/', function (req, res, next) {
|
return app.use('/', function (req, res, next) {
|
||||||
if (!req.headers.host) {
|
if (!req.headers.host) {
|
||||||
next(new Error('missing HTTP Host header'));
|
next(new Error('missing HTTP Host header'));
|
||||||
|
@ -331,7 +339,7 @@ module.exports = function (opts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[serve]', req.url, hostname, pathname, dirname);
|
console.log('[serve]', req.url, hostname, pathname, dirname);
|
||||||
dirname = path.resolve(opts.cwd, dirname.replace(/:hostname/, hostname));
|
dirname = path.resolve(conf.cwd, dirname.replace(/:hostname/, hostname));
|
||||||
if (!serveStaticMap[dirname]) {
|
if (!serveStaticMap[dirname]) {
|
||||||
serveStaticMap[dirname] = serveStatic(dirname);
|
serveStaticMap[dirname] = serveStatic(dirname);
|
||||||
}
|
}
|
||||||
|
@ -355,7 +363,7 @@ module.exports = function (opts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[indexes]', req.url, hostname, pathname, dirname);
|
console.log('[indexes]', req.url, hostname, pathname, dirname);
|
||||||
dirname = path.resolve(opts.cwd, dirname.replace(/:hostname/, hostname));
|
dirname = path.resolve(conf.cwd, dirname.replace(/:hostname/, hostname));
|
||||||
if (!serveStaticMap[dirname]) {
|
if (!serveStaticMap[dirname]) {
|
||||||
serveIndexMap[dirname] = serveIndex(dirname);
|
serveIndexMap[dirname] = serveIndex(dirname);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function bindTcpAndRelease(port, cb) {
|
||||||
|
var server = require('net').createServer();
|
||||||
|
server.on('error', function (e) {
|
||||||
|
cb(e);
|
||||||
|
});
|
||||||
|
server.listen(port, function () {
|
||||||
|
server.close();
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPorts(config, cb) {
|
||||||
|
var bound = {};
|
||||||
|
var failed = {};
|
||||||
|
|
||||||
|
bindTcpAndRelease(80, function (e) {
|
||||||
|
if (e) {
|
||||||
|
failed[80] = e;
|
||||||
|
//console.log(e.code);
|
||||||
|
//console.log(e.message);
|
||||||
|
} else {
|
||||||
|
bound['80'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindTcpAndRelease(443, function (e) {
|
||||||
|
if (e) {
|
||||||
|
failed[443] = e;
|
||||||
|
} else {
|
||||||
|
bound['443'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bound['80'] && bound['443']) {
|
||||||
|
//config.tcp.ports = [ 80, 443 ];
|
||||||
|
cb(null, bound);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("default ports 80 and 443 are not available, trying 8443");
|
||||||
|
|
||||||
|
bindTcpAndRelease(8443, function (e) {
|
||||||
|
if (e) {
|
||||||
|
failed[8443] = e;
|
||||||
|
} else {
|
||||||
|
bound['8443'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(failed, bound);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.checkPorts = checkPorts;
|
|
@ -1,199 +1,274 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports.create = function (config) {
|
module.exports.create = function (deps, config) {
|
||||||
|
console.log('config', config);
|
||||||
|
|
||||||
//var PromiseA = global.Promise;
|
//var PromiseA = global.Promise;
|
||||||
var PromiseA = require('bluebird');
|
var PromiseA = require('bluebird');
|
||||||
|
var greenlock = require('greenlock');
|
||||||
|
var listeners = require('./servers').listeners;
|
||||||
|
var parseSni = require('sni');
|
||||||
|
var modules = { };
|
||||||
|
var program = {
|
||||||
|
tlsOptions: require('localhost.daplie.me-certificates').merge({})
|
||||||
|
, acmeDirectoryUrl: 'https://acme-v01.api.letsencrypt.org/directory'
|
||||||
|
, challengeType: 'tls-sni-01'
|
||||||
|
};
|
||||||
|
var secureContexts = {};
|
||||||
|
var tunnelAdminTlsOpts = {};
|
||||||
var tls = require('tls');
|
var tls = require('tls');
|
||||||
var https = require('httpolyglot');
|
|
||||||
var http = require('http');
|
|
||||||
var path = require('path');
|
|
||||||
var httpPort = 80;
|
|
||||||
var httpsPort = 443;
|
|
||||||
var lrPort = 35729;
|
|
||||||
var portFallback = 8443;
|
|
||||||
var insecurePortFallback = 4080;
|
|
||||||
|
|
||||||
function showError(err, port) {
|
var tcpRouter = {
|
||||||
if ('EACCES' === err.code) {
|
_map: { }
|
||||||
console.error(err);
|
, _create: function (address, port) {
|
||||||
console.warn("You do not have permission to use '" + port + "'.");
|
// port provides hinting for http, smtp, etc
|
||||||
console.warn("You can probably fix that by running as Administrator or root.");
|
return function (conn, firstChunk) {
|
||||||
}
|
console.log('[tcpRouter] ' + address + ':' + port + ' servername');
|
||||||
else if ('EADDRINUSE' === err.code) {
|
|
||||||
console.warn("Another server is already running on '" + port + "'.");
|
// At this point we cannot necessarily trace which port or address the socket came from
|
||||||
console.warn("You can probably fix that by rebooting your computer (or stopping it if you know what it is).");
|
// (because node's netowrking layer == 💩 )
|
||||||
|
var m;
|
||||||
|
var str;
|
||||||
|
var servername;
|
||||||
|
// TODO test per-module
|
||||||
|
// Maybe HTTP
|
||||||
|
if (firstChunk[0] > 32 && firstChunk[0] < 127) {
|
||||||
|
str = firstChunk.toString();
|
||||||
|
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
|
||||||
|
servername = (m && m[1].toLowerCase() || '').split(':')[0];
|
||||||
|
//conn.__servername = servername;
|
||||||
|
console.log('[tcpRouter] hostname', servername);
|
||||||
|
if (/HTTP\//i.test(str)) {
|
||||||
|
//conn.__service = 'http';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log('1010');
|
||||||
|
|
||||||
function createInsecureServer(port, _delete_me_, opts) {
|
if (!servername) {
|
||||||
return new PromiseA(function (realResolve) {
|
// TODO allow tcp tunneling
|
||||||
var server = http.createServer();
|
// TODO we need some way of tagging tcp as either terminated tls or insecure
|
||||||
|
conn.write(
|
||||||
function resolve() {
|
"HTTP/1.1 404 Not Found\r\n"
|
||||||
realResolve(server);
|
+ "Date: Fri, 31 Dec 1999 23:59:59 GMT\r\n"
|
||||||
}
|
+ "Content-Type: text/html\r\n"
|
||||||
|
+ "Content-Length: " + 9 + "\r\n"
|
||||||
server.on('error', function (err) {
|
+ "\r\n"
|
||||||
if (opts.errorInsecurePort || opts.manualInsecurePort) {
|
+ "Not Found"
|
||||||
showError(err, port);
|
);
|
||||||
process.exit(1);
|
conn.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.errorInsecurePort = err.toString();
|
console.log('1020');
|
||||||
|
if (/\blocalhost\.admin\./.test(servername) || /\badmin\.localhost\./.test(servername)
|
||||||
return createInsecureServer(insecurePortFallback, null, opts).then(resolve);
|
|| /\blocalhost\.alpha\./.test(servername) || /\balpha\.localhost\./.test(servername)) {
|
||||||
});
|
console.log('1050');
|
||||||
|
if (!modules.admin) {
|
||||||
server.on('request', opts.redirectApp);
|
modules.admin = require('./modules/admin.js').create(deps, config);
|
||||||
|
}
|
||||||
server.listen(port, function () {
|
console.log('1100');
|
||||||
opts.insecurePort = port;
|
modules.admin.emit('connection', conn);
|
||||||
resolve();
|
console.log('1500');
|
||||||
});
|
return;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createServer(port, _delete_me_, content, opts) {
|
if (!modules.http) {
|
||||||
function approveDomains(params, certs, cb) {
|
if (!modules.http) {
|
||||||
|
modules.http = require('./modules/http.js').create(deps, config);
|
||||||
|
}
|
||||||
|
modules.http.emit('connection', conn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
, get: function getTcpRouter(address, port) {
|
||||||
|
address = address || '0.0.0.0';
|
||||||
|
|
||||||
|
var id = address + ':' + port;
|
||||||
|
if (!tcpRouter._map[id]) {
|
||||||
|
tcpRouter._map[id] = tcpRouter._create(address, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tcpRouter._map[id];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var tlsRouter = {
|
||||||
|
_map: { }
|
||||||
|
, _create: function (address, port) {
|
||||||
|
// port provides hinting for https, smtps, etc
|
||||||
|
return function (socket, servername) {
|
||||||
|
//program.tlsTunnelServer.emit('connection', socket);
|
||||||
|
//return;
|
||||||
|
console.log('[tlsRouter] ' + address + ':' + port + ' servername', servername);
|
||||||
|
|
||||||
|
var packerStream = require('tunnel-packer').Stream;
|
||||||
|
var myDuplex = packerStream.create(socket);
|
||||||
|
|
||||||
|
// needs to wind up in one of 3 states:
|
||||||
|
// 1. Proxied / Tunneled (we don't even need to put it through the tlsSocket)
|
||||||
|
// 2. Admin (skips normal processing)
|
||||||
|
// 3. Terminated (goes on to a particular module or route)
|
||||||
|
//myDuplex.__tlsTerminated = true;
|
||||||
|
program.tlsTunnelServer.emit('connection', myDuplex);
|
||||||
|
|
||||||
|
socket.on('data', function (chunk) {
|
||||||
|
console.log('[' + Date.now() + '] tls socket data', chunk.byteLength);
|
||||||
|
myDuplex.push(chunk);
|
||||||
|
});
|
||||||
|
socket.on('error', function (err) {
|
||||||
|
console.error('[error] httpsTunnel (Admin) TODO close');
|
||||||
|
console.error(err);
|
||||||
|
myDuplex.emit('error', err);
|
||||||
|
});
|
||||||
|
socket.on('close', function () {
|
||||||
|
myDuplex.close();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
, get: function getTcpRouter(address, port) {
|
||||||
|
address = address || '0.0.0.0';
|
||||||
|
|
||||||
|
var id = address + ':' + port;
|
||||||
|
if (!tlsRouter._map[id]) {
|
||||||
|
tlsRouter._map[id] = tlsRouter._create(address, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsRouter._map[id];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function handler(conn, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
console.log('[handler]', conn.localAddres, conn.localPort, opts.secure);
|
||||||
|
|
||||||
|
// TODO inspect SNI and HTTP Host
|
||||||
|
conn.once('data', function (firstChunk) {
|
||||||
|
var servername;
|
||||||
|
|
||||||
|
process.nextTick(function () {
|
||||||
|
conn.unshift(firstChunk);
|
||||||
|
});
|
||||||
|
// copying stuff over to firstChunk because the network abstraction goes too deep to find these again
|
||||||
|
//firstChunk.__port = conn.__port;
|
||||||
|
|
||||||
|
// TLS
|
||||||
|
if (22 === firstChunk[0]) {
|
||||||
|
servername = (parseSni(firstChunk)||'').toLowerCase() || 'localhost.invalid';
|
||||||
|
//conn.__servername = servername;
|
||||||
|
//conn.__tls = true;
|
||||||
|
//conn.__tlsTerminated = false;
|
||||||
|
//firstChunk.__servername = conn.__servername;
|
||||||
|
//firstChunk.__tls = true;
|
||||||
|
//firstChunk.__tlsTerminated = false;
|
||||||
|
console.log('tryTls');
|
||||||
|
tlsRouter.get(conn.localAddress, conn.localPort)(conn, servername);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO how to tag as insecure?
|
||||||
|
console.log('tryTcp');
|
||||||
|
tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, { secure: opts.secure || false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
if ('http' === config.tcp.default || !config.tcp.default) {
|
||||||
|
console.log('deal with as http');
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
function approveDomains(opts, certs, cb) {
|
||||||
// This is where you check your database and associated
|
// This is where you check your database and associated
|
||||||
// email addresses with domains and agreements and such
|
// email addresses with domains and agreements and such
|
||||||
var domains = params.domains;
|
|
||||||
//var p;
|
|
||||||
console.log('approveDomains');
|
|
||||||
console.log(domains);
|
|
||||||
|
|
||||||
|
|
||||||
// The domains being approved for the first time are listed in opts.domains
|
// The domains being approved for the first time are listed in opts.domains
|
||||||
// Certs being renewed are listed in certs.altnames
|
// Certs being renewed are listed in certs.altnames
|
||||||
if (certs) {
|
|
||||||
params.domains = certs.altnames;
|
function complete(err, stuff) {
|
||||||
//p = PromiseA.resolve();
|
opts.email = stuff.email;
|
||||||
|
opts.agreeTos = stuff.agreeTos;
|
||||||
|
opts.server = stuff.server;
|
||||||
|
opts.challengeType = stuff.challengeType;
|
||||||
|
|
||||||
|
cb(null, { options: opts, certs: certs });
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
//params.email = opts.email;
|
if (certs) {
|
||||||
if (!opts.agreeTos) {
|
// TODO make sure the same options are used for renewal as for registration?
|
||||||
console.error("You have not previously registered '" + domains + "' so you must specify --agree-tos to agree to both the Let's Encrypt and Daplie DNS terms of service.");
|
opts.domains = certs.altnames;
|
||||||
process.exit(1);
|
|
||||||
|
cb(null, { options: opts, certs: certs });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
params.agreeTos = opts.agreeTos;
|
|
||||||
|
// check config for domain name
|
||||||
|
if (-1 !== config.tls.servernames.indexOf(opts.domain)) {
|
||||||
|
// TODO how to handle SANs?
|
||||||
|
// TODO fetch domain-specific email
|
||||||
|
// TODO fetch domain-specific acmeDirectory
|
||||||
|
// NOTE: you can also change other options such as `challengeType` and `challenge`
|
||||||
|
// opts.challengeType = 'http-01';
|
||||||
|
// opts.challenge = require('le-challenge-fs').create({}); // TODO this doesn't actually work yet
|
||||||
|
complete(null, {
|
||||||
|
email: config.tls.email, agreeTos: true, server: program.acmeDirectoryUrl, challengeType: program.challengeType });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO ask http module about the default path (/srv/www/:hostname)
|
||||||
|
// (if it exists, we can allow and add to config)
|
||||||
|
if (!modules.http) {
|
||||||
|
modules.http = require('./modules/http.js').create(config);
|
||||||
|
}
|
||||||
|
modules.http.checkServername(opts.domain).then(function (stuff) {
|
||||||
|
if (!stuff.domains) {
|
||||||
|
// TODO once precheck is implemented we can just let it pass if it passes, yknow?
|
||||||
|
cb(new Error('domain is not allowed'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ddns.token(params.email, domains[0])
|
complete(null, {
|
||||||
params.email = opts.email;
|
domain: stuff.domain || stuff.domains[0]
|
||||||
params.refreshToken = opts.refreshToken;
|
, domains: stuff.domains
|
||||||
params.challengeType = 'dns-01';
|
, email: program.email
|
||||||
params.cli = opts.argv;
|
, server: program.acmeDirectoryUrl
|
||||||
|
, challengeType: program.challengeType
|
||||||
cb(null, { options: params, certs: certs });
|
});
|
||||||
|
return;
|
||||||
|
}, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PromiseA(function (realResolve) {
|
function getAcme() {
|
||||||
var app = require('../lib/app.js');
|
return greenlock.create({
|
||||||
var ipaddr = require('ipaddr.js');
|
|
||||||
var addresses = [];
|
|
||||||
|
|
||||||
Object.keys(opts.ifaces).forEach(function (ifacename) {
|
//server: 'staging'
|
||||||
var iface = opts.ifaces[ifacename];
|
server: 'https://acme-v01.api.letsencrypt.org/directory'
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
var Oauth3 = require('oauth3-cli');
|
|
||||||
var oauth3 = Oauth3.create({ device: { hostname: opts.device } });
|
|
||||||
return Oauth3.Devices.one(oauth3).then(function (device) {
|
|
||||||
return Oauth3.Devices.all(oauth3).then(function (devices) {
|
|
||||||
return { devices: devices, device: device.device || device };
|
|
||||||
});
|
|
||||||
}).then(function (devices) {
|
|
||||||
devices.device.secret = undefined;
|
|
||||||
console.log('devices');
|
|
||||||
console.log(devices);
|
|
||||||
var directive = {
|
|
||||||
global: opts.global
|
|
||||||
, sites: opts.sites
|
|
||||||
, defaults: opts.defaults
|
|
||||||
, cwd: process.cwd()
|
|
||||||
, ifaces: opts.ifaces
|
|
||||||
, addresses: addresses
|
|
||||||
, devices: devices.devices
|
|
||||||
, device: devices.device
|
|
||||||
, net: {
|
|
||||||
createConnection: function (opts, cb) {
|
|
||||||
// opts = { host, port, data
|
|
||||||
// , /*proprietary to tunneler*/ servername, remoteAddress, remoteFamily, remotePort
|
|
||||||
// , secure (tls already terminated by a proxy) }
|
|
||||||
// // http://stackoverflow.com/questions/10348906/how-to-know-if-a-request-is-http-or-https-in-node-js
|
|
||||||
// var packerStream = require('tunnel-packer').Stream;
|
|
||||||
// TODO here we will have the tls termination (or re-forward)
|
|
||||||
return require('net').createConnection(opts, cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var server;
|
|
||||||
var insecureServer;
|
|
||||||
|
|
||||||
function resolve() {
|
|
||||||
realResolve({
|
|
||||||
plainServer: insecureServer
|
|
||||||
, server: server
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns an instance of node-letsencrypt with additional helper methods
|
|
||||||
var webrootPath = require('os').tmpdir();
|
|
||||||
var leChallengeFs = require('le-challenge-fs').create({ webrootPath: webrootPath });
|
|
||||||
//var leChallengeSni = require('le-challenge-sni').create({ webrootPath: webrootPath });
|
|
||||||
var leChallengeDdns = require('le-challenge-ddns').create({ ttl: 1 });
|
|
||||||
var lex = require('greenlock-express').create({
|
|
||||||
// set to https://acme-v01.api.letsencrypt.org/directory in production
|
|
||||||
server: opts.debug ? 'staging' : 'https://acme-v01.api.letsencrypt.org/directory'
|
|
||||||
|
|
||||||
// If you wish to replace the default plugins, you may do so here
|
|
||||||
//
|
|
||||||
, challenges: {
|
, challenges: {
|
||||||
'http-01': leChallengeFs
|
// TODO dns-01
|
||||||
, 'tls-sni-01': leChallengeFs // leChallengeSni
|
'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges', debug: config.debug })
|
||||||
, 'dns-01': leChallengeDdns
|
, 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug })
|
||||||
|
//, 'dns-01': require('le-challenge-ddns').create()
|
||||||
}
|
}
|
||||||
, challengeType: (opts.tunnel ? 'http-01' : 'dns-01')
|
|
||||||
, store: require('le-store-certbot').create({
|
|
||||||
webrootPath: webrootPath
|
|
||||||
, configDir: path.join((opts.homedir || '~'), 'letsencrypt', 'etc')
|
|
||||||
, homedir: opts.homedir
|
|
||||||
})
|
|
||||||
, webrootPath: webrootPath
|
|
||||||
|
|
||||||
// You probably wouldn't need to replace the default sni handler
|
, store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' })
|
||||||
// See https://git.daplie.com/Daplie/le-sni-auto if you think you do
|
|
||||||
//, sni: require('le-sni-auto').create({})
|
//, email: program.email
|
||||||
|
|
||||||
|
//, agreeTos: program.agreeTos
|
||||||
|
|
||||||
, approveDomains: approveDomains
|
, approveDomains: approveDomains
|
||||||
});
|
|
||||||
|
|
||||||
var secureContexts = {
|
//, approvedDomains: program.servernames
|
||||||
'localhost.daplie.me': null
|
|
||||||
};
|
});
|
||||||
opts.httpsOptions.SNICallback = function (sni, cb ) {
|
}
|
||||||
|
|
||||||
|
Object.keys(program.tlsOptions).forEach(function (key) {
|
||||||
|
tunnelAdminTlsOpts[key] = program.tlsOptions[key];
|
||||||
|
});
|
||||||
|
tunnelAdminTlsOpts.SNICallback = function (sni, cb) {
|
||||||
|
console.log("[tlsOptions.SNICallback] SNI: '" + sni + "'");
|
||||||
|
|
||||||
var tlsOptions;
|
var tlsOptions;
|
||||||
console.log('[https] sni', sni);
|
|
||||||
|
|
||||||
// Static Certs
|
// Static Certs
|
||||||
if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) {
|
if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) {
|
||||||
|
@ -204,485 +279,31 @@ module.exports.create = function (config) {
|
||||||
if (tlsOptions) {
|
if (tlsOptions) {
|
||||||
secureContexts[sni] = tls.createSecureContext(tlsOptions);
|
secureContexts[sni] = tls.createSecureContext(tlsOptions);
|
||||||
}
|
}
|
||||||
|
if (secureContexts[sni]) {
|
||||||
|
console.log('Got static secure context:', sni, secureContexts[sni]);
|
||||||
cb(null, secureContexts[sni]);
|
cb(null, secureContexts[sni]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic Certs
|
|
||||||
lex.httpsOptions.SNICallback(sni, cb);
|
|
||||||
};
|
|
||||||
server = https.createServer(opts.httpsOptions);
|
|
||||||
|
|
||||||
server.on('error', function (err) {
|
|
||||||
if (opts.errorPort || opts.manualPort) {
|
|
||||||
showError(err, port);
|
|
||||||
process.exit(1);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.errorPort = err.toString();
|
if (!program.greenlock) {
|
||||||
|
program.greenlock = getAcme();
|
||||||
return createServer(portFallback, null, content, opts).then(resolve);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(port, function () {
|
|
||||||
opts.port = port;
|
|
||||||
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: [ 'node_modules' ]
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info("[livereload] watching " + opts.pubdir);
|
|
||||||
console.warn("WARNING: If CPU usage spikes to 100% it's because too many files are being watched");
|
|
||||||
// TODO create map of directories to watch from opts.sites and iterate over it
|
|
||||||
server2.watch(opts.pubdir);
|
|
||||||
}
|
}
|
||||||
|
(program.greenlock.tlsOptions||program.greenlock.httpsOptions).SNICallback(servername, cb);
|
||||||
// if we haven't disabled insecure port
|
|
||||||
if ('false' !== opts.insecurePort) {
|
|
||||||
// and both ports are the default
|
|
||||||
if ((httpsPort === opts.port && httpPort === opts.insecurePort)
|
|
||||||
// or other case
|
|
||||||
|| (httpPort !== opts.insecurePort && opts.port !== opts.insecurePort)
|
|
||||||
) {
|
|
||||||
return createInsecureServer(opts.insecurePort, null, opts).then(function (_server) {
|
|
||||||
insecureServer = _server;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.insecurePort = opts.port;
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
if ('function' === typeof app) {
|
|
||||||
app = app(directive);
|
|
||||||
} else if ('function' === typeof app.create) {
|
|
||||||
app = app.create(directive);
|
|
||||||
}
|
|
||||||
|
|
||||||
server.on('request', function (req, res) {
|
|
||||||
console.log('[' + req.method + '] ' + req.url);
|
|
||||||
if (!req.socket.encrypted && !/\/\.well-known\/acme-challenge\//.test(req.url)) {
|
|
||||||
opts.redirectApp(req, res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('function' === typeof app) {
|
|
||||||
app(req, res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.end('not ready');
|
|
||||||
});
|
|
||||||
|
|
||||||
return PromiseA.resolve(app).then(function (_app) {
|
|
||||||
app = _app;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.createServer = createServer;
|
|
||||||
|
|
||||||
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) {
|
program.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) {
|
||||||
// TODO remove in v3.x (aka goldilocks)
|
console.log('(pre-terminated) tls connection');
|
||||||
argv.key = argv.key || '/etc/letsencrypt/live/' + letsencryptHost + '/privkey.pem';
|
// things get a little messed up here
|
||||||
argv.cert = argv.cert || '/etc/letsencrypt/live/' + letsencryptHost + '/fullchain.pem';
|
//tlsSocket.on('data', function (chunk) {
|
||||||
argv.root = argv.root || argv.chain || '';
|
// console.log('terminated data:', chunk.toString());
|
||||||
argv.sites = argv.sites || letsencryptHost;
|
//});
|
||||||
argv['serve-root'] = argv['serve-root'] || argv['serve-chain'];
|
//(program.httpTunnelServer || program.httpServer).emit('connection', tlsSocket);
|
||||||
// argv[express-app]
|
//tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, { secure: false });
|
||||||
}
|
handler(tlsSocket, { secure: true });
|
||||||
|
});
|
||||||
|
|
||||||
if (argv['serve-root'] && !argv.root) {
|
PromiseA.all(config.tcp.ports.map(function (port) {
|
||||||
console.error("You must specify bath --root to use --serve-root");
|
return listeners.tcp.add(port, handler);
|
||||||
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();
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
module.exports.create = function (deps, conf) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var path = require('path');
|
||||||
|
//var defaultServername = 'localhost.daplie.me';
|
||||||
|
var defaultWebRoot = '.';
|
||||||
|
var assetsPath = path.join(__dirname, '..', '..', 'packages', 'assets');
|
||||||
|
var opts = /*conf.http ||*/ {};
|
||||||
|
|
||||||
|
opts.sites = [];
|
||||||
|
opts.sites._map = {};
|
||||||
|
|
||||||
|
// argv.sites
|
||||||
|
|
||||||
|
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') } ] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = require('../app.js')(deps, { cwd: conf.cwd, http: opts });
|
||||||
|
var http = require('http');
|
||||||
|
return http.createServer(app);
|
||||||
|
};
|
|
@ -0,0 +1,392 @@
|
||||||
|
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();
|
|
@ -0,0 +1,107 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var serversMap = module.exports._serversMap = {};
|
||||||
|
|
||||||
|
module.exports.addTcpListener = function (port, handler) {
|
||||||
|
var PromiseA = require('bluebird');
|
||||||
|
|
||||||
|
return new PromiseA(function (resolve, reject) {
|
||||||
|
var stat = serversMap[port] || serversMap[port];
|
||||||
|
|
||||||
|
if (stat) {
|
||||||
|
if (stat._closing) {
|
||||||
|
module.exports.destroyTcpListener(port);
|
||||||
|
}
|
||||||
|
else if (handler !== stat.handler) {
|
||||||
|
|
||||||
|
// we'll replace the current listener
|
||||||
|
stat.handler = handler;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// this exact listener is already open
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var enableDestroy = require('server-destroy');
|
||||||
|
var net = require('net');
|
||||||
|
var resolved;
|
||||||
|
var server = net.createServer();
|
||||||
|
|
||||||
|
stat = serversMap[port] = {
|
||||||
|
server: server
|
||||||
|
, handler: handler
|
||||||
|
, _closing: false
|
||||||
|
};
|
||||||
|
|
||||||
|
server.on('connection', function (conn) {
|
||||||
|
conn.__port = port;
|
||||||
|
conn.__proto = 'tcp';
|
||||||
|
stat.handler(conn);
|
||||||
|
});
|
||||||
|
server.on('error', function (e) {
|
||||||
|
delete serversMap[port];
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
reject(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handler.onError) {
|
||||||
|
handler.onError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, function () {
|
||||||
|
resolved = true;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
enableDestroy(server); // adds .destroy
|
||||||
|
});
|
||||||
|
};
|
||||||
|
module.exports.closeTcpListener = function (port) {
|
||||||
|
var PromiseA = require('bluebird');
|
||||||
|
|
||||||
|
return new PromiseA(function (resolve) {
|
||||||
|
var stat = serversMap[port];
|
||||||
|
if (!stat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stat.server.on('close', function () {
|
||||||
|
// once the clients close too
|
||||||
|
delete serversMap[port];
|
||||||
|
if (stat._closing) {
|
||||||
|
stat._closing(); // resolve
|
||||||
|
stat._closing = null;
|
||||||
|
}
|
||||||
|
stat = null;
|
||||||
|
});
|
||||||
|
stat._closing = resolve;
|
||||||
|
stat.server.close();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
module.exports.destroyTcpListener = function (port) {
|
||||||
|
var stat = serversMap[port];
|
||||||
|
delete serversMap[port];
|
||||||
|
stat.server.destroy();
|
||||||
|
if (stat._closing) {
|
||||||
|
stat._closing();
|
||||||
|
stat._closing = null;
|
||||||
|
}
|
||||||
|
stat = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.listeners = {
|
||||||
|
tcp: {
|
||||||
|
add: module.exports.addTcpListener
|
||||||
|
, close: module.exports.closeTcpListener
|
||||||
|
, destroy: module.exports.destroyTcpListener
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// TODO needs some sort of config-sync
|
||||||
|
process.on('message', function (conf) {
|
||||||
|
var deps = {
|
||||||
|
messenger: process
|
||||||
|
};
|
||||||
|
require('./goldilocks.js').create(deps, conf);
|
||||||
|
});
|
|
@ -63,6 +63,7 @@
|
||||||
"scmp": "git+https://github.com/freewil/scmp.git#1.x",
|
"scmp": "git+https://github.com/freewil/scmp.git#1.x",
|
||||||
"serve-index": "^1.7.0",
|
"serve-index": "^1.7.0",
|
||||||
"serve-static": "^1.10.0",
|
"serve-static": "^1.10.0",
|
||||||
|
"server-destroy": "^1.0.1",
|
||||||
"stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1"
|
"stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports.dependencies = [ 'OAUTH3', 'storage.owners', 'options.device' ];
|
module.exports.dependencies = [ 'OAUTH3', 'storage.owners', 'options.device' ];
|
||||||
module.exports.create = function (deps) {
|
module.exports.create = function (deps, conf) {
|
||||||
var scmp = require('scmp');
|
var scmp = require('scmp');
|
||||||
var crypto = require('crypto');
|
var crypto = require('crypto');
|
||||||
var jwt = require('jsonwebtoken');
|
var jwt = require('jsonwebtoken');
|
||||||
|
@ -69,7 +69,7 @@ module.exports.create = function (deps) {
|
||||||
|
|
||||||
if (req.body.ip_url) {
|
if (req.body.ip_url) {
|
||||||
// TODO set options / GunDB
|
// TODO set options / GunDB
|
||||||
deps.options.ip_url = req.body.ip_url;
|
conf.ip_url = req.body.ip_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return deps.storage.owners.all().then(function (results) {
|
return deps.storage.owners.all().then(function (results) {
|
||||||
|
@ -139,7 +139,7 @@ module.exports.create = function (deps) {
|
||||||
isAuthorized(req, res, function () {
|
isAuthorized(req, res, function () {
|
||||||
if ('POST' !== req.method) {
|
if ('POST' !== req.method) {
|
||||||
res.setHeader('Content-Type', 'application/json;');
|
res.setHeader('Content-Type', 'application/json;');
|
||||||
res.end(JSON.stringify(deps.recase.snakeCopy(deps.options)));
|
res.end(JSON.stringify(deps.recase.snakeCopy(conf.snake_copy)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue