diff --git a/bin/telebit-relay.js b/bin/telebit-relay.js index 684b86f..0779691 100755 --- a/bin/telebit-relay.js +++ b/bin/telebit-relay.js @@ -47,7 +47,13 @@ function applyConfig(config) { } else { state.Promise = require('bluebird'); } - state.tlsOptions = {}; // TODO just close the sockets that would use this early? or use the admin servername + state.tlsOptions = { + // Handles disconnected devices + // TODO allow user to opt-in to wildcard hosting for a better error page? + SNICallback: function (servername, cb) { + return state.greenlock.tlsOptions.SNICallback(state.config.webminDomain || state.servernames[0], cb); + } + }; // TODO just close the sockets that would use this early? or use the admin servername state.config = config; state.servernames = config.servernames || []; state.secret = state.config.secret; diff --git a/lib/handlers.js b/lib/handlers.js index c8b2acd..4a94bc6 100644 --- a/lib/handlers.js +++ b/lib/handlers.js @@ -10,7 +10,7 @@ function noSniCallback(tag) { var err = new Error("[noSniCallback] no handler set for '" + tag + "':'" + servername + "'"); console.error(err.message); cb(new Error(err)); - } + }; } module.exports.create = function (state) { @@ -72,6 +72,28 @@ module.exports.create = function (state) { state.tlsInvalidSniServer.on('tlsClientError', function () { console.error('tlsClientError InvalidSniServer'); }); + state.createHttpInvalid = function (servername) { + return http.createServer(function (req, res) { + if (!servername) { + res.statusCode = 422; + res.end( + "3. An inexplicable temporal shift of the quantum realm... that makes me feel uncomfortable.\n\n" + + "[ERROR] No SNI header was sent. I can only think of two possible explanations for this:\n" + + "\t1. You really love Windows XP and you just won't let go of Internet Explorer 6\n" + + "\t2. You're writing a bot and you forgot to set the servername parameter\n" + ); + return; + } + + // TODO use req.headers.host instead of servername (since domain fronting is disabled anyway) + res.statusCode = 502; + res.end( + "
It looks like '" + encodeURIComponent(servername) + "' isn't connected right now.
" + + "Error: 502 Bad Gateway" + ); + }); + }; state.httpsInvalid = function (servername, socket) { // none of these methods work: // httpsServer.emit('connection', socket); // this didn't work @@ -82,28 +104,9 @@ module.exports.create = function (state) { //state.tlsInvalidSniServer.emit('connection', wrapSocket(socket)); var tlsInvalidSniServer = tls.createServer(state.tlsOptions, function (tlsSocket) { console.log('[tlsInvalid] tls connection'); - // things get a little messed up here - var httpInvalidSniServer = http.createServer(function (req, res) { - if (!servername) { - res.statusCode = 422; - res.end( - "3. An inexplicable temporal shift of the quantum realm... that makes me feel uncomfortable.\n\n" - + "[ERROR] No SNI header was sent. I can only think of two possible explanations for this:\n" - + "\t1. You really love Windows XP and you just won't let go of Internet Explorer 6\n" - + "\t2. You're writing a bot and you forgot to set the servername parameter\n" - ); - return; - } - - res.end( - "You came in hot looking for '" + servername + "' and, granted, the IP address for that domain" - + " must be pointing here (or else how could you be here?), nevertheless either it's not registered" - + " in the internal system at all (which Seth says isn't even a thing) or there is no device" - + " connected on the south side of the network which has informed me that it's ready to have traffic" - + " for that domain forwarded to it (sorry I didn't check that deeply to determine which).\n\n" - + "Either way, you're doing strange things that make me feel uncomfortable... Please don't touch me there any more."); - }); - httpInvalidSniServer.emit('connection', tlsSocket); + // We create an entire http server object because it's difficult to figure out + // how to access the original tlsSocket to get the servername + state.createHttpInvalid(servername).emit('connection', tlsSocket); }); tlsInvalidSniServer.on('tlsClientError', function () { console.error('tlsClientError InvalidSniServer httpsInvalid'); diff --git a/lib/unwrap-tls.js b/lib/unwrap-tls.js index 86e356e..46694fc 100644 --- a/lib/unwrap-tls.js +++ b/lib/unwrap-tls.js @@ -27,6 +27,16 @@ module.exports.createTcpConnectionHandler = function (state) { var str; var m; + if (!firstChunk) { + try { + conn.end(); + } catch(e) { + console.error("[lib/unwrap-tls.js] Error:", e); + conn.destroy(); + } + return; + } + //conn.pause(); conn.unshift(firstChunk); @@ -40,6 +50,8 @@ module.exports.createTcpConnectionHandler = function (state) { function deferData(fn) { if (fn) { state[fn](servername, conn); + } else { + console.error("[SANITY ERROR] '" + fn + "' doesn't have a state handler"); } /* process.nextTick(function () { @@ -48,33 +60,78 @@ module.exports.createTcpConnectionHandler = function (state) { */ } - function tryTls() { - var vhost; - - if (!state.servernames.length) { - console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)"); - deferData('httpsSetupServer'); - return; + var httpOutcomes = { + missingServername: function () { + console.log("[debug] [http] missing servername"); + // TODO use a more specific error page + deferData('handleInsecureHttp'); } - - if (-1 !== state.servernames.indexOf(servername)) { - if (state.debug) { console.log("[Admin]", servername); } - deferData('httpsTunnel'); - return; + , requiresSetup: function () { + console.log("[debug] [http] requires setup"); + // TODO Insecure connections for setup will not work on secure domains (i.e. .app) + state.httpSetupServer.emit('connection', conn); } - - if (state.config.nowww && /^www\./i.test(servername)) { - console.log("TODO: use www bare redirect"); + , isInternal: function () { + console.log("[debug] [http] is known internally (admin)"); + if (/well-known/.test(str)) { + deferData('handleHttp'); + } else { + deferData('handleInsecureHttp'); + } } + , isVhost: function () { + console.log("[debug] [http] is vhost (normal server)"); + if (/well-known/.test(str)) { + deferData('handleHttp'); + } else { + deferData('handleInsecureHttp'); + } + } + , assumeExternal: function () { + console.log("[debug] [http] assume external"); + var service = 'http'; - if (!servername) { + if (!Devices.exist(state.deviceLists, servername)) { + // It would be better to just re-read the host header rather + // than creating a whole server object, but this is a "rare" + // case and I'm feeling lazy right now. + console.log("[debug] [http] no device connected"); + state.createHttpInvalid(servername).emit('connection', conn); + return; + } + + // TODO make https redirect configurable on a per-domain basis + // /^\/\.well-known\/acme-challenge\//.test(str) + if (/well-known/.test(str)) { + // HTTP + console.log("[debug] [http] passthru"); + pipeWs(servername, service, Devices.next(state.deviceLists, servername), conn, serviceport); + return; + } else { + console.log("[debug] [http] redirect to https"); + deferData('handleInsecureHttp'); + } + } + }; + var tlsOutcomes = { + missingServername: function () { if (state.debug) { console.log("No SNI was given, so there's nothing we can do here"); } deferData('httpsInvalid'); - return; } - - function run() { - var nextDevice = Devices.next(state.deviceLists, servername); + , requiresSetup: function () { + console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)"); + deferData('httpsSetupServer'); + } + , isInternal: function () { + if (state.debug) { console.log("[Admin]", servername); } + deferData('httpsTunnel'); + } + , isVhost: function (vhost) { + if (state.debug) { console.log("[tcp] [vhost]", state.config.vhost, "=>", vhost); } + deferData('httpsVhost'); + } + , assumeExternal: function () { + var nextDevice = Devices.next(state.deviceLists, servername); if (!nextDevice) { if (state.debug) { console.log("No devices match the given servername"); } deferData('httpsInvalid'); @@ -82,27 +139,33 @@ module.exports.createTcpConnectionHandler = function (state) { } if (state.debug) { console.log("pipeWs(servername, service, deviceLists['" + servername + "'], socket)"); } - deferData(); pipeWs(servername, service, nextDevice, conn, serviceport); } + }; + + function handleConnection(outcomes) { + var vhost; + + // No routing information available + if (!servername) { outcomes.missingServername(); return; } + // Server needs to be set up + if (!state.servernames.length) { outcomes.requiresSetup(); return; } + // This is one of the admin domains + if (-1 !== state.servernames.indexOf(servername)) { outcomes.isInternal(); return; } // TODO don't run an fs check if we already know this is working elsewhere //if (!state.validHosts) { state.validHosts = {}; } if (state.config.vhost) { - vhost = state.config.vhost.replace(/:hostname/, (servername||'reallydoesntexist')); - if (state.debug) { console.log("[tcp] [vhost]", state.config.vhost, "=>", vhost); } - //state.httpsVhost(servername, conn); - //return; + vhost = state.config.vhost.replace(/:hostname/, servername); require('fs').readdir(vhost, function (err, nodes) { if (state.debug && err) { console.log("VHOST error", err); } - if (err || !nodes) { run(); return; } - //if (nodes) { deferData('httpsVhost'); return; } - deferData('httpsVhost'); + if (err || !nodes) { outcomes.assumeExternal(); return; } + outcomes.isVhost(vhost); }); return; } - run(); + outcomes.assumeExternal(); } // https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155 @@ -111,40 +174,19 @@ module.exports.createTcpConnectionHandler = function (state) { service = 'https'; servername = (sni(firstChunk)||'').toLowerCase().trim(); if (state.debug) { console.log("[tcp] tls hello from '" + servername + "'"); } - tryTls(); + handleConnection(tlsOutcomes); return; } if (firstChunk[0] > 32 && firstChunk[0] < 127) { + // (probably) HTTP str = firstChunk.toString(); m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); servername = (m && m[1].toLowerCase() || '').split(':')[0]; if (state.debug) { console.log("[tcp] http hostname '" + servername + "'"); } if (/HTTP\//i.test(str)) { - if (!state.servernames.length) { - console.info("[tcp] No admin servername. Entering setup mode."); - deferData(); - state.httpSetupServer.emit('connection', conn); - return; - } - - service = 'http'; - // TODO make https redirect configurable - // /^\/\.well-known\/acme-challenge\//.test(str) - if (/well-known/.test(str)) { - // HTTP - if (Devices.exist(state.deviceLists, servername)) { - deferData(); - pipeWs(servername, service, Devices.next(state.deviceLists, servername), conn, serviceport); - return; - } - deferData('handleHttp'); - return; - } - - // redirect to https - deferData('handleInsecureHttp'); + handleConnection(httpOutcomes); return; } }