From 67aa28aecef85341383df0ce828607eb46dee391 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 26 Apr 2017 20:16:47 -0600 Subject: [PATCH] WIP merging walnut, serve-https, and stunnel.js --- bin/goldilocks.js | 99 ++- lib/app.js | 72 +- lib/check-ports.js | 55 ++ lib/goldilocks.js | 937 +++++++----------------- lib/modules/admin.js | 66 ++ lib/modules/http.js | 392 ++++++++++ lib/servers.js | 107 +++ lib/worker.js | 9 + package.json | 1 + packages/apis/com.daplie.caddy/index.js | 6 +- 10 files changed, 1044 insertions(+), 700 deletions(-) create mode 100644 lib/check-ports.js create mode 100644 lib/modules/admin.js create mode 100644 lib/modules/http.js create mode 100644 lib/servers.js create mode 100644 lib/worker.js diff --git a/bin/goldilocks.js b/bin/goldilocks.js index 53a85f0..4125450 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -1,10 +1,32 @@ #!/usr/bin/env node 'use strict'; +var cluster = require('cluster'); + +if (!cluster.isMaster) { + require('../lib/worker.js'); + return; +} + +function run(config) { + // TODO spin up multiple workers + // TODO use greenlock-cluster + function work() { + var worker = cluster.fork(); + worker.on('exit', work).on('online', function () { + console.log('[worker]', worker.id, 'online'); + // Worker is listening + worker.send(config); + }); + } + console.log('config.tcp.ports', config.tcp.ports); + work(); +} + function readConfigAndRun(args) { var fs = require('fs'); var path = require('path'); - var cwd = args.cwd || process.cwd(); + var cwd = args.cwd; var text; var filename; var config; @@ -13,13 +35,13 @@ function readConfigAndRun(args) { text = fs.readFileSync(path.join(cwd, args.config), 'utf8'); } else { - filename = path.join(cwd, 'Goldilocks.yml'); + filename = path.join(cwd, 'goldilocks.yml'); if (fs.existsSync(filename)) { text = fs.readFileSync(filename, 'utf8'); } else { - filename = path.join(cwd, 'Goldilocks.json'); + filename = path.join(cwd, 'goldilocks.json'); if (fs.existsSync(filename)) { text = fs.readFileSync(filename, 'utf8'); } 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) { - readConfigAndRun({}); + readEnv({ cwd: process.cwd() }); } else if (process.argv.length === 4) { 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) { @@ -56,11 +137,15 @@ else if (process.argv.length > 2) { program .version(require('package.json').version) + .option('--agree-tos [url1,url2]', "agree to all Terms of Service for Daplie, Let's Encrypt, etc (or specific URLs only)") .option('--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('--email ', "(Re)set default email to use for Daplie, Let's Encrypt, ACME, etc.") + .option('--debug', "Enable debug output") .parse(process.argv); - readConfigAndRun(program); + program.cwd = process.cwd(); + readEnv(program); } else { throw new Error("impossible number of arguments: " + process.argv.length); diff --git a/lib/app.js b/lib/app.js index 9cf038d..dfd844f 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = function (opts) { +module.exports = function (deps, conf) { var express = require('express'); //var finalhandler = require('finalhandler'); var serveStatic = require('serve-static'); @@ -11,7 +11,7 @@ module.exports = function (opts) { var serveStaticMap = {}; var serveIndexMap = {}; - var content = opts.content; + var content = conf.content; //var server; var serveInit; var app; @@ -106,7 +106,7 @@ module.exports = function (opts) { } , recase: require('recase').create({}) , request: request - , options: opts + , options: conf , api: { // TODO move loopback to oauth3.api('tunnel:loopback') loopback: function (deps, session, opts2) { @@ -184,7 +184,7 @@ module.exports = function (opts) { , stunneld: result.tunnelUrl // we'll provide faux networking and pipe as we please , services: { https: { '*': 443 }, http: { '*': 80 }, smtp: { '*': 25}, smtps: { '*': 587 /*also 465/starttls*/ } /*, ssh: { '*': 22 }*/ } - , net: opts.net + , net: conf.net }; if (tun) { @@ -199,7 +199,7 @@ module.exports = function (opts) { if (!tun) { tun = stunnel.connect(opts3); - opts.tun = true; + conf.tun = true; } }); /* @@ -214,37 +214,44 @@ module.exports = function (opts) { 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) { opts.sites = []; } opts.sites._map = {}; opts.sites.forEach(function (site) { - if (!opts.sites._map[site.$id]) { - 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; - }); - }); + Sites.add(opts.sites._map, site); }); function mapMap(el, i, arr) { @@ -277,6 +284,7 @@ module.exports = function (opts) { path.modules._map = {}; path.modules.forEach(mapMap); }); + return app.use('/', function (req, res, next) { if (!req.headers.host) { next(new Error('missing HTTP Host header')); @@ -331,7 +339,7 @@ module.exports = function (opts) { } 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]) { serveStaticMap[dirname] = serveStatic(dirname); } @@ -355,7 +363,7 @@ module.exports = function (opts) { } 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]) { serveIndexMap[dirname] = serveIndex(dirname); } diff --git a/lib/check-ports.js b/lib/check-ports.js new file mode 100644 index 0000000..fc098c0 --- /dev/null +++ b/lib/check-ports.js @@ -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; diff --git a/lib/goldilocks.js b/lib/goldilocks.js index bde7a88..ad7e86c 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -1,688 +1,309 @@ 'use strict'; -module.exports.create = function (config) { +module.exports.create = function (deps, config) { + console.log('config', config); + //var PromiseA = global.Promise; var PromiseA = require('bluebird'); - 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; + 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'); - function showError(err, port) { - if ('EACCES' === err.code) { - console.error(err); - console.warn("You do not have permission to use '" + port + "'."); - console.warn("You can probably fix that by running as Administrator or root."); - } - else if ('EADDRINUSE' === err.code) { - console.warn("Another server is already running on '" + port + "'."); - console.warn("You can probably fix that by rebooting your computer (or stopping it if you know what it is)."); + var tcpRouter = { + _map: { } + , _create: function (address, port) { + // port provides hinting for http, smtp, etc + return function (conn, firstChunk) { + console.log('[tcpRouter] ' + address + ':' + port + ' servername'); + + // At this point we cannot necessarily trace which port or address the socket came from + // (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) { - return new PromiseA(function (realResolve) { - var server = http.createServer(); + function approveDomains(opts, certs, cb) { + // This is where you check your database and associated + // email addresses with domains and agreements and such - function resolve() { - realResolve(server); + // The domains being approved for the first time are listed in opts.domains + // 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) { - if (opts.errorInsecurePort || opts.manualInsecurePort) { - showError(err, port); - process.exit(1); - return; - } + , store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' }) - 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) { - function approveDomains(params, certs, cb) { - // This is where you check your database and associated - // email addresses with domains and agreements and such - var domains = params.domains; - //var p; - console.log('approveDomains'); - console.log(domains); + 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; - // The domains being approved for the first time are listed in opts.domains - // Certs being renewed are listed in certs.altnames - if (certs) { - params.domains = certs.altnames; - //p = PromiseA.resolve(); + // Static Certs + if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) { + // TODO implement + if (!secureContexts[sni]) { + tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {}); } - else { - //params.email = opts.email; - 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 (tlsOptions) { + secureContexts[sni] = tls.createSecureContext(tlsOptions); } - - // ddns.token(params.email, domains[0]) - params.email = opts.email; - 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)"); + if (secureContexts[sni]) { + console.log('Got static secure context:', sni, secureContexts[sni]); + cb(null, secureContexts[sni]); 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 - }); - }); + if (!program.greenlock) { + program.greenlock = getAcme(); } + (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' - 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(); + PromiseA.all(config.tcp.ports.map(function (port) { + return listeners.tcp.add(port, handler); + })); }; diff --git a/lib/modules/admin.js b/lib/modules/admin.js new file mode 100644 index 0000000..82c9609 --- /dev/null +++ b/lib/modules/admin.js @@ -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); +}; diff --git a/lib/modules/http.js b/lib/modules/http.js new file mode 100644 index 0000000..7adf9a1 --- /dev/null +++ b/lib/modules/http.js @@ -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(); diff --git a/lib/servers.js b/lib/servers.js new file mode 100644 index 0000000..a0afb0b --- /dev/null +++ b/lib/servers.js @@ -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 + } +}; diff --git a/lib/worker.js b/lib/worker.js new file mode 100644 index 0000000..87f9338 --- /dev/null +++ b/lib/worker.js @@ -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); +}); diff --git a/package.json b/package.json index 23fb45d..ccaa30a 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "scmp": "git+https://github.com/freewil/scmp.git#1.x", "serve-index": "^1.7.0", "serve-static": "^1.10.0", + "server-destroy": "^1.0.1", "stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1" } } diff --git a/packages/apis/com.daplie.caddy/index.js b/packages/apis/com.daplie.caddy/index.js index 40ecc1b..02c7914 100644 --- a/packages/apis/com.daplie.caddy/index.js +++ b/packages/apis/com.daplie.caddy/index.js @@ -1,7 +1,7 @@ 'use strict'; module.exports.dependencies = [ 'OAUTH3', 'storage.owners', 'options.device' ]; -module.exports.create = function (deps) { +module.exports.create = function (deps, conf) { var scmp = require('scmp'); var crypto = require('crypto'); var jwt = require('jsonwebtoken'); @@ -69,7 +69,7 @@ module.exports.create = function (deps) { if (req.body.ip_url) { // 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) { @@ -139,7 +139,7 @@ module.exports.create = function (deps) { isAuthorized(req, res, function () { if ('POST' !== req.method) { 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; }