improved feedback for bad TLS/TCP gateways
This commit is contained in:
parent
e24f9412dd
commit
5777a885a4
|
@ -81,6 +81,7 @@ module.exports.create = function (deps, config) {
|
||||||
|
|
||||||
function createTcpForwarder(mod) {
|
function createTcpForwarder(mod) {
|
||||||
var destination = mod.address.split(':');
|
var destination = mod.address.split(':');
|
||||||
|
var connected = false;
|
||||||
|
|
||||||
return function (conn) {
|
return function (conn) {
|
||||||
var newConn = deps.net.createConnection({
|
var newConn = deps.net.createConnection({
|
||||||
|
@ -91,22 +92,28 @@ module.exports.create = function (deps, config) {
|
||||||
, remoteAddress: conn.remoteAddress
|
, remoteAddress: conn.remoteAddress
|
||||||
, remotePort: conn.remotePort
|
, remotePort: conn.remotePort
|
||||||
}, function () {
|
}, 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
|
// 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.
|
// for the event to prevent it from crashing us.
|
||||||
newConn.on('error', function (err) {
|
newConn.on('error', function (err) {
|
||||||
console.error('TCP forward connection error', err);
|
if (connected) {
|
||||||
|
console.error('TCP forward remote error', err);
|
||||||
conn.end();
|
conn.end();
|
||||||
|
} else {
|
||||||
|
console.log('TCP forward connection error', err);
|
||||||
|
require('./proxy-err-resp').sendBadGateway(conn, err, config.debug);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
conn.on('error', function (err) {
|
conn.on('error', function (err) {
|
||||||
console.error('TCP forward client error', err);
|
console.error('TCP forward client error', err);
|
||||||
newConn.end();
|
newConn.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
newConn.pipe(conn);
|
|
||||||
conn.pipe(newConn);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,14 +69,10 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
|
||||||
|
|
||||||
proxy.on('error', function (err, req, res) {
|
proxy.on('error', function (err, req, res) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
res.writeHead(502);
|
res.statusCode = 502;
|
||||||
if (err.code === 'ECONNREFUSED') {
|
res.setHeader('Content-Type', 'text/html');
|
||||||
res.end('The connection was refused. Most likely the service being connected to '
|
res.setHeader('Connection', 'close');
|
||||||
+ 'has stopped running or the configuration is wrong.');
|
res.end(require('../proxy-err-resp').getRespBody(err, conf.debug));
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.end('Bad Gateway: ' + err.code);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return function (req, res, next) {
|
return function (req, res, next) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ module.exports.create = function (deps, config, netHandler) {
|
||||||
var tls = require('tls');
|
var tls = require('tls');
|
||||||
var parseSni = require('sni');
|
var parseSni = require('sni');
|
||||||
var greenlock = require('greenlock');
|
var greenlock = require('greenlock');
|
||||||
|
var localhostCerts = require('localhost.daplie.me-certificates');
|
||||||
var domainMatches = require('../match-domain').match;
|
var domainMatches = require('../match-domain').match;
|
||||||
|
|
||||||
function extractSocketProp(socket, propName) {
|
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({
|
var le = greenlock.create({
|
||||||
// server: 'staging'
|
// server: 'staging'
|
||||||
server: 'https://acme-v01.api.letsencrypt.org/directory'
|
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())) {
|
if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) {
|
||||||
// TODO implement
|
// TODO implement
|
||||||
if (!secureContexts[sni]) {
|
if (!secureContexts[sni]) {
|
||||||
tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {});
|
tlsOptions = localhostCerts.mergeTlsOptions(sni, {});
|
||||||
}
|
}
|
||||||
if (tlsOptions) {
|
if (tlsOptions) {
|
||||||
secureContexts[sni] = tls.createSecureContext(tlsOptions);
|
secureContexts[sni] = tls.createSecureContext(tlsOptions);
|
||||||
|
@ -120,7 +149,7 @@ module.exports.create = function (deps, config, netHandler) {
|
||||||
le.tlsOptions.SNICallback(sni, cb);
|
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);
|
console.log('(pre-terminated) tls connection, addr:', socket.remoteAddress);
|
||||||
|
|
||||||
netHandler(socket, {
|
netHandler(socket, {
|
||||||
|
@ -135,6 +164,7 @@ module.exports.create = function (deps, config, netHandler) {
|
||||||
|
|
||||||
function proxy(socket, opts, mod) {
|
function proxy(socket, opts, mod) {
|
||||||
var destination = mod.address.split(':');
|
var destination = mod.address.split(':');
|
||||||
|
var connected = false;
|
||||||
|
|
||||||
var newConn = deps.net.createConnection({
|
var newConn = deps.net.createConnection({
|
||||||
port: destination[1]
|
port: destination[1]
|
||||||
|
@ -145,62 +175,60 @@ module.exports.create = function (deps, config, netHandler) {
|
||||||
, remoteFamily: opts.family || extractSocketProp(socket, 'remoteFamily')
|
, remoteFamily: opts.family || extractSocketProp(socket, 'remoteFamily')
|
||||||
, remoteAddress: opts.address || extractSocketProp(socket, 'remoteAddress')
|
, remoteAddress: opts.address || extractSocketProp(socket, 'remoteAddress')
|
||||||
, remotePort: opts.port || extractSocketProp(socket, 'remotePort')
|
, 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
|
// 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.
|
// for the event to prevent it from crashing us.
|
||||||
newConn.on('error', function (err) {
|
newConn.on('error', function (err) {
|
||||||
console.error('TLS proxy connection error', err);
|
if (connected) {
|
||||||
|
console.error('TLS proxy remote error', err);
|
||||||
socket.end();
|
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) {
|
socket.on('error', function (err) {
|
||||||
console.error('TLS proxy client error', err);
|
console.error('TLS proxy client error', err);
|
||||||
newConn.end();
|
newConn.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
newConn.write(opts.firstChunk);
|
|
||||||
newConn.pipe(socket);
|
|
||||||
socket.pipe(newConn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function terminate(socket, opts) {
|
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) {
|
if (opts.hyperPeek) {
|
||||||
// This connection was peeked at using a method that doesn't interferre with the TLS
|
// 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
|
// 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
|
// 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).
|
// new connection (thus removing need to get data off the new connection).
|
||||||
terminator.emit('connection', socket);
|
terminateServer.emit('connection', socket);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
// The hyperPeek flag wasn't set, so we had to read data off of this connection, which
|
// 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.
|
// 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)
|
// See https://github.com/nodejs/node/issues/8752 (node's internal networking layer == 💩 sometimes)
|
||||||
var myDuplex = require('tunnel-packer').Stream.create(socket);
|
terminateServer.emit('connection', wrapSocket(socket, opts));
|
||||||
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) {
|
function handleConn(socket, opts) {
|
||||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue