diff --git a/lib/goldilocks.js b/lib/goldilocks.js index d92d8d9..0c2248f 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -20,6 +20,22 @@ module.exports.create = function (deps, config) { var tunnelAdminTlsOpts = {}; var tls = require('tls'); + function domainMatches(pattern, servername) { + // Everything matches '*' + if (pattern === '*') { + return true; + } + + if (/^\*./.test(pattern)) { + // get rid of the leading "*." to more easily check the servername against it + pattern = pattern.slice(2); + return pattern === servername.slice(-pattern.length); + } + + // pattern doesn't contains any wildcards, so exact match is required + return pattern === servername; + } + var tcpRouter = { _map: { } , _create: function (address, port) { @@ -123,68 +139,103 @@ module.exports.create = function (deps, config) { } }; var tlsRouter = { - _map: { } - , _create: function (address, port/*, nextServer*/) { - // port provides hinting for https, smtps, etc - return function (socket, firstChunk, opts) { - if (opts.hyperPeek) { - // See "PEEK COMMENT" for more info - // This was peeked at properly, so we don't have to re-wrap it - // in order to get the connection to not hang. - // The real first 'data' and 'readable' events will occur as they should - program.tlsTunnelServer.emit('connection', socket); - return; + proxy: function (socket, opts, mod) { + var newConn = deps.net.createConnection({ + port: mod.port + , host: mod.address || '127.0.0.1' + + , servername: opts.servername + , data: opts.data + , remoteFamily: opts.family || socket.remoteFamily || socket._remoteFamily || socket._handle._parent.owner.stream.remoteFamily + , remoteAddress: opts.address || socket.remoteAddress || socket._remoteAddress || socket._handle._parent.owner.stream.remoteAddress + , remotePort: opts.port || socket.remotePort || socket._remotePort || socket._handle._parent.owner.stream.remotePort + }, function () { + // this will happen before 'data' is triggered + }); + + newConn.pipe(socket); + socket.pipe(newConn); + } + , terminate: function (socket) { + // We terminate the TLS by emitting the connections of the TLS server and it should handle + // everything we need to do for us. + program.tlsTunnelServer.emit('connection', socket); + } + + , handleModules: function (socket, opts) { + // needs to wind up in one of 2 states: + // 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket) + // 2. Terminated (goes on to a particular module or route, including the admin interface) + + var handled = (config.tls.modules || []).some(function (mod) { + var relevant = mod.domains.some(function (pattern) { + return domainMatches(pattern, opts.servername); + }); + if (!relevant) { + return false; } - var servername = opts.servername; - var packerStream = require('tunnel-packer').Stream; - var myDuplex = packerStream.create(socket); + if (mod.name === 'proxy') { + tlsRouter.proxy(socket, opts, mod); + } + else if (mod.name === 'terminate') { + tlsRouter.terminate(socket); + } + else { + console.error('saw unknown TLS module', mod); + return false; + } - myDuplex.remoteAddress = opts.remoteAddress || myDuplex.remoteAddress; - myDuplex.remotePort = opts.remotePort || myDuplex.remotePort; - console.log('[tlsRouter] ' + address + ':' + port + ' servername', servername, myDuplex.remoteAddress); + return true; + }); - // needs to wind up in one of 3 states: - // 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket) - // 2. Admin Interface (skips the proxying) - // 3. Terminated (goes on to a particular module or route) - //myDuplex.__tlsTerminated = true; - - 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(firstChunk); - }); - - // nextServer.emit could be used here - program.tlsTunnelServer.emit('connection', myDuplex); - - // Why all this wacky-do with the myDuplex? - // because https://github.com/nodejs/node/issues/8854, that's why - // (because node's internal networking layer == 💩 sometimes) - 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(); - }); - }; + // We gotta do something, so when in doubt terminate the TLS since we don't really have + // any good place to default to when proxying. + if (!handled) { + tlsRouter.terminate(socket); + } } - , 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); + , processSocket: function (socket, firstChunk, opts) { + if (opts.hyperPeek) { + // See "PEEK COMMENT" for more info + // This was already peeked at by the tunneler and this connection has been created + // in a way that should work with node's TLS server, so we don't need to do any + // of the myDuplex stuff that we need to do with non-tunnel connections. + tlsRouter.handleModules(socket, opts); + return; } - return tlsRouter._map[id]; + // Why all this wacky-do with the myDuplex? + // because https://github.com/nodejs/node/issues/8854, that's why + // (because node's internal networking layer == 💩 sometimes) + var myDuplex = require('tunnel-packer').Stream.create(socket); + myDuplex.remoteAddress = opts.remoteAddress || myDuplex.remoteAddress; + myDuplex.remotePort = opts.remotePort || myDuplex.remotePort; + + 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(); + }); + + var address = opts.localAddress || socket.localAddress; + var port = opts.localPort || socket.localPort; + console.log('[tlsRouter] ' + address + ':' + port + ' servername', opts.servername, myDuplex.remoteAddress); + + tlsRouter.handleModules(myDuplex, opts); + 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(firstChunk); + }); } }; @@ -197,7 +248,7 @@ module.exports.create = function (deps, config) { if (0x16 === firstChunk[0]/* && 0x01 === firstChunk[5]*/) { console.log('tryTls'); opts.servername = (parseSni(firstChunk)||'').toLowerCase() || 'localhost.invalid'; - tlsRouter.get(opts.localAddress || conn.localAddress, conn.localPort)(conn, firstChunk, opts); + tlsRouter.processSocket(conn, firstChunk, opts); return; }