WIP merging walnut, serve-https, and stunnel.js

This commit is contained in:
AJ ONeal 2017-04-26 20:16:47 -06:00
parent 4267955286
commit 67aa28aece
10 changed files with 1044 additions and 700 deletions

View File

@ -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);

View File

@ -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,37 +214,44 @@ module.exports = function (opts) {
app = express(); app = express();
var Sites = {
add: function (sitesMap, site) {
if (!sitesMap[site.$id]) {
sitesMap[site.$id] = site;
}
if (!site.paths) {
site.paths = [];
}
if (!site.paths._map) {
site.paths._map = {};
}
site.paths.forEach(function (path) {
site.paths._map[path.$id] = path;
if (!path.modules) {
path.modules = [];
}
if (!path.modules._map) {
path.modules._map = {};
}
path.modules.forEach(function (module) {
path.modules._map[module.$id] = module;
});
});
}
};
var opts = conf.http;
if (!opts.sites) { if (!opts.sites) {
opts.sites = []; opts.sites = [];
} }
opts.sites._map = {}; opts.sites._map = {};
opts.sites.forEach(function (site) { opts.sites.forEach(function (site) {
if (!opts.sites._map[site.$id]) { Sites.add(opts.sites._map, site);
opts.sites._map[site.$id] = site;
}
if (!site.paths) {
site.paths = [];
}
if (!site.paths._map) {
site.paths._map = {};
}
site.paths.forEach(function (path) {
site.paths._map[path.$id] = path;
if (!path.modules) {
path.modules = [];
}
if (!path.modules._map) {
path.modules._map = {};
}
path.modules.forEach(function (module) {
path.modules._map[module.$id] = module;
});
});
}); });
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);
} }

55
lib/check-ports.js Normal file
View File

@ -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;

View File

@ -1,688 +1,309 @@
'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 tls = require('tls'); var greenlock = require('greenlock');
var https = require('httpolyglot'); var listeners = require('./servers').listeners;
var http = require('http'); var parseSni = require('sni');
var path = require('path'); var modules = { };
var httpPort = 80; var program = {
var httpsPort = 443; tlsOptions: require('localhost.daplie.me-certificates').merge({})
var lrPort = 35729; , acmeDirectoryUrl: 'https://acme-v01.api.letsencrypt.org/directory'
var portFallback = 8443; , challengeType: 'tls-sni-01'
var insecurePortFallback = 4080; };
var secureContexts = {};
var tunnelAdminTlsOpts = {};
var tls = require('tls');
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');
if (!servername) {
// TODO allow tcp tunneling
// TODO we need some way of tagging tcp as either terminated tls or insecure
conn.write(
"HTTP/1.1 404 Not Found\r\n"
+ "Date: Fri, 31 Dec 1999 23:59:59 GMT\r\n"
+ "Content-Type: text/html\r\n"
+ "Content-Length: " + 9 + "\r\n"
+ "\r\n"
+ "Not Found"
);
conn.end();
return;
}
console.log('1020');
if (/\blocalhost\.admin\./.test(servername) || /\badmin\.localhost\./.test(servername)
|| /\blocalhost\.alpha\./.test(servername) || /\balpha\.localhost\./.test(servername)) {
console.log('1050');
if (!modules.admin) {
modules.admin = require('./modules/admin.js').create(deps, config);
}
console.log('1100');
modules.admin.emit('connection', conn);
console.log('1500');
return;
}
if (!modules.http) {
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 createInsecureServer(port, _delete_me_, opts) { function approveDomains(opts, certs, cb) {
return new PromiseA(function (realResolve) { // This is where you check your database and associated
var server = http.createServer(); // email addresses with domains and agreements and such
function resolve() { // The domains being approved for the first time are listed in opts.domains
realResolve(server); // Certs being renewed are listed in certs.altnames
function complete(err, stuff) {
opts.email = stuff.email;
opts.agreeTos = stuff.agreeTos;
opts.server = stuff.server;
opts.challengeType = stuff.challengeType;
cb(null, { options: opts, certs: certs });
}
if (certs) {
// TODO make sure the same options are used for renewal as for registration?
opts.domains = certs.altnames;
cb(null, { options: opts, certs: certs });
return;
}
// 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;
}
complete(null, {
domain: stuff.domain || stuff.domains[0]
, domains: stuff.domains
, email: program.email
, server: program.acmeDirectoryUrl
, challengeType: program.challengeType
});
return;
}, cb);
}
function getAcme() {
return greenlock.create({
//server: 'staging'
server: 'https://acme-v01.api.letsencrypt.org/directory'
, challenges: {
// TODO dns-01
'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges', debug: config.debug })
, 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug })
//, 'dns-01': require('le-challenge-ddns').create()
} }
server.on('error', function (err) { , store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' })
if (opts.errorInsecurePort || opts.manualInsecurePort) {
showError(err, port);
process.exit(1);
return;
}
opts.errorInsecurePort = err.toString(); //, email: program.email
return createInsecureServer(insecurePortFallback, null, opts).then(resolve); //, agreeTos: program.agreeTos
});
server.on('request', opts.redirectApp); , approveDomains: approveDomains
//, approvedDomains: program.servernames
server.listen(port, function () {
opts.insecurePort = port;
resolve();
});
}); });
} }
function createServer(port, _delete_me_, content, opts) { Object.keys(program.tlsOptions).forEach(function (key) {
function approveDomains(params, certs, cb) { tunnelAdminTlsOpts[key] = program.tlsOptions[key];
// This is where you check your database and associated });
// email addresses with domains and agreements and such tunnelAdminTlsOpts.SNICallback = function (sni, cb) {
var domains = params.domains; console.log("[tlsOptions.SNICallback] SNI: '" + sni + "'");
//var p;
console.log('approveDomains');
console.log(domains);
var tlsOptions;
// The domains being approved for the first time are listed in opts.domains // Static Certs
// Certs being renewed are listed in certs.altnames if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) {
if (certs) { // TODO implement
params.domains = certs.altnames; if (!secureContexts[sni]) {
//p = PromiseA.resolve(); tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {});
} }
else { if (tlsOptions) {
//params.email = opts.email; secureContexts[sni] = tls.createSecureContext(tlsOptions);
if (!opts.agreeTos) {
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.");
process.exit(1);
return;
}
params.agreeTos = opts.agreeTos;
} }
if (secureContexts[sni]) {
// ddns.token(params.email, domains[0]) console.log('Got static secure context:', sni, secureContexts[sni]);
params.email = opts.email; cb(null, secureContexts[sni]);
params.refreshToken = opts.refreshToken;
params.challengeType = 'dns-01';
params.cli = opts.argv;
cb(null, { options: params, certs: certs });
}
return new PromiseA(function (realResolve) {
var app = require('../lib/app.js');
var ipaddr = require('ipaddr.js');
var addresses = [];
Object.keys(opts.ifaces).forEach(function (ifacename) {
var iface = opts.ifaces[ifacename];
iface.ipv4.forEach(function (ip) {
addresses.push(ip);
});
iface.ipv6.forEach(function (ip) {
addresses.push(ip);
});
});
addresses.sort(function (a, b) {
if (a.family !== b.family) {
return 'IPv4' === a.family ? 1 : -1;
}
return a.address > b.address ? 1 : -1;
});
addresses.forEach(function (addr) {
addr.range = ipaddr.parse(addr.address).range();
});
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: {
'http-01': leChallengeFs
, 'tls-sni-01': leChallengeFs // leChallengeSni
, 'dns-01': leChallengeDdns
}
, 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
// See https://git.daplie.com/Daplie/le-sni-auto if you think you do
//, sni: require('le-sni-auto').create({})
, approveDomains: approveDomains
});
var secureContexts = {
'localhost.daplie.me': null
};
opts.httpsOptions.SNICallback = function (sni, cb ) {
var tlsOptions;
console.log('[https] sni', sni);
// Static Certs
if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) {
// TODO implement
if (!secureContexts[sni]) {
tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {});
}
if (tlsOptions) {
secureContexts[sni] = tls.createSecureContext(tlsOptions);
}
cb(null, secureContexts[sni]);
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();
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);
}
// 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) {
// 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; 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');
}
} }
if (!program.greenlock) {
opts.cwd = process.cwd(); program.greenlock = getAcme();
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
});
});
} }
(program.greenlock.tlsOptions||program.greenlock.httpsOptions).SNICallback(servername, cb);
};
opts.groups = []; program.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) {
console.log('(pre-terminated) tls connection');
// things get a little messed up here
//tlsSocket.on('data', function (chunk) {
// console.log('terminated data:', chunk.toString());
//});
//(program.httpTunnelServer || program.httpServer).emit('connection', tlsSocket);
//tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, { secure: false });
handler(tlsSocket, { secure: true });
});
// 'packages', 'assets', 'com.daplie.caddy' PromiseA.all(config.tcp.ports.map(function (port) {
opts.global = { return listeners.tcp.add(port, handler);
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();
}; };

66
lib/modules/admin.js Normal file
View File

@ -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);
};

392
lib/modules/http.js Normal file
View File

@ -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();

107
lib/servers.js Normal file
View File

@ -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
}
};

9
lib/worker.js Normal file
View File

@ -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);
});

View File

@ -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"
} }
} }

View File

@ -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;
} }