From febe106a813b3c009ea8716dff9f73af1b5cc90c Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 16 May 2017 17:19:26 -0600 Subject: [PATCH 01/15] changed how HTTP proxying works Note that with the way it is currently, proxying modules take priority over other modules even if they come later in the list. --- lib/modules/http.js | 246 +++++++++++++++++++++++++++++++------------- package.json | 1 - 2 files changed, 172 insertions(+), 75 deletions(-) diff --git a/lib/modules/http.js b/lib/modules/http.js index 8a4da01..593e867 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -1,12 +1,12 @@ 'use strict'; module.exports.create = function (deps, conf, greenlockMiddleware) { + var PromiseA = require('bluebird'); var express = require('express'); var app = express(); var adminApp = require('./admin').create(deps, conf); var domainMatches = require('../domain-utils').match; var separatePort = require('../domain-utils').separatePort; - var proxyRoutes = []; var adminDomains = [ /\blocalhost\.admin\./ @@ -15,8 +15,65 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { , /\balpha\.localhost\./ ]; + function parseHeaders(conn, opts) { + // There should already be a `firstChunk` on the opts, but because we might sometimes + // need more than that to get all the headers it's easier to always read the data off + // the connection and put it back later if we need to. + opts.firstChunk = Buffer.alloc(0); + + // First we make sure we have all of the headers. + return new PromiseA(function (resolve, reject) { + if (opts.firstChunk.includes('\r\n\r\n')) { + resolve(opts.firstChunk.toString()); + return; + } + + var errored = false; + function handleErr(err) { + errored = true; + reject(err); + } + conn.once('error', handleErr); + + function handleChunk(chunk) { + if (!errored) { + opts.firstChunk = Buffer.concat([opts.firstChunk, chunk]); + if (opts.firstChunk.includes('\r\n\r\n')) { + resolve(opts.firstChunk.toString()); + conn.removeListener('error', handleErr); + } else { + conn.once('data', handleChunk); + } + } + } + conn.once('data', handleChunk); + }).then(function (firstStr) { + var headerSection = firstStr.split('\r\n\r\n')[0]; + var lines = headerSection.split('\r\n'); + var result = {}; + + lines.slice(1).forEach(function (line) { + var match = /(.*)\s*:\s*(.*)/.exec(line); + if (match) { + result[match[1].toLowerCase()] = match[2]; + } else { + console.error('HTTP header line does not match pattern', line); + } + }); + + var match = /^([a-zA-Z]+)\s+(\S+)\s+HTTP/.exec(lines[0]); + if (!match) { + throw new Error('first line of "HTTP" does not match pattern: '+lines[0]); + } + result.method = match[1].toUpperCase(); + result.url = match[2]; + + return result; + }); + } + function moduleMatchesHost(req, mod) { - var host = separatePort(req.headers.host).host; + var host = separatePort((req.headers || req).host).host; return mod.domains.some(function (pattern) { return domainMatches(pattern, host); @@ -92,54 +149,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { res.end('Not Found'); } - function createProxyRoute(mod) { - // This is the easiest way to override the createConnections function the proxy - // module uses, but take note the since we don't have control over where this is - // called the extra options availabled will be different. - var agent = new require('http').Agent({}); - agent.createConnection = deps.net.createConnection; - - var proxy = require('http-proxy').createProxyServer({ - agent: agent - , target: 'http://' + mod.address - , xfwd: true - , toProxy: true - }); - - // We want to override the default value for some headers with the extra information we - // have available to us in the opts object attached to the connection. - proxy.on('proxyReq', function (proxyReq, req) { - var conn = req.connection; - var opts = conn.__opts; - proxyReq.setHeader('X-Forwarded-For', opts.remoteAddress || conn.remoteAddress); - }); - - proxy.on('error', function (err, req, res) { - console.log(err); - res.statusCode = 502; - res.setHeader('Content-Type', 'text/html'); - res.setHeader('Connection', 'close'); - res.end(require('../proxy-err-resp').getRespBody(err, conf.debug)); - }); - - return { - web: function (req, res, next) { - if (moduleMatchesHost(req, mod)) { - proxy.web(req, res); - } else { - next(); - } - } - , ws: function (req, socket, head, next) { - if (moduleMatchesHost(req, mod)) { - proxy.ws(req, socket, head); - } else { - next(); - } - } - }; - } - function createRedirectRoute(mod) { // Escape any characters that (can) have special meaning in regular expression // but that aren't the special characters we have interest in. @@ -202,12 +211,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { app.use(handleAdmin); (conf.http.modules || []).forEach(function (mod) { - if (mod.name === 'proxy') { - var proxyRoute = createProxyRoute(mod); - proxyRoutes.push(proxyRoute); - app.use(proxyRoute.web); - } - else if (mod.name === 'redirect') { + if (mod.name === 'redirect') { app.use(createRedirectRoute(mod)); } else if (mod.name === 'static') { @@ -220,27 +224,121 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { app.use(respond404); - var server = require('http').createServer(function (req, res) { - app(req, res) - }); + var server = require('http').createServer(app); - server.on('upgrade', function (req, socket, head) { - if (!proxyRoutes.length) { - socket.end(); + function handleHttp(conn, opts) { + server.emit('connection', conn); + + // We need to put back whatever data we read off to determine the connection was HTTP + // and to parse the headers. Must be done after data handlers added but before any new + // data comes in. + process.nextTick(function () { + conn.unshift(opts.firstChunk); + }); + + // Convenience return for all the check* functions. + return true; + } + + function checkACME(conn, opts, headers) { + if (headers.url.indexOf('/.well-known/acme-challenge/') !== 0) { + return false; } - var prs = proxyRoutes.slice(); - function proxyWs() { - var proxyRoute = prs.shift(); - if (!proxyRoute) { - socket.end(); - return; + return handleHttp(conn, opts); + } + + function checkRedirect(conn, opts, headers) { + if (conf.http.allowInsecure || conn.encrypted) { + return false; + } + if (conf.http.trustProxy && 'https' === headers['x-forwarded-proto']) { + return false; + } + + return handleHttp(conn, opts); + } + + function checkAdmin(conn, opts, headers) { + var admin = adminDomains.some(function (re) { + return re.test(headers.host); + }); + + if (admin) { + return handleHttp(conn, opts); + } + return false; + } + + function checkProxy(mod, conn, opts, headers) { + if (!moduleMatchesHost(headers, mod)) { + return false; + } + + var connected = false; + var newConnOpts = separatePort(mod.address); + newConnOpts.servername = separatePort(headers.host).host; + newConnOpts.data = opts.firstChunk; + + newConnOpts.remoteFamily = opts.family || conn.remoteFamily; + newConnOpts.remoteAddress = opts.address || conn.remoteAddress; + newConnOpts.remotePort = opts.port || conn.remotePort; + + var newConn = deps.net.createConnection(newConnOpts, function () { + connected = true; + newConn.write(opts.firstChunk); + newConn.pipe(conn); + conn.pipe(newConn); + }); + + // Not sure how to effectively report this to the user or client, but we need to listen + // for the event to prevent it from crashing us. + newConn.on('error', function (err) { + if (connected) { + console.error('HTTP proxy remote error', err); + conn.end(); + } else { + require('../proxy-err-resp').sendBadGateway(conn, err, conf.debug); + } + }); + conn.on('error', function (err) { + console.error('HTTP proxy client error', err); + newConn.end(); + }); + + return true; + } + + function handleConnection(conn) { + var opts = conn.__opts; + parseHeaders(conn, opts) + .then(function (headers) { + if (checkACME(conn, opts, headers)) { return; } + if (checkRedirect(conn, opts, headers)) { return; } + if (checkAdmin(conn, opts, headers)) { return; } + + var handled = (conf.http.modules || []).some(function (mod) { + if (mod.name === 'proxy') { + return checkProxy(mod, conn, opts, headers); + } + }); + if (handled) { + return; + } + + server.emit('connection', conn); + process.nextTick(function () { + conn.unshift(opts.firstChunk); + }); + }) + ; + } + + return { + emit: function (type, value) { + if (type === 'connection') { + handleConnection(value); } - proxyRoute.ws(req, socket, head, proxyWs); } - - proxyWs(); - }); - - return server; + }; }; diff --git a/package.json b/package.json index 6202868..197b0d4 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "finalhandler": "^0.4.0", "greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master", "greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master", - "http-proxy": "^1.16.2", "httpolyglot": "^0.1.1", "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", "ipify": "^1.1.0", From d25ceadf4aff632bec6db99d63b070990330acc8 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 17 May 2017 17:12:04 -0600 Subject: [PATCH 02/15] changed how TLS sockets are wrapped --- lib/modules/tls.js | 71 ++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/lib/modules/tls.js b/lib/modules/tls.js index a292c93..db642fd 100644 --- a/lib/modules/tls.js +++ b/lib/modules/tls.js @@ -9,38 +9,61 @@ module.exports.create = function (deps, config, netHandler) { function extractSocketProp(socket, propName) { // remoteAddress, remotePort... ugh... https://github.com/nodejs/node/issues/8854 - return socket[propName] - || socket['_' + propName] - || socket._handle._parent.owner.stream[propName] - ; + var value = socket[propName] || socket['_' + propName]; + try { + value = value || socket._handle._parent.owner.stream[propName]; + } catch (e) {} + + try { + value = value || socket._handle._parentWrap[propName]; + value = value || socket._handle._parentWrap._handle.owner.stream[propName]; + } catch (e) {} + + return value || ''; } + var addressNames = [ + 'remoteAddress' + , 'remotePort' + , 'remoteFamily' + , 'localAddress' + , 'localPort' + ]; function wrapSocket(socket, opts) { - var myDuplex = require('tunnel-packer').Stream.create(socket); - myDuplex.remoteFamily = opts.remoteFamily || myDuplex.remoteFamily; - myDuplex.remoteAddress = opts.remoteAddress || myDuplex.remoteAddress; - myDuplex.remotePort = opts.remotePort || myDuplex.remotePort; + var reader = require('socket-pair').create(function (err, writer) { + if (err) { + reader.emit('error', err); + return; + } - 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.end(); + process.nextTick(function () { + socket.unshift(opts.firstChunk); + }); + + socket.pipe(writer); + writer.pipe(socket); + + socket.on('error', function (err) { + console.log('wrapped TLS socket error', err); + reader.emit('error', err); + }); + writer.on('error', function (err) { + console.error('socket-pair writer error', err); + // If the writer had an error the reader probably did too, and I don't think we'll + // get much out of emitting this on the original socket, so logging is enough. + }); }); - process.nextTick(function () { - // this must happen after the socket is emitted to the next in the chain, - // but before any more data comes in via the network - socket.unshift(opts.firstChunk); + // We can't set these properties the normal way because there is a getter without a setter, + // but we can use defineProperty. We reuse the descriptor even though we will be manipulating + // it because we will only ever set the value and we set it every time. + var descriptor = {enumerable: true, configurable: true, writable: true}; + addressNames.forEach(function (name) { + descriptor.value = opts[name] || extractSocketProp(socket, name); + Object.defineProperty(reader, name, descriptor); }); - return myDuplex; + return reader; } var le = greenlock.create({ From df3a818914d7204ec52a0162b2b7df9030909516 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 17 May 2017 14:06:24 -0600 Subject: [PATCH 03/15] reduced code duplication for proxying --- lib/goldilocks.js | 36 ++++------------------ lib/modules/http.js | 24 +-------------- lib/modules/tls.js | 51 ++++++++----------------------- lib/proxy-conn.js | 71 +++++++++++++++++++++++++++++++++++++++++++ lib/proxy-err-resp.js | 32 ------------------- lib/worker.js | 2 ++ 6 files changed, 93 insertions(+), 123 deletions(-) create mode 100644 lib/proxy-conn.js delete mode 100644 lib/proxy-err-resp.js diff --git a/lib/goldilocks.js b/lib/goldilocks.js index 709ade1..e938bfd 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -80,40 +80,16 @@ module.exports.create = function (deps, config) { } function createTcpForwarder(mod) { - var destination = mod.address.split(':'); - var connected = false; - return function (conn) { - var newConn = deps.net.createConnection({ - port: destination[1] - , host: destination[0] || '127.0.0.1' + var newConnOpts = require('./domain-utils').separatePort(mod.address); - , remoteFamily: conn.remoteFamily - , remoteAddress: conn.remoteAddress - , remotePort: conn.remotePort - }, function () { - connected = true; - - newConn.pipe(conn); - conn.pipe(newConn); - }); - - // Not sure how to effectively report this to the user or client, but we need to listen - // for the event to prevent it from crashing us. - newConn.on('error', function (err) { - if (connected) { - console.error('TCP forward remote error', err); - conn.end(); - } else { - console.log('TCP forward connection error', err); - require('./proxy-err-resp').sendBadGateway(conn, err, config.debug); - } - }); - conn.on('error', function (err) { - console.error('TCP forward client error', err); - newConn.end(); + ['remote', 'local'].forEach(function (end) { + ['Family', 'Address', 'Port'].forEach(function (name) { + newConnOpts[end+name] = conn[end+name]; + }); }); + deps.proxy(conn, newConnOpts); }; } diff --git a/lib/modules/http.js b/lib/modules/http.js index 593e867..9434e03 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -275,7 +275,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { return false; } - var connected = false; var newConnOpts = separatePort(mod.address); newConnOpts.servername = separatePort(headers.host).host; newConnOpts.data = opts.firstChunk; @@ -284,28 +283,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { newConnOpts.remoteAddress = opts.address || conn.remoteAddress; newConnOpts.remotePort = opts.port || conn.remotePort; - var newConn = deps.net.createConnection(newConnOpts, function () { - connected = true; - newConn.write(opts.firstChunk); - newConn.pipe(conn); - conn.pipe(newConn); - }); - - // Not sure how to effectively report this to the user or client, but we need to listen - // for the event to prevent it from crashing us. - newConn.on('error', function (err) { - if (connected) { - console.error('HTTP proxy remote error', err); - conn.end(); - } else { - require('../proxy-err-resp').sendBadGateway(conn, err, conf.debug); - } - }); - conn.on('error', function (err) { - console.error('HTTP proxy client error', err); - newConn.end(); - }); - + deps.proxy(conn, newConnOpts, opts.firstChunk); return true; } diff --git a/lib/modules/tls.js b/lib/modules/tls.js index db642fd..9c00ae1 100644 --- a/lib/modules/tls.js +++ b/lib/modules/tls.js @@ -186,49 +186,24 @@ module.exports.create = function (deps, config, netHandler) { }); function proxy(socket, opts, mod) { - var destination = mod.address.split(':'); - var connected = false; + var newConnOpts = require('../domain-utils').separatePort(mod.address); + newConnOpts.servername = opts.servername; + newConnOpts.data = opts.firstChunk; - var newConn = deps.net.createConnection({ - port: destination[1] - , host: destination[0] || '127.0.0.1' + newConnOpts.remoteFamily = opts.family || extractSocketProp(socket, 'remoteFamily'); + newConnOpts.remoteAddress = opts.address || extractSocketProp(socket, 'remoteAddress'); + newConnOpts.remotePort = opts.port || extractSocketProp(socket, 'remotePort'); - , servername: opts.servername - , data: opts.firstChunk - , remoteFamily: opts.family || extractSocketProp(socket, 'remoteFamily') - , remoteAddress: opts.address || extractSocketProp(socket, 'remoteAddress') - , remotePort: opts.port || extractSocketProp(socket, 'remotePort') - }, function () { - connected = true; - if (!opts.hyperPeek) { - newConn.write(opts.firstChunk); - } - newConn.pipe(socket); - socket.pipe(newConn); - }); - - // Not sure how to effectively report this to the user or client, but we need to listen - // for the event to prevent it from crashing us. - newConn.on('error', function (err) { - if (connected) { - console.error('TLS proxy remote error', err); - socket.end(); + deps.proxy(socket, newConnOpts, opts.firstChunk, function () { + // This function is called in the event of a connection error and should decrypt + // the socket so the proxy module can send a 502 HTTP response. + var tlsOpts = localhostCerts.mergeTlsOptions('localhost.daplie.me', {isServer: true}); + if (opts.hyperPeek) { + return new tls.TLSSocket(socket, tlsOpts); } else { - console.log('TLS proxy connection error', err); - var tlsOpts = localhostCerts.mergeTlsOptions('localhost.daplie.me', {isServer: true}); - var decrypted; - if (opts.hyperPeek) { - decrypted = new tls.TLSSocket(socket, tlsOpts); - } else { - decrypted = new tls.TLSSocket(wrapSocket(socket, opts), tlsOpts); - } - require('../proxy-err-resp').sendBadGateway(decrypted, err, config.debug); + return new tls.TLSSocket(wrapSocket(socket, opts), tlsOpts); } }); - socket.on('error', function (err) { - console.error('TLS proxy client error', err); - newConn.end(); - }); } function terminate(socket, opts) { diff --git a/lib/proxy-conn.js b/lib/proxy-conn.js new file mode 100644 index 0000000..476546c --- /dev/null +++ b/lib/proxy-conn.js @@ -0,0 +1,71 @@ +'use strict'; + +function getRespBody(err, debug) { + if (debug) { + return err.toString(); + } + + if (err.code === 'ECONNREFUSED') { + return 'The connection was refused. Most likely the service being connected to ' + + 'has stopped running or the configuration is wrong.'; + } + + return 'Bad Gateway: ' + err.code; +} + +function sendBadGateway(conn, err, debug) { + var body = getRespBody(err, debug); + + conn.write([ + 'HTTP/1.1 502 Bad Gateway' + , 'Date: ' + (new Date()).toUTCString() + , 'Connection: close' + , 'Content-Type: text/html' + , 'Content-Length: ' + body.length + , '' + , body + ].join('\r\n')); + conn.end(); +} + +module.exports.getRespBody = getRespBody; +module.exports.sendBadGateway = sendBadGateway; + +module.exports.create = function (deps, config) { + return function proxy(conn, newConnOpts, firstChunk, decrypt) { + var connected = false; + var newConn = deps.net.createConnection(newConnOpts, function () { + connected = true; + + if (firstChunk) { + newConn.write(firstChunk); + } + + newConn.pipe(conn); + conn.pipe(newConn); + }); + + newConn.on('error', function (err) { + if (connected) { + // Not sure how to report this to a user or a client. We can assume that some data + // has already been exchanged, so we can't really be sure what we can send in addition + // that wouldn't result in a parse error. + console.log('proxy remote error', err); + conn.end(); + } else { + console.log('proxy connection error', err); + if (decrypt) { + sendBadGateway(decrypt(conn), err, config.debug); + } else { + sendBadGateway(conn, err, config.debug); + } + } + }); + + // Listening for this largely to prevent uncaught exceptions. + conn.on('error', function (err) { + console.log('proxy client error', err); + newConn.end(); + }); + }; +}; diff --git a/lib/proxy-err-resp.js b/lib/proxy-err-resp.js deleted file mode 100644 index c2a35a8..0000000 --- a/lib/proxy-err-resp.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -function getRespBody(err, debug) { - if (debug) { - return err.toString(); - } - - if (err.code === 'ECONNREFUSED') { - return 'The connection was refused. Most likely the service being connected to ' - + 'has stopped running or the configuration is wrong.'; - } - - return 'Bad Gateway: ' + err.code; -} - -function sendBadGateway(conn, err, debug) { - var body = getRespBody(err, debug); - - conn.write([ - 'HTTP/1.1 502 Bad Gateway' - , 'Date: ' + (new Date()).toUTCString() - , 'Connection: close' - , 'Content-Type: text/html' - , 'Content-Length: ' + body.length - , '' - , body - ].join('\r\n')); - conn.end(); -} - -module.exports.getRespBody = getRespBody; -module.exports.sendBadGateway = sendBadGateway; diff --git a/lib/worker.js b/lib/worker.js index 79db594..23724cb 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -9,5 +9,7 @@ process.on('message', function (conf) { // HTTP proxying connection creation is not something we currently control. , net: require('net') }; + deps.proxy = require('./proxy-conn').create(deps, conf); + require('./goldilocks.js').create(deps, conf); }); From 47bcdcf2a6621c5b9839859c421b4b8af0ab9a6b Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 17 May 2017 18:43:44 -0600 Subject: [PATCH 04/15] added X-Forwarded header before HTTP proxy --- lib/modules/http.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/modules/http.js b/lib/modules/http.js index 9434e03..704f331 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -275,6 +275,30 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { return false; } + var index = opts.firstChunk.indexOf('\r\n\r\n'); + var body = opts.firstChunk.slice(index); + + var head = opts.firstChunk.slice(0, index).toString(); + var headLines = head.split('\r\n'); + // First strip any existing `X-Forwarded-*` headers (for security purposes?) + headLines = headLines.filter(function (line) { + return !/^x-forwarded/i.test(line); + }); + // Then add our own `X-Forwarded` headers at the end. + if (conf.http.trustProxy && headers['x-forwarded-proto']) { + headLines.push('X-Forwarded-Proto: ' + headers['x-forwarded-proto']); + } else { + headLines.push('X-Forwarded-Proto: ' + conn.encrypted ? 'https' : 'http'); + } + var proxyChain = (headers['x-forwarded-for'] || '').split(/ *, */).filter(Boolean); + proxyChain.push(opts.remoteAddress || opts.address || conn.remoteAddress); + headLines.push('X-Forwarded-For: ' + proxyChain.join(', ')); + headLines.push('X-Forwarded-Host: ' + headers.host); + // Then convert all of the head lines back into a header buffer. + head = Buffer.from(headLines.join('\r\n')); + + opts.firstChunk = Buffer.concat([head, body]); + var newConnOpts = separatePort(mod.address); newConnOpts.servername = separatePort(headers.host).host; newConnOpts.data = opts.firstChunk; From 27e818f41ae514ec326db5edd59d11fb3d028c0b Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 17 May 2017 19:16:45 -0600 Subject: [PATCH 05/15] started splitting http handling into multiple servers --- lib/modules/admin.js | 4 +- lib/modules/http.js | 107 ++++++++++++++++++------------------------- 2 files changed, 46 insertions(+), 65 deletions(-) diff --git a/lib/modules/admin.js b/lib/modules/admin.js index 7f5a81a..691fc4f 100644 --- a/lib/modules/admin.js +++ b/lib/modules/admin.js @@ -59,6 +59,6 @@ module.exports.create = function (deps, conf) { ] }); - /* device, addresses, cwd, http */ - return require('../app.js')(deps, conf, opts); + var app = require('../app.js')(deps, conf, opts); + return require('http').createServer(app); }; diff --git a/lib/modules/http.js b/lib/modules/http.js index 704f331..709bf28 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -4,7 +4,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { var PromiseA = require('bluebird'); var express = require('express'); var app = express(); - var adminApp = require('./admin').create(deps, conf); var domainMatches = require('../domain-utils').match; var separatePort = require('../domain-utils').separatePort; @@ -80,16 +79,28 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { }); } - function verifyHost(fullHost) { - var host = separatePort(fullHost).host; + // We handle both HTTPS and HTTP traffic on the same ports, and we want to redirect + // any unencrypted requests to the same port they came from unless it came in on + // the default HTTP port, in which case there wont be a port specified in the host. + var redirecters = {}; + var ipv4Re = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; + var ipv6Re = /^\[[0-9a-fA-F:]+\]$/; + function redirectHttps(req, res) { + var host = separatePort(req.headers.host); - if (host === 'localhost') { - return fullHost.replace(host, 'localhost.daplie.me'); + if (!redirecters[host.port]) { + redirecters[host.port] = require('redirect-https')({ port: host.port }); } + // localhost and IP addresses cannot have real SSL certs (and don't contain any useful + // info for redirection either), so we direct some hosts to either localhost.daplie.me + // or the "primary domain" ie the first manually specified domain. + if (host.host === 'localhost') { + req.headers.host = 'localhost.daplie.me' + (host.port ? ':'+host.port : ''); + } // Test for IPv4 and IPv6 addresses. These patterns will match some invalid addresses, // but since those still won't be valid domains that won't really be a problem. - if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) || /^\[[0-9a-fA-F:]+\]$/.test(host)) { + if (ipv4Re.test(host.host) || ipv6Re.test(host.host)) { if (!conf.http.primaryDomain) { (conf.http.modules || []).some(function (mod) { return mod.domains.some(function (domain) { @@ -100,48 +111,12 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { }); }); } - return fullHost.replace(host, conf.http.primaryDomain || host); + if (conf.http.primaryDomain) { + req.headers.host = conf.http.primaryDomain + (host.port ? ':'+host.port : ''); + } } - return fullHost; - } - - // We handle both HTTPS and HTTP traffic on the same ports, and we want to redirect - // any unencrypted requests to the same port they came from unless it came in on - // the default HTTP port, in which case there wont be a port specified in the host. - var redirecters = {}; - function redirectHttps(req, res, next) { - if (conf.http.allowInsecure) { - next(); - return; - } - - var port = separatePort(req.headers.host).port; - if (!redirecters[port]) { - redirecters[port] = require('redirect-https')({ - port: port - , trustProxy: conf.http.trustProxy - }); - } - - // localhost and IP addresses cannot have real SSL certs (and don't contain any useful - // info for redirection either), so we direct some hosts to either localhost.daplie.me - // or the "primary domain" ie the first manually specified domain. - req.headers.host = verifyHost(req.headers.host); - - redirecters[port](req, res, next); - } - - function handleAdmin(req, res, next) { - var admin = adminDomains.some(function (re) { - return re.test(req.headers.host); - }); - - if (admin) { - adminApp(req, res); - } else { - next(); - } + redirecters[host.port](req, res); } function respond404(req, res) { @@ -206,10 +181,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { }; } - app.use(greenlockMiddleware); - app.use(redirectHttps); - app.use(handleAdmin); - (conf.http.modules || []).forEach(function (mod) { if (mod.name === 'redirect') { app.use(createRedirectRoute(mod)); @@ -217,7 +188,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { else if (mod.name === 'static') { app.use(createStaticRoute(mod)); } - else { + else if (mod.name !== 'proxy') { console.warn('unknown HTTP module', mod); } }); @@ -226,7 +197,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { var server = require('http').createServer(app); - function handleHttp(conn, opts) { + function emitConnection(server, conn, opts) { server.emit('connection', conn); // We need to put back whatever data we read off to determine the connection was HTTP @@ -240,15 +211,21 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { return true; } + var acmeServer; function checkACME(conn, opts, headers) { if (headers.url.indexOf('/.well-known/acme-challenge/') !== 0) { return false; } - return handleHttp(conn, opts); + if (!acmeServer) { + acmeServer = require('http').createServer(greenlockMiddleware); + } + return emitConnection(acmeServer, conn, opts); } - function checkRedirect(conn, opts, headers) { + + var httpsRedirectServer; + function checkHttps(conn, opts, headers) { if (conf.http.allowInsecure || conn.encrypted) { return false; } @@ -256,16 +233,23 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { return false; } - return handleHttp(conn, opts); + if (!httpsRedirectServer) { + httpsRedirectServer = require('http').createServer(redirectHttps); + } + return emitConnection(httpsRedirectServer, conn, opts); } + var adminServer; function checkAdmin(conn, opts, headers) { var admin = adminDomains.some(function (re) { return re.test(headers.host); }); if (admin) { - return handleHttp(conn, opts); + if (!adminServer) { + adminServer = require('./admin').create(deps, conf); + } + return emitConnection(adminServer, conn, opts); } return false; } @@ -315,9 +299,9 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { var opts = conn.__opts; parseHeaders(conn, opts) .then(function (headers) { - if (checkACME(conn, opts, headers)) { return; } - if (checkRedirect(conn, opts, headers)) { return; } - if (checkAdmin(conn, opts, headers)) { return; } + if (checkACME(conn, opts, headers)) { return; } + if (checkHttps(conn, opts, headers)) { return; } + if (checkAdmin(conn, opts, headers)) { return; } var handled = (conf.http.modules || []).some(function (mod) { if (mod.name === 'proxy') { @@ -328,10 +312,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { return; } - server.emit('connection', conn); - process.nextTick(function () { - conn.unshift(opts.firstChunk); - }); + emitConnection(server, conn, opts); }) ; } From dbbae2311cec70813610bc8f77b2db7c12f87e7c Mon Sep 17 00:00:00 2001 From: tigerbot Date: Thu, 18 May 2017 11:58:10 -0600 Subject: [PATCH 06/15] moved HTTP redirection to the net layer --- lib/modules/http.js | 82 ++++++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/lib/modules/http.js b/lib/modules/http.js index 709bf28..5cbb5b8 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -124,36 +124,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { res.end('Not Found'); } - function createRedirectRoute(mod) { - // Escape any characters that (can) have special meaning in regular expression - // but that aren't the special characters we have interest in. - var from = mod.from.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&'); - // Then modify the characters we are interested in so they do what we want in - // the regular expression after being compiled. - from = from.replace(/\*/g, '(.*)'); - var fromRe = new RegExp('^' + from + '/?$'); - - return function (req, res, next) { - if (!moduleMatchesHost(req, mod)) { - next(); - return; - } - - var match = fromRe.exec(req.url); - if (!match) { - next(); - return; - } - - var to = mod.to; - match.slice(1).forEach(function (globMatch, index) { - to = to.replace(':'+(index+1), globMatch); - }); - res.writeHead(mod.status || 301, { 'Location': to }); - res.end(); - }; - } - function createStaticRoute(mod) { var getStaticApp, staticApp; if (/:hostname/.test(mod.root)) { @@ -182,13 +152,10 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { } (conf.http.modules || []).forEach(function (mod) { - if (mod.name === 'redirect') { - app.use(createRedirectRoute(mod)); - } - else if (mod.name === 'static') { + if (mod.name === 'static') { app.use(createStaticRoute(mod)); } - else if (mod.name !== 'proxy') { + else if (mod.name !== 'proxy' && mod.name !== 'redirect') { console.warn('unknown HTTP module', mod); } }); @@ -295,6 +262,48 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { return true; } + function checkRedirect(mod, conn, opts, headers) { + if (!moduleMatchesHost(headers, mod)) { + return false; + } + + if (!mod.fromRe || mod.fromRe.origSrc !== mod.from) { + // Escape any characters that (can) have special meaning in regular expression + // but that aren't the special characters we have interest in. + var from = mod.from.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&'); + // Then modify the characters we are interested in so they do what we want in + // the regular expression after being compiled. + from = from.replace(/\*/g, '(.*)'); + var fromRe = new RegExp('^' + from + '/?$'); + fromRe.origSrc = mod.from; + // We don't want this property showing up in the actual config file or the API, + // so we define it this way so it's not enumberable. + Object.defineProperty(mod, 'fromRe', {value: fromRe, configurable: true}); + } + + var match = mod.fromRe.exec(headers.url); + if (!match) { + return false; + } + + var to = mod.to; + match.slice(1).forEach(function (globMatch, index) { + to = to.replace(':'+(index+1), globMatch); + }); + var status = mod.status || 301; + var code = require('http').STATUS_CODES[status] || 'Unknown'; + + conn.end([ + 'HTTP/1.1 ' + status + ' ' + code + , 'Date: ' + (new Date()).toUTCString() + , 'Location: ' + to + , 'Content-Length: 0' + , '' + , '' + ].join('\r\n')); + return true; + } + function handleConnection(conn) { var opts = conn.__opts; parseHeaders(conn, opts) @@ -307,6 +316,9 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { if (mod.name === 'proxy') { return checkProxy(mod, conn, opts, headers); } + else if (mod.name === 'redirect') { + return checkRedirect(mod, conn, opts, headers); + } }); if (handled) { return; From aa28a72f3f80891d9cd7b533bde5de65bbf1a0b3 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Thu, 18 May 2017 14:09:02 -0600 Subject: [PATCH 07/15] moved HTTP static file detection to net layer --- lib/modules/http.js | 142 ++++++++++++++++++++++++++------------------ 1 file changed, 83 insertions(+), 59 deletions(-) diff --git a/lib/modules/http.js b/lib/modules/http.js index 5cbb5b8..d5fa572 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -2,8 +2,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { var PromiseA = require('bluebird'); - var express = require('express'); - var app = express(); + var statAsync = PromiseA.promisify(require('fs').stat); var domainMatches = require('../domain-utils').match; var separatePort = require('../domain-utils').separatePort; @@ -119,51 +118,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { redirecters[host.port](req, res); } - function respond404(req, res) { - res.writeHead(404); - res.end('Not Found'); - } - - function createStaticRoute(mod) { - var getStaticApp, staticApp; - if (/:hostname/.test(mod.root)) { - staticApp = {}; - getStaticApp = function (hostname) { - if (!staticApp[hostname]) { - staticApp[hostname] = express.static(mod.root.replace(':hostname', hostname)); - } - return staticApp[hostname]; - }; - } - else { - staticApp = express.static(mod.root); - getStaticApp = function () { - return staticApp; - }; - } - - return function (req, res, next) { - if (moduleMatchesHost(req, mod)) { - getStaticApp(separatePort(req.headers.host).host)(req, res, next); - } else { - next(); - } - }; - } - - (conf.http.modules || []).forEach(function (mod) { - if (mod.name === 'static') { - app.use(createStaticRoute(mod)); - } - else if (mod.name !== 'proxy' && mod.name !== 'redirect') { - console.warn('unknown HTTP module', mod); - } - }); - - app.use(respond404); - - var server = require('http').createServer(app); - function emitConnection(server, conn, opts) { server.emit('connection', conn); @@ -190,7 +144,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { return emitConnection(acmeServer, conn, opts); } - var httpsRedirectServer; function checkHttps(conn, opts, headers) { if (conf.http.allowInsecure || conn.encrypted) { @@ -297,6 +250,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { 'HTTP/1.1 ' + status + ' ' + code , 'Date: ' + (new Date()).toUTCString() , 'Location: ' + to + , 'Connection: close' , 'Content-Length: 0' , '' , '' @@ -304,6 +258,57 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { return true; } + var staticServer; + var staticHandlers = {}; + function serveStatic(req, res) { + var rootDir = req.connection.rootDir; + + if (!staticHandlers[rootDir]) { + staticHandlers[rootDir] = require('express').static(rootDir, { fallthrough: false }); + } + + staticHandlers[rootDir](req, res, function (err) { + if (err) { + res.statusCode = err.statusCode; + } else { + res.statusCode = 404; + } + res.setHeader('Content-Type', 'text/html'); + + if (res.statusCode === 404) { + res.end('File Not Found'); + } else { + res.end(require('http').STATUS_CODES[res.statusCode]); + } + }); + } + function checkStatic(mod, conn, opts, headers) { + if (!moduleMatchesHost(headers, mod)) { + return false; + } + + var rootDir = mod.root.replace(':hostname', separatePort(headers.host).host); + return statAsync(rootDir) + .then(function (stats) { + if (!stats || !stats.isDirectory()) { + return false; + } + + if (!staticServer) { + staticServer = require('http').createServer(serveStatic); + } + conn.rootDir = rootDir; + return emitConnection(staticServer, conn, opts); + }) + .catch(function (err) { + if (err.code !== 'ENOENT') { + console.warn('errored stating', rootDir, 'for serving static files', err); + } + return false; + }) + ; + } + function handleConnection(conn) { var opts = conn.__opts; parseHeaders(conn, opts) @@ -312,19 +317,38 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { if (checkHttps(conn, opts, headers)) { return; } if (checkAdmin(conn, opts, headers)) { return; } - var handled = (conf.http.modules || []).some(function (mod) { - if (mod.name === 'proxy') { - return checkProxy(mod, conn, opts, headers); - } - else if (mod.name === 'redirect') { - return checkRedirect(mod, conn, opts, headers); + var prom = PromiseA.resolve(false); + (conf.http.modules || []).forEach(function (mod) { + prom = prom.then(function (handled) { + if (handled) { + return handled; + } + if (mod.name === 'proxy') { + return checkProxy(mod, conn, opts, headers); + } + if (mod.name === 'redirect') { + return checkRedirect(mod, conn, opts, headers); + } + if (mod.name === 'static') { + return checkStatic(mod, conn, opts, headers); + } + console.warn('unknown HTTP module found', mod); + }); + }); + + prom.then(function (handled) { + if (!handled) { + conn.end([ + 'HTTP/1.1 404 Not Found' + , 'Date: ' + (new Date()).toUTCString() + , 'Content-Type: text/html' + , 'Content-Length: 9' + , 'Connection: close' + , '' + , 'Not Found' + ].join('\r\n')); } }); - if (handled) { - return; - } - - emitConnection(server, conn, opts); }) ; } From 5bbf57a57a6be8aa62dff3bbbc2523865b49ee31 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Thu, 18 May 2017 14:14:44 -0600 Subject: [PATCH 08/15] tweaked proxy behavior on error/close --- lib/proxy-conn.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/proxy-conn.js b/lib/proxy-conn.js index 476546c..b9a704c 100644 --- a/lib/proxy-conn.js +++ b/lib/proxy-conn.js @@ -45,13 +45,16 @@ module.exports.create = function (deps, config) { conn.pipe(newConn); }); + // Listening for this largely to prevent uncaught exceptions. + conn.on('error', function (err) { + console.log('proxy client error', err); + }); newConn.on('error', function (err) { if (connected) { // Not sure how to report this to a user or a client. We can assume that some data // has already been exchanged, so we can't really be sure what we can send in addition // that wouldn't result in a parse error. console.log('proxy remote error', err); - conn.end(); } else { console.log('proxy connection error', err); if (decrypt) { @@ -62,10 +65,12 @@ module.exports.create = function (deps, config) { } }); - // Listening for this largely to prevent uncaught exceptions. - conn.on('error', function (err) { - console.log('proxy client error', err); - newConn.end(); + // Make sure that once one side closes, no I/O activity will happen on the other side. + conn.on('close', function () { + newConn.destroy(); + }); + newConn.on('close', function () { + conn.destroy(); }); }; }; From 73d339660940335083d4cdb457e1a878fe1b4fd6 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 23 May 2017 12:21:24 -0600 Subject: [PATCH 09/15] removed some unused file and cleaned package.json I used git grep to find all require statements coupled with sed, sort and uniq to create a list of all node modules actually required in our code, then went through package.json to make the list match our dependencies. --- lib/tunnel.js | 144 --------------------------------------------- package.json | 11 ++-- stages/01-serve.js | 23 -------- 3 files changed, 4 insertions(+), 174 deletions(-) delete mode 100644 lib/tunnel.js delete mode 100644 stages/01-serve.js diff --git a/lib/tunnel.js b/lib/tunnel.js deleted file mode 100644 index a4ea58c..0000000 --- a/lib/tunnel.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict'; - -module.exports.create = function (opts, servers) { - // servers = { plainserver, server } - var Oauth3 = require('oauth3-cli'); - var Tunnel = require('daplie-tunnel').create({ - Oauth3: Oauth3 - , PromiseA: opts.PromiseA - , CLI: { - init: function (rs, ws/*, state, options*/) { - // noop - return ws; - } - } - }).Tunnel; - var stunnel = require('stunnel'); - var killcount = 0; - - /* - var Dup = { - write: function (chunk, encoding, cb) { - this.__my_socket.push(chunk, encoding); - cb(); - } - , read: function (size) { - var x = this.__my_socket.read(size); - if (x) { this.push(x); } - } - , setTimeout: function () { - console.log('TODO implement setTimeout on Duplex'); - } - }; - - var httpServer = require('http').createServer(function (req, res) { - console.log('req.socket.encrypted', req.socket.encrypted); - res.end('Hello, tunneled World!'); - }); - - var tlsServer = require('tls').createServer(opts.httpsOptions, function (tlsSocket) { - console.log('tls connection'); - // things get a little messed up here - httpServer.emit('connection', tlsSocket); - - // try again - //servers.server.emit('connection', tlsSocket); - }); - */ - - process.on('SIGINT', function () { - killcount += 1; - console.log('[quit] closing http and https servers'); - if (killcount >= 3) { - process.exit(1); - } - if (servers.server) { - servers.server.close(); - } - if (servers.insecureServer) { - servers.insecureServer.close(); - } - }); - - return Tunnel.token({ - refreshToken: opts.refreshToken - , email: opts.email - , domains: opts.sites.map(function (site) { - return site.name; - }) - , device: { hostname: opts.devicename || opts.device } - }).then(function (result) { - // { jwt, tunnelUrl } - var locals = []; - opts.sites.map(function (site) { - locals.push({ - protocol: 'https' - , hostname: site.name - , port: opts.port - }); - locals.push({ - protocol: 'http' - , hostname: site.name - , port: opts.insecurePort || opts.port - }); - }); - return stunnel.connect({ - token: result.jwt - , stunneld: result.tunnelUrl - // XXX TODO BUG // this is just for testing - , insecure: /*opts.insecure*/ true - , locals: locals - // a simple passthru is proving to not be so simple - , net: require('net') /* - { - createConnection: function (info, cb) { - // data is the hello packet / first chunk - // info = { data, servername, port, host, remoteAddress: { family, address, port } } - - var myDuplex = new (require('stream').Duplex)(); - var myDuplex2 = new (require('stream').Duplex)(); - // duplex = { write, push, end, events: [ 'readable', 'data', 'error', 'end' ] }; - - myDuplex2.__my_socket = myDuplex; - myDuplex.__my_socket = myDuplex2; - - myDuplex2._write = Dup.write; - myDuplex2._read = Dup.read; - - myDuplex._write = Dup.write; - myDuplex._read = Dup.read; - - myDuplex.remoteFamily = info.remoteFamily; - myDuplex.remoteAddress = info.remoteAddress; - myDuplex.remotePort = info.remotePort; - - // socket.local{Family,Address,Port} - myDuplex.localFamily = 'IPv4'; - myDuplex.localAddress = '127.0.01'; - myDuplex.localPort = info.port; - - myDuplex.setTimeout = Dup.setTimeout; - - // this doesn't seem to work so well - //servers.server.emit('connection', myDuplex); - - // try a little more manual wrapping / unwrapping - var firstByte = info.data[0]; - if (firstByte < 32 || firstByte >= 127) { - tlsServer.emit('connection', myDuplex); - } - else { - httpServer.emit('connection', myDuplex); - } - - if (cb) { - process.nextTick(cb); - } - - return myDuplex2; - } - } - //*/ - }); - }); -}; diff --git a/package.json b/package.json index cb2446c..67be583 100644 --- a/package.json +++ b/package.json @@ -41,29 +41,26 @@ "bluebird": "^3.4.6", "body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1", "commander": "^2.9.0", - "daplie-tunnel": "git+https://git.daplie.com/Daplie/daplie-cli-tunnel.git#master", - "ddns-cli": "git+https://git.daplie.com/Daplie/node-ddns-client.git#master", "express": "git+https://github.com/expressjs/express.git#4.x", "finalhandler": "^0.4.0", "greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master", - "greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master", - "httpolyglot": "^0.1.1", "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", "ipify": "^1.1.0", "js-yaml": "^3.8.3", + "jsonwebtoken": "^7.4.0", "le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master", "le-challenge-fs": "git+https://git.daplie.com/Daplie/le-challenge-webroot.git#master", "le-challenge-sni": "^2.0.1", - "livereload": "^0.6.0", + "le-store-certbot": "git+https://git.daplie.com/Daplie/le-store-certbot.git#master", "localhost.daplie.me-certificates": "^1.3.0", - "minimist": "^1.1.1", - "oauth3-cli": "git+https://git.daplie.com/OAuth3/oauth3-cli.git#master", "recase": "git+https://git.daplie.com/coolaj86/recase-js.git#v1.0.4", "redirect-https": "^1.1.0", + "request": "^2.81.0", "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", + "sni": "^1.0.0", "socket-pair": "^1.0.0", "stream-pair": "^1.0.3", "stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1" diff --git a/stages/01-serve.js b/stages/01-serve.js deleted file mode 100644 index 8f92791..0000000 --- a/stages/01-serve.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -var https = require('httpolyglot'); -var httpsOptions = require('localhost.daplie.me-certificates').merge({}); -var httpsPort = 8443; -var redirectApp = require('redirect-https')({ - port: httpsPort -}); - -var server = https.createServer(httpsOptions); - -server.on('request', function (req, res) { - if (!req.socket.encrypted) { - redirectApp(req, res); - return; - } - - res.end("Hello, Encrypted World!"); -}); - -server.listen(httpsPort, function () { - console.log('https://' + 'localhost.daplie.me' + (443 === httpsPort ? ':' : ':' + httpsPort)); -}); From 1f8e44947fd2a9048f0108dde63a27588c1a9b34 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 23 May 2017 16:23:43 -0600 Subject: [PATCH 10/15] added simple mDNS responder --- bin/goldilocks.js | 3 + lib/goldilocks.js | 4 ++ lib/mdns.js | 144 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 152 insertions(+) create mode 100644 lib/mdns.js diff --git a/bin/goldilocks.js b/bin/goldilocks.js index b19d5e0..205074c 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -73,6 +73,9 @@ function readConfigAndRun(args) { if (!config.dns) { config.dns = { modules: { name: 'proxy', port: 3053 } }; } + if (!config.mdns) { + config.mdns = { port: 5353, broadcast: '224.0.0.251', ttl: 300 }; + } if (!config.tcp) { config.tcp = {}; } diff --git a/lib/goldilocks.js b/lib/goldilocks.js index e938bfd..be18c92 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -225,5 +225,9 @@ module.exports.create = function (deps, config) { } } + if (!config.mdns.disabled) { + require('./mdns').start(deps, config); + } + return PromiseA.all(listenPromises); }; diff --git a/lib/mdns.js b/lib/mdns.js new file mode 100644 index 0000000..cc08ccd --- /dev/null +++ b/lib/mdns.js @@ -0,0 +1,144 @@ +'use strict'; + +var PromiseA = require('bluebird'); +var fs = PromiseA.promisifyAll(require('fs')); +var idFilename = require('path').join(__dirname, '..', 'var', 'mdns-id'); +var queryName = '_cloud._tcp.local'; + +var randomId = { + get: function () { + return fs.readFileAsync(idFilename) + .catch(function (err) { + if (err.code !== 'ENOENT') { + return PromiseA.reject(err); + } + var id = require('crypto').randomBytes(5).toString('hex'); + return randomId.set(id); + }); + } + +, set: function (value) { + return fs.writeFileAsync(idFilename, value) + .then(function () { + return value; + }); + } +}; + +function createResponse(name, packet, ttl) { + var rpacket = { + header: { + id: packet.header.id + , qr: 1 + , opcode: 0 + , aa: 1 + , tc: 0 + , rd: 0 + , ra: 0 + , res1: 0 + , res2: 0 + , res3: 0 + , rcode: 0 + , } + , question: packet.question + , answer: [] + , authority: [] + , additional: [] + , edns_options: [] + }; + + rpacket.answer.push({ + name: queryName + , typeName: 'PTR' + , ttl: ttl + , className: 'IN' + , data: name + '.' + queryName + }); + + var ifaces = require('./local-ip').find(); + Object.keys(ifaces).forEach(function (iname) { + var iface = ifaces[iname]; + + iface.ipv4.forEach(function (addr) { + rpacket.additional.push({ + name: name + '.local' + , typeName: 'A' + , ttl: ttl + , className: 'IN' + , address: addr.address + }); + }); + + iface.ipv6.forEach(function (addr) { + rpacket.additional.push({ + name: name + '.local' + , typeName: 'AAAA' + , ttl: ttl + , className: 'IN' + , address: addr.address + }); + }); + }); + + rpacket.additional.push({ + name: name + '.' + queryName + , typeName: 'SRV' + , ttl: ttl + , className: 'IN' + , priority: 1 + , weight: 0 + , port: 443 + , target: name + ".local" + }); + rpacket.additional.push({ + name: name + '._device-info._tcp.local' + , typeName: 'TXT' + , ttl: ttl + , className: 'IN' + , data: ["model=CloudHome1,1", "dappsvers=1"] + }); + + return require('dns-suite').DNSPacket.write(rpacket); +} + +module.exports.start = function (deps, config) { + var socket = require('dgram').createSocket({ type: 'udp4', reuseAddr: true }); + var dns = require('dns-suite'); + + socket.on('message', function (message, rinfo) { + // console.log('Received %d bytes from %s:%d', message.length, rinfo.address, rinfo.port); + + var packet; + try { + packet = dns.DNSPacket.parse(message); + } + catch (er) { + // `dns-suite` actually errors on a lot of the packets floating around in our network, + // so don't bother logging any errors. (We still use `dns-suite` because unlike `dns-js` + // it can successfully craft the one packet we want to send.) + return; + } + + // Only respond to queries. + if (packet.header.qr !== 0) { + return; + } + // Only respond if they were asking for cloud devices. + if (packet.question.length !== 1 || packet.question[0].name !== queryName) { + return; + } + + randomId.get().then(function (name) { + var resp = createResponse(name, packet, config.mdns.ttl); + socket.send(resp, config.mdns.port, config.mdns.broadcast); + }); + }); + + socket.bind(config.mdns.port, function () { + var addr = this.address(); + console.log('bound on UDP %s:%d for mDNS', addr.address, addr.port); + + socket.setBroadcast(true); + socket.addMembership(config.mdns.broadcast); + }); +}; diff --git a/package.json b/package.json index 67be583..0f2629a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "bluebird": "^3.4.6", "body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1", "commander": "^2.9.0", + "dns-suite": "git+https://git@git.daplie.com:Daplie/dns-suite#v1", "express": "git+https://github.com/expressjs/express.git#4.x", "finalhandler": "^0.4.0", "greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master", From 1e3021c66928a5c9f72c42ce36c237b919976060 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 23 May 2017 18:26:03 -0600 Subject: [PATCH 11/15] added ability to scope config by domain (issue #25) --- goldilocks.example.yml | 16 +++++++ lib/modules/http.js | 98 ++++++++++++++++++++++++++++-------------- lib/modules/tls.js | 40 +++++++++++------ 3 files changed, 110 insertions(+), 44 deletions(-) diff --git a/goldilocks.example.yml b/goldilocks.example.yml index 80f1283..54e128b 100644 --- a/goldilocks.example.yml +++ b/goldilocks.example.yml @@ -10,6 +10,12 @@ tcp: address: '127.0.0.1:8022' tls: + domains: + - names: + - localhost.gamma.daplie.me + modules: + - name: proxy + address: '127.0.0.1:6443' modules: - name: proxy domains: @@ -21,6 +27,16 @@ http: trust_proxy: true allow_insecure: false primary_domain: localhost.foo.daplie.me + domains: + - names: + - localhost.baz.daplie.me + modules: + - name: redirect + from: /nowhere/in/particular + to: /just/an/example + - name: proxy + address: '127.0.0.1:3001' + modules: - name: redirect domains: diff --git a/lib/modules/http.js b/lib/modules/http.js index d5fa572..c6f2b1b 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -70,14 +70,43 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { }); } - function moduleMatchesHost(req, mod) { + function hostMatchesDomains(req, domains) { var host = separatePort((req.headers || req).host).host; - return mod.domains.some(function (pattern) { + return domains.some(function (pattern) { return domainMatches(pattern, host); }); } + function determinePrimaryHost() { + var result; + if (Array.isArray(conf.http.domains)) { + conf.http.domains.some(function (dom) { + return dom.names.some(function (domain) { + if (domain[0] !== '*') { + result = domain; + return true; + } + }); + }); + } + if (result) { + return result; + } + + if (Array.isArray(conf.http.modules)) { + conf.http.modules.some(function (mod) { + return mod.domains.some(function (domain) { + if (domain[0] !== '*') { + result = domain; + return true; + } + }); + }); + } + return result; + } + // We handle both HTTPS and HTTP traffic on the same ports, and we want to redirect // any unencrypted requests to the same port they came from unless it came in on // the default HTTP port, in which case there wont be a port specified in the host. @@ -100,18 +129,14 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { // Test for IPv4 and IPv6 addresses. These patterns will match some invalid addresses, // but since those still won't be valid domains that won't really be a problem. if (ipv4Re.test(host.host) || ipv6Re.test(host.host)) { - if (!conf.http.primaryDomain) { - (conf.http.modules || []).some(function (mod) { - return mod.domains.some(function (domain) { - if (domain[0] !== '*') { - conf.http.primaryDomain = domain; - return true; - } - }); - }); - } + var dest; if (conf.http.primaryDomain) { - req.headers.host = conf.http.primaryDomain + (host.port ? ':'+host.port : ''); + dest = conf.http.primaryDomain; + } else { + dest = determinePrimaryHost(); + } + if (dest) { + req.headers.host = dest + (host.port ? ':'+host.port : ''); } } @@ -175,10 +200,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { } function checkProxy(mod, conn, opts, headers) { - if (!moduleMatchesHost(headers, mod)) { - return false; - } - var index = opts.firstChunk.indexOf('\r\n\r\n'); var body = opts.firstChunk.slice(index); @@ -216,10 +237,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { } function checkRedirect(mod, conn, opts, headers) { - if (!moduleMatchesHost(headers, mod)) { - return false; - } - if (!mod.fromRe || mod.fromRe.origSrc !== mod.from) { // Escape any characters that (can) have special meaning in regular expression // but that aren't the special characters we have interest in. @@ -283,10 +300,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { }); } function checkStatic(mod, conn, opts, headers) { - if (!moduleMatchesHost(headers, mod)) { - return false; - } - var rootDir = mod.root.replace(':hostname', separatePort(headers.host).host); return statAsync(rootDir) .then(function (stats) { @@ -309,6 +322,12 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { ; } + var moduleChecks = { + proxy: checkProxy + , redirect: checkRedirect + , static: checkStatic + }; + function handleConnection(conn) { var opts = conn.__opts; parseHeaders(conn, opts) @@ -318,19 +337,34 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { if (checkAdmin(conn, opts, headers)) { return; } var prom = PromiseA.resolve(false); + (conf.http.domains || []).forEach(function (dom) { + prom = prom.then(function (handled) { + if (handled) { + return handled; + } + if (!hostMatchesDomains(headers, dom.names)) { + return false; + } + + return dom.modules.some(function (mod) { + if (moduleChecks[mod.name]) { + return moduleChecks[mod.name](mod, conn, opts, headers); + } + console.warn('unknown HTTP module under domains', dom.names.join(','), mod); + }); + }); + }); (conf.http.modules || []).forEach(function (mod) { prom = prom.then(function (handled) { if (handled) { return handled; } - if (mod.name === 'proxy') { - return checkProxy(mod, conn, opts, headers); + if (!hostMatchesDomains(headers, mod.domains)) { + return false; } - if (mod.name === 'redirect') { - return checkRedirect(mod, conn, opts, headers); - } - if (mod.name === 'static') { - return checkStatic(mod, conn, opts, headers); + + if (moduleChecks[mod.name]) { + return moduleChecks[mod.name](mod, conn, opts, headers); } console.warn('unknown HTTP module found', mod); }); diff --git a/lib/modules/tls.js b/lib/modules/tls.js index 9c00ae1..a93c1da 100644 --- a/lib/modules/tls.js +++ b/lib/modules/tls.js @@ -204,6 +204,7 @@ module.exports.create = function (deps, config, netHandler) { return new tls.TLSSocket(wrapSocket(socket, opts), tlsOpts); } }); + return true; } function terminate(socket, opts) { @@ -244,30 +245,45 @@ module.exports.create = function (deps, config, netHandler) { return; } - var handled = (config.tls.modules || []).some(function (mod) { - var relevant = mod.domains.some(function (pattern) { + function checkModule(mod) { + if (mod.name === 'proxy') { + return proxy(socket, opts, mod); + } + if (mod.name !== 'acme') { + console.error('saw unknown TLS module', mod); + } + } + + var handled = (config.tls.domains || []).some(function (dom) { + var relevant = dom.names.some(function (pattern) { return domainMatches(pattern, opts.servername); }); if (!relevant) { return false; } - if (mod.name === 'proxy') { - proxy(socket, opts, mod); - } - else { - console.error('saw unknown TLS module', mod); + return dom.modules.some(checkModule); + }); + if (handled) { + return; + } + + handled = (config.tls.modules || []).some(function (mod) { + var relevant = mod.domains.some(function (pattern) { + return domainMatches(pattern, opts.servername); + }); + if (!relevant) { return false; } - - return true; + return checkModule(mod); }); + if (handled) { + return; + } // TODO: figure out all of the domains that the other modules intend to handle, and only // terminate those ones, closing connections for all others. - if (!handled) { - terminate(socket, opts); - } + terminate(socket, opts); } return { From be67f04afa008f44a644a0e5e266be054fe897f1 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 24 May 2017 11:42:17 -0600 Subject: [PATCH 12/15] added the mDNS options to the example config --- bin/goldilocks.js | 8 +++++--- goldilocks.example.yml | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bin/goldilocks.js b/bin/goldilocks.js index 205074c..e853336 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -73,9 +73,11 @@ function readConfigAndRun(args) { if (!config.dns) { config.dns = { modules: { name: 'proxy', port: 3053 } }; } - if (!config.mdns) { - config.mdns = { port: 5353, broadcast: '224.0.0.251', ttl: 300 }; - } + // Use Object.assign to add any properties needed but not defined in the mdns config. + // It will first copy the defaults into an empty object, then copy any real config over that. + var mdnsDefaults = { port: 5353, broadcast: '224.0.0.251', ttl: 300 }; + config.mdns = Object.assign({}, mdnsDefaults, config.mdns || {}); + if (!config.tcp) { config.tcp = {}; } diff --git a/goldilocks.example.yml b/goldilocks.example.yml index 54e128b..7836148 100644 --- a/goldilocks.example.yml +++ b/goldilocks.example.yml @@ -52,3 +52,9 @@ http: domains: - '*.localhost.daplie.me' root: '/srv/www/:hostname' + +mdns: + disabled: false + port: 5353 + broadcast: '224.0.0.251' + ttl: 300 From 21a77ad10a52b81bdf1637d21274f7bc11de66f0 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 24 May 2017 13:05:37 -0600 Subject: [PATCH 13/15] added way to specify proxy destination --- bin/goldilocks.js | 4 ++-- goldilocks.example.yml | 5 +++-- lib/goldilocks.js | 26 ++++++++++++++++++++------ lib/modules/http.js | 4 +++- lib/modules/tls.js | 4 +++- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/bin/goldilocks.js b/bin/goldilocks.js index e853336..28d33c0 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -71,7 +71,7 @@ function readConfigAndRun(args) { config = recase.camelCopy(config); if (!config.dns) { - config.dns = { modules: { name: 'proxy', port: 3053 } }; + config.dns = { modules: [{ name: 'proxy', port: 3053 }] }; } // Use Object.assign to add any properties needed but not defined in the mdns config. // It will first copy the defaults into an empty object, then copy any real config over that. @@ -82,7 +82,7 @@ function readConfigAndRun(args) { config.tcp = {}; } if (!config.http) { - config.http = { modules: { name: 'proxy', port: 3000 } }; + config.http = { modules: [{ name: 'proxy', domains: ['*'], port: 3000 }] }; } if (!config.tls) { console.log("TODO: tls: { modules: { name: 'acme', email: 'foo@bar.com', domains: [ '*' ] } }"); diff --git a/goldilocks.example.yml b/goldilocks.example.yml index 7836148..31d4f72 100644 --- a/goldilocks.example.yml +++ b/goldilocks.example.yml @@ -35,7 +35,7 @@ http: from: /nowhere/in/particular to: /just/an/example - name: proxy - address: '127.0.0.1:3001' + port: 3001 modules: - name: redirect @@ -47,7 +47,8 @@ http: - name: proxy domains: - localhost.daplie.me - address: '127.0.0.1:4000' + host: locahost + port: 4000 - name: static domains: - '*.localhost.daplie.me' diff --git a/lib/goldilocks.js b/lib/goldilocks.js index be18c92..ae6c552 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -74,22 +74,36 @@ module.exports.create = function (deps, config) { } function dnsListener(msg) { - var dgram = require('dgram'); - var socket = dgram.createSocket('udp4'); - socket.send(msg, config.dns.proxy.port, config.dns.proxy.address || '127.0.0.1'); + if (!Array.isArray(config.dns.modules)) { + return; + } + var socket = require('dgram').createSocket('udp4'); + config.dns.modules.forEach(function (mod) { + if (mod.name !== 'proxy') { + console.warn('found bad DNS module', mod); + return; + } + var dest = require('./domain-utils').separatePort(mod.address || ''); + dest.port = dest.port || mod.port; + dest.host = dest.host || mod.host || 'localhost'; + socket.send(msg, dest.port, dest.host); + }); } function createTcpForwarder(mod) { - return function (conn) { - var newConnOpts = require('./domain-utils').separatePort(mod.address); + var dest = require('./domain-utils').separatePort(mod.address || ''); + dest.port = dest.port || mod.port; + dest.host = dest.host || mod.host || 'localhost'; + return function (conn) { + var newConnOpts = {}; ['remote', 'local'].forEach(function (end) { ['Family', 'Address', 'Port'].forEach(function (name) { newConnOpts[end+name] = conn[end+name]; }); }); - deps.proxy(conn, newConnOpts); + deps.proxy(conn, Object.assign({}, dest, newConnOpts)); }; } diff --git a/lib/modules/http.js b/lib/modules/http.js index c6f2b1b..6d78a58 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -224,7 +224,9 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { opts.firstChunk = Buffer.concat([head, body]); - var newConnOpts = separatePort(mod.address); + var newConnOpts = separatePort(mod.address || ''); + newConnOpts.port = newConnOpts.port || mod.port; + newConnOpts.host = newConnOpts.host || mod.host || 'localhost'; newConnOpts.servername = separatePort(headers.host).host; newConnOpts.data = opts.firstChunk; diff --git a/lib/modules/tls.js b/lib/modules/tls.js index a93c1da..34ad6fe 100644 --- a/lib/modules/tls.js +++ b/lib/modules/tls.js @@ -186,7 +186,9 @@ module.exports.create = function (deps, config, netHandler) { }); function proxy(socket, opts, mod) { - var newConnOpts = require('../domain-utils').separatePort(mod.address); + var newConnOpts = require('../domain-utils').separatePort(mod.address || ''); + newConnOpts.port = newConnOpts.port || mod.port; + newConnOpts.host = newConnOpts.host || mod.host || 'localhost'; newConnOpts.servername = opts.servername; newConnOpts.data = opts.firstChunk; From 3633c7570bbd9bb6b1b26d4e635765ef05648d1d Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 24 May 2017 18:16:01 -0600 Subject: [PATCH 14/15] added support for different ACME config for different domains --- goldilocks.example.yml | 22 +++++++ lib/modules/tls.js | 129 +++++++++++++++++++++++++---------------- 2 files changed, 100 insertions(+), 51 deletions(-) diff --git a/goldilocks.example.yml b/goldilocks.example.yml index 31d4f72..c593118 100644 --- a/goldilocks.example.yml +++ b/goldilocks.example.yml @@ -10,18 +10,40 @@ tcp: address: '127.0.0.1:8022' tls: + acme: + email: 'joe.shmoe@example.com' + server: 'https://acme-staging.api.letsencrypt.org/directory' + challenge_type: 'http-01' + approved_domains: + - localhost.baz.daplie.me + - localhost.beta.daplie.me domains: - names: - localhost.gamma.daplie.me modules: - name: proxy address: '127.0.0.1:6443' + - names: + - beta.localhost.daplie.me + - baz.localhost.daplie.me + modules: + - name: acme + email: 'owner@example.com' + challenge_type: 'tls-sni-01' + # default server is 'https://acme-v01.api.letsencrypt.org/directory' modules: - name: proxy domains: - localhost.bar.daplie.me - localhost.foo.daplie.me address: '127.0.0.1:5443' + - name: acme + email: 'guest@example.com' + challenge_type: 'http-01' + domains: + - foo.localhost.daplie.me + - gamma.localhost.daplie.me + http: trust_proxy: true diff --git a/lib/modules/tls.js b/lib/modules/tls.js index 34ad6fe..a18ab2d 100644 --- a/lib/modules/tls.js +++ b/lib/modules/tls.js @@ -22,6 +22,12 @@ module.exports.create = function (deps, config, netHandler) { return value || ''; } + function nameMatchesDomains(name, domains) { + return domains.some(function (pattern) { + return domainMatches(pattern, name); + }); + } + var addressNames = [ 'remoteAddress' , 'remotePort' @@ -67,17 +73,17 @@ module.exports.create = function (deps, config, netHandler) { } var le = greenlock.create({ - // server: 'staging' server: 'https://acme-v01.api.letsencrypt.org/directory' , challenges: { - 'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges', debug: config.debug }) + 'http-01': require('le-challenge-fs').create({ debug: config.debug }) , 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug }) // TODO dns-01 - //, 'dns-01': require('le-challenge-ddns').create() + //, 'dns-01': require('le-challenge-ddns').create({ debug: config.debug }) } + , challengeType: 'http-01' - , store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' }) + , store: require('le-store-certbot').create({ debug: config.debug }) , approveDomains: function (opts, certs, cb) { // This is where you check your database and associated @@ -88,60 +94,87 @@ module.exports.create = function (deps, config, netHandler) { 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; } - function complete(optsOverride) { - Object.keys(optsOverride).forEach(function (key) { - opts[key] = optsOverride[key]; - }); + function complete(optsOverride, domains) { + if (!cb) { + console.warn('tried to complete domain approval multiple times'); + return; + } + // We can't request certificates for wildcard domains, so filter any of those + // out of this list and put the domain that triggered this in the list if needed. + domains = (domains || []).filter(function (dom) { return dom[0] !== '*'; }); + if (domains.indexOf(opts.domain) < 0) { + domains.push(opts.domain); + } + // TODO: allow user to specify options for challenges or storage. + + Object.assign(opts, optsOverride, { domains: domains, agreeTos: true }); cb(null, { options: opts, certs: certs }); + cb = null; } + var handled = false; + if (Array.isArray(config.tls.domains)) { + handled = config.tls.domains.some(function (dom) { + if (!nameMatchesDomains(opts.domain, dom.names)) { + return false; + } - // 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({ + return dom.modules.some(function (mod) { + if (mod.name !== 'acme') { + return false; + } + complete(mod, dom.names); + return true; + }); + }); + } + if (handled) { + return; + } + + if (Array.isArray(config.tls.modules)) { + handled = config.tls.modules.some(function (mod) { + if (mod.name !== 'acme') { + return false; + } + if (!nameMatchesDomains(opts.domain, mod.domains)) { + return false; + } + + complete(mod, mod.domains); + return true; + }); + } + if (handled) { + return; + } + + var defAcmeConf; + if (config.tls.acme) { + defAcmeConf = config.tls.acme; + } else { + defAcmeConf = { email: config.tls.email - , agreeTos: true , server: config.tls.acmeDirectoryUrl || le.server - , challengeType: config.tls.challengeType || 'http-01' - }); + , challengeType: config.tls.challengeType || le.challengeType + , approvedDomains: config.tls.servernames + }; + } + + // Check config for domain name + // TODO: if `approvedDomains` isn't defined check all other modules to see if they can + // handle this domain (and what other domains it's grouped with). + if (-1 !== (defAcmeConf.approvedDomains || []).indexOf(opts.domain)) { + complete(defAcmeConf, defAcmeConf.approvedDomains); return; } - // TODO ask http module (and potentially all other modules) about what domains it can - // handle. We can allow any domains that other modules will handle after we terminate TLS. cb(new Error('domain is not allowed')); - // if (!modules.http) { - // modules.http = require('./modules/http.js').create(deps, config); - // } - // modules.http.checkServername(opts.domain).then(function (stuff) { - // if (!stuff || !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({ - // domain: stuff.domain || stuff.domains[0] - // , domains: stuff.domains - // , email: stuff.email || program.email - // , server: stuff.acmeDirectoryUrl || program.acmeDirectoryUrl - // , challengeType: stuff.challengeType || program.challengeType - // , challenge: stuff.challenge - // }); - // return; - // }, cb); } }); le.tlsOptions = le.tlsOptions || le.httpsOptions; @@ -257,10 +290,7 @@ module.exports.create = function (deps, config, netHandler) { } var handled = (config.tls.domains || []).some(function (dom) { - var relevant = dom.names.some(function (pattern) { - return domainMatches(pattern, opts.servername); - }); - if (!relevant) { + if (!nameMatchesDomains(opts.servername, dom.names)) { return false; } @@ -271,10 +301,7 @@ module.exports.create = function (deps, config, netHandler) { } handled = (config.tls.modules || []).some(function (mod) { - var relevant = mod.domains.some(function (pattern) { - return domainMatches(pattern, opts.servername); - }); - if (!relevant) { + if (!nameMatchesDomains(opts.servername, mod.domains)) { return false; } return checkModule(mod); From 2eb6d1bc951f54e3b08be5290a801ad1bd3cc3e6 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 24 May 2017 18:20:02 -0600 Subject: [PATCH 15/15] made more command line flags do things --- bin/goldilocks.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/bin/goldilocks.js b/bin/goldilocks.js index 28d33c0..70526cd 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -69,6 +69,7 @@ function readConfigAndRun(args) { var recase = require('recase').create({}); config = recase.camelCopy(config); + config.debug = config.debug || args.debug; if (!config.dns) { config.dns = { modules: [{ name: 'proxy', port: 3053 }] }; @@ -85,15 +86,17 @@ function readConfigAndRun(args) { config.http = { modules: [{ name: 'proxy', domains: ['*'], port: 3000 }] }; } if (!config.tls) { - console.log("TODO: tls: { modules: { name: 'acme', email: 'foo@bar.com', domains: [ '*' ] } }"); - config.tls = { - agreeTos: args.agreeTos || args.agree || args['agree-tos'] - , servernames: (args.servernames||'').split(',').filter(Boolean).map(function (str) { return str.toLowerCase(); }) - }; + config.tls = {}; + } + if (!config.tls.acme && (args.email || args.agreeTos)) { + config.tls.acme = {}; + } + if (typeof args.agreeTos === 'string') { + config.tls.acme.approvedDomains = args.agreeTos.split(','); } if (args.email) { config.email = args.email; - config.tls.email = args.email; + config.tls.acme.email = args.email; } // maybe this should not go in config... but be ephemeral in some way? @@ -195,20 +198,20 @@ function readConfigAndRun(args) { function readEnv(args) { // TODO + try { + if (process.env.GOLDILOCKS_HOME) { + process.chdir(process.env.GOLDILOCKS_HOME); + } + } catch (err) {} + var env = { tunnel: process.env.GOLDILOCKS_TUNNEL_TOKEN || process.env.GOLDILOCKS_TUNNEL && true , email: process.env.GOLDILOCKS_EMAIL - , cwd: process.env.GOLDILOCKS_HOME + , cwd: process.env.GOLDILOCKS_HOME || process.cwd() , 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); + readConfigAndRun(Object.assign({}, env, args)); } var program = require('commander'); @@ -222,5 +225,4 @@ program .option('--debug', "Enable debug output") .parse(process.argv); -program.cwd = process.cwd(); readEnv(program);