diff --git a/lib/goldilocks.js b/lib/goldilocks.js index f0f269a..023a44a 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -81,6 +81,7 @@ 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({ @@ -91,22 +92,28 @@ module.exports.create = function (deps, config) { , 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) { - console.error('TCP forward connection error', err); - conn.end(); + 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(); }); - newConn.pipe(conn); - conn.pipe(newConn); }; } diff --git a/lib/modules/http.js b/lib/modules/http.js index ab41e6c..7643862 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -69,14 +69,10 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { proxy.on('error', function (err, req, res) { console.log(err); - res.writeHead(502); - if (err.code === 'ECONNREFUSED') { - res.end('The connection was refused. Most likely the service being connected to ' - + 'has stopped running or the configuration is wrong.'); - } - else { - res.end('Bad Gateway: ' + err.code); - } + res.statusCode = 502; + res.setHeader('Content-Type', 'text/html'); + res.setHeader('Connection', 'close'); + res.end(require('../proxy-err-resp').getRespBody(err, conf.debug)); }); return function (req, res, next) { diff --git a/lib/modules/tls.js b/lib/modules/tls.js index 16f551f..1b6b606 100644 --- a/lib/modules/tls.js +++ b/lib/modules/tls.js @@ -4,6 +4,7 @@ module.exports.create = function (deps, config, netHandler) { var tls = require('tls'); var parseSni = require('sni'); var greenlock = require('greenlock'); + var localhostCerts = require('localhost.daplie.me-certificates'); var domainMatches = require('../match-domain').match; function extractSocketProp(socket, propName) { @@ -14,6 +15,34 @@ module.exports.create = function (deps, config, netHandler) { ; } + 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; + + 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 () { + // 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); + }); + + return myDuplex; + } + var le = greenlock.create({ // server: 'staging' server: 'https://acme-v01.api.letsencrypt.org/directory' @@ -105,7 +134,7 @@ module.exports.create = function (deps, config, netHandler) { if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) { // TODO implement if (!secureContexts[sni]) { - tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {}); + tlsOptions = localhostCerts.mergeTlsOptions(sni, {}); } if (tlsOptions) { secureContexts[sni] = tls.createSecureContext(tlsOptions); @@ -120,7 +149,7 @@ module.exports.create = function (deps, config, netHandler) { le.tlsOptions.SNICallback(sni, cb); }; - var terminator = tls.createServer(terminatorOpts, function (socket) { + var terminateServer = tls.createServer(terminatorOpts, function (socket) { console.log('(pre-terminated) tls connection, addr:', socket.remoteAddress); netHandler(socket, { @@ -135,6 +164,7 @@ module.exports.create = function (deps, config, netHandler) { function proxy(socket, opts, mod) { var destination = mod.address.split(':'); + var connected = false; var newConn = deps.net.createConnection({ port: destination[1] @@ -145,62 +175,60 @@ module.exports.create = function (deps, config, netHandler) { , 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) { - console.error('TLS proxy connection error', err); - socket.end(); + if (connected) { + console.error('TLS proxy remote error', err); + socket.end(); + } 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); + } }); socket.on('error', function (err) { console.error('TLS proxy client error', err); newConn.end(); }); - - newConn.write(opts.firstChunk); - newConn.pipe(socket); - socket.pipe(newConn); } function terminate(socket, opts) { - console.log('[tls-terminate] ' + opts.localAddress || socket.localAddress + ':' + opts.localPort || socket.localPort + ' servername', opts.servername, socket.remoteAddress); + console.log( + '[tls-terminate]' + , opts.localAddress || socket.localAddress +':'+ opts.localPort || socket.localPort + , 'servername=' + opts.servername + , opts.remoteAddress || socket.remoteAddress + ); if (opts.hyperPeek) { // This connection was peeked at using a method that doesn't interferre with the TLS // server's ability to handle it properly. Currently the only way this happens is // with tunnel connections where we have the first chunk of data before creating the // new connection (thus removing need to get data off the new connection). - terminator.emit('connection', socket); - return; + terminateServer.emit('connection', socket); + } + else { + // The hyperPeek flag wasn't set, so we had to read data off of this connection, which + // means we can no longer use it directly in the TLS server. + // See https://github.com/nodejs/node/issues/8752 (node's internal networking layer == 💩 sometimes) + terminateServer.emit('connection', wrapSocket(socket, opts)); } - - // The hyperPeek flag wasn't set, so we had to read data off of this connection, which - // means we can no longer use it directly in the TLS server. - // See https://github.com/nodejs/node/issues/8752 (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(); - }); - - terminator.emit('connection', myDuplex); - 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); - }); } function handleConn(socket, opts) { diff --git a/lib/proxy-err-resp.js b/lib/proxy-err-resp.js new file mode 100644 index 0000000..c2a35a8 --- /dev/null +++ b/lib/proxy-err-resp.js @@ -0,0 +1,32 @@ +'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;