move tls-unwrapping (it may be sharable with client)
This commit is contained in:
parent
98d31fc8d7
commit
07965d8eac
|
@ -0,0 +1,55 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Devices = module.exports;
|
||||||
|
Devices.add = function (store, servername, newDevice) {
|
||||||
|
var devices = store[servername] || [];
|
||||||
|
devices.push(newDevice);
|
||||||
|
store[servername] = devices;
|
||||||
|
};
|
||||||
|
Devices.remove = function (store, servername, device) {
|
||||||
|
var devices = store[servername] || [];
|
||||||
|
var index = devices.indexOf(device);
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
console.warn('attempted to remove non-present device', device.deviceId, 'from', servername);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return devices.splice(index, 1)[0];
|
||||||
|
};
|
||||||
|
Devices.list = function (store, servername) {
|
||||||
|
if (store[servername] && store[servername].length) {
|
||||||
|
return store[servername];
|
||||||
|
}
|
||||||
|
// There wasn't an exact match so check any of the wildcard domains, sorted longest
|
||||||
|
// first so the one with the biggest natural match with be found first.
|
||||||
|
var deviceList = [];
|
||||||
|
Object.keys(store).filter(function (pattern) {
|
||||||
|
return pattern[0] === '*' && store[pattern].length;
|
||||||
|
}).sort(function (a, b) {
|
||||||
|
return b.length - a.length;
|
||||||
|
}).some(function (pattern) {
|
||||||
|
var subPiece = pattern.slice(1);
|
||||||
|
if (subPiece === servername.slice(-subPiece.length)) {
|
||||||
|
console.log('"'+servername+'" matches "'+pattern+'"');
|
||||||
|
deviceList = store[pattern];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return deviceList;
|
||||||
|
};
|
||||||
|
Devices.exist = function (store, servername) {
|
||||||
|
return !!(Devices.list(store, servername).length);
|
||||||
|
};
|
||||||
|
Devices.next = function (store, servername) {
|
||||||
|
var devices = Devices.list(store, servername);
|
||||||
|
var device;
|
||||||
|
|
||||||
|
if (devices._index >= devices.length) {
|
||||||
|
devices._index = 0;
|
||||||
|
}
|
||||||
|
device = devices[devices._index || 0];
|
||||||
|
devices._index = (devices._index || 0) + 1;
|
||||||
|
|
||||||
|
return device;
|
||||||
|
};
|
|
@ -0,0 +1,153 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var packer = require('tunnel-packer');
|
||||||
|
var sni = require('sni');
|
||||||
|
|
||||||
|
function pipeWs(servername, service, conn, remote) {
|
||||||
|
console.log('[pipeWs] servername:', servername, 'service:', service);
|
||||||
|
|
||||||
|
var browserAddr = packer.socketToAddr(conn);
|
||||||
|
browserAddr.service = service;
|
||||||
|
var cid = packer.addrToId(browserAddr);
|
||||||
|
conn.tunnelCid = cid;
|
||||||
|
console.log('[pipeWs] browser is', cid, 'home-cloud is', packer.socketToId(remote.upgradeReq.socket));
|
||||||
|
|
||||||
|
function sendWs(data, serviceOverride) {
|
||||||
|
if (remote.ws && (!conn.tunnelClosing || serviceOverride)) {
|
||||||
|
try {
|
||||||
|
remote.ws.send(packer.pack(browserAddr, data, serviceOverride), { binary: true });
|
||||||
|
// If we can't send data over the websocket as fast as this connection can send it to us
|
||||||
|
// (or there are a lot of connections trying to send over the same websocket) then we
|
||||||
|
// need to pause the connection for a little. We pause all connections if any are paused
|
||||||
|
// to make things more fair so a connection doesn't get stuck waiting for everyone else
|
||||||
|
// to finish because it got caught on the boundary. Also if serviceOverride is set it
|
||||||
|
// means the connection is over, so no need to pause it.
|
||||||
|
if (!serviceOverride && (remote.pausedConns.length || remote.ws.bufferedAmount > 1024*1024)) {
|
||||||
|
// console.log('pausing', cid, 'to allow web socket to catch up');
|
||||||
|
conn.pause();
|
||||||
|
remote.pausedConns.push(conn);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[pipeWs] error sending websocket message', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remote.clients[cid] = conn;
|
||||||
|
conn.on('data', function (chunk) {
|
||||||
|
console.log('[pipeWs] data from browser to tunneler', chunk.byteLength);
|
||||||
|
sendWs(chunk);
|
||||||
|
});
|
||||||
|
conn.on('error', function (err) {
|
||||||
|
console.warn('[pipeWs] browser connection error', err);
|
||||||
|
});
|
||||||
|
conn.on('close', function (hadErr) {
|
||||||
|
console.log('[pipeWs] browser connection closing');
|
||||||
|
sendWs(null, hadErr ? 'error': 'end');
|
||||||
|
delete remote.clients[cid];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.createTcpConnectionHandler = function (copts) {
|
||||||
|
var Devices = copts.Devices;
|
||||||
|
|
||||||
|
return function onTcpConnection(conn) {
|
||||||
|
// this works when I put it here, but I don't know if it's tls yet here
|
||||||
|
// httpsServer.emit('connection', socket);
|
||||||
|
//tls3000.emit('connection', socket);
|
||||||
|
|
||||||
|
//var tlsSocket = new tls.TLSSocket(socket, { secureContext: tls.createSecureContext(tlsOpts) });
|
||||||
|
//tlsSocket.on('data', function (chunk) {
|
||||||
|
// console.log('dummy', chunk.byteLength);
|
||||||
|
//});
|
||||||
|
|
||||||
|
//return;
|
||||||
|
conn.once('data', function (firstChunk) {
|
||||||
|
// BUG XXX: this assumes that the packet won't be chunked smaller
|
||||||
|
// than the 'hello' or the point of the 'Host' header.
|
||||||
|
// This is fairly reasonable, but there are edge cases where
|
||||||
|
// it does not hold (such as manual debugging with telnet)
|
||||||
|
// and so it should be fixed at some point in the future
|
||||||
|
|
||||||
|
// defer after return (instead of being in many places)
|
||||||
|
process.nextTick(function () {
|
||||||
|
conn.unshift(firstChunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
var service = 'tcp';
|
||||||
|
var servername;
|
||||||
|
var str;
|
||||||
|
var m;
|
||||||
|
|
||||||
|
function tryTls() {
|
||||||
|
if (-1 !== copts.servernames.indexOf(servername)) {
|
||||||
|
console.log("Lock and load, admin interface time!");
|
||||||
|
copts.httpsTunnel(servername, conn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!servername) {
|
||||||
|
console.log("No SNI was given, so there's nothing we can do here");
|
||||||
|
copts.httpsInvalid(servername, conn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextDevice = Devices.next(copts.deviceLists, servername);
|
||||||
|
if (!nextDevice) {
|
||||||
|
console.log("No devices match the given servername");
|
||||||
|
copts.httpsInvalid(servername, conn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])");
|
||||||
|
pipeWs(servername, service, conn, nextDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
|
||||||
|
if (22 === firstChunk[0]) {
|
||||||
|
// TLS
|
||||||
|
service = 'https';
|
||||||
|
servername = (sni(firstChunk)||'').toLowerCase();
|
||||||
|
console.log("tls hello servername:", servername);
|
||||||
|
tryTls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstChunk[0] > 32 && firstChunk[0] < 127) {
|
||||||
|
str = firstChunk.toString();
|
||||||
|
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
|
||||||
|
servername = (m && m[1].toLowerCase() || '').split(':')[0];
|
||||||
|
console.log('servername', servername);
|
||||||
|
if (/HTTP\//i.test(str)) {
|
||||||
|
service = 'http';
|
||||||
|
// TODO disallow http entirely
|
||||||
|
// /^\/\.well-known\/acme-challenge\//.test(str)
|
||||||
|
if (/well-known/.test(str)) {
|
||||||
|
// HTTP
|
||||||
|
if (Devices.exist(copts.deviceLists, servername)) {
|
||||||
|
pipeWs(servername, service, conn, Devices.next(copts.deviceLists, servername));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
copts.handleHttp(servername, conn);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// redirect to https
|
||||||
|
copts.handleInsecureHttp(servername, conn);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Got unexpected connection", str);
|
||||||
|
conn.write(JSON.stringify({ error: {
|
||||||
|
message: "not sure what you were trying to do there..."
|
||||||
|
, code: 'E_INVALID_PROTOCOL' }
|
||||||
|
}));
|
||||||
|
conn.end();
|
||||||
|
});
|
||||||
|
conn.on('error', function (err) {
|
||||||
|
console.error('[error] tcp socket raw TODO forward and close');
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
211
wstunneld.js
211
wstunneld.js
|
@ -1,6 +1,5 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var sni = require('sni');
|
|
||||||
var url = require('url');
|
var url = require('url');
|
||||||
var PromiseA = require('bluebird');
|
var PromiseA = require('bluebird');
|
||||||
var jwt = require('jsonwebtoken');
|
var jwt = require('jsonwebtoken');
|
||||||
|
@ -12,65 +11,16 @@ function timeoutPromise(duration) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var Devices = {};
|
var Devices = require('./lib/device-tracker');
|
||||||
Devices.add = function (store, servername, newDevice) {
|
|
||||||
var devices = store[servername] || [];
|
|
||||||
devices.push(newDevice);
|
|
||||||
store[servername] = devices;
|
|
||||||
};
|
|
||||||
Devices.remove = function (store, servername, device) {
|
|
||||||
var devices = store[servername] || [];
|
|
||||||
var index = devices.indexOf(device);
|
|
||||||
|
|
||||||
if (index < 0) {
|
|
||||||
console.warn('attempted to remove non-present device', device.deviceId, 'from', servername);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return devices.splice(index, 1)[0];
|
|
||||||
};
|
|
||||||
Devices.list = function (store, servername) {
|
|
||||||
if (store[servername] && store[servername].length) {
|
|
||||||
return store[servername];
|
|
||||||
}
|
|
||||||
// There wasn't an exact match so check any of the wildcard domains, sorted longest
|
|
||||||
// first so the one with the biggest natural match with be found first.
|
|
||||||
var deviceList = [];
|
|
||||||
Object.keys(store).filter(function (pattern) {
|
|
||||||
return pattern[0] === '*' && store[pattern].length;
|
|
||||||
}).sort(function (a, b) {
|
|
||||||
return b.length - a.length;
|
|
||||||
}).some(function (pattern) {
|
|
||||||
var subPiece = pattern.slice(1);
|
|
||||||
if (subPiece === servername.slice(-subPiece.length)) {
|
|
||||||
console.log('"'+servername+'" matches "'+pattern+'"');
|
|
||||||
deviceList = store[pattern];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return deviceList;
|
|
||||||
};
|
|
||||||
Devices.exist = function (store, servername) {
|
|
||||||
return !!(Devices.list(store, servername).length);
|
|
||||||
};
|
|
||||||
Devices.next = function (store, servername) {
|
|
||||||
var devices = Devices.list(store, servername);
|
|
||||||
var device;
|
|
||||||
|
|
||||||
if (devices._index >= devices.length) {
|
|
||||||
devices._index = 0;
|
|
||||||
}
|
|
||||||
device = devices[devices._index || 0];
|
|
||||||
devices._index = (devices._index || 0) + 1;
|
|
||||||
|
|
||||||
return device;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.store = { Devices: Devices };
|
module.exports.store = { Devices: Devices };
|
||||||
module.exports.create = function (copts) {
|
module.exports.create = function (copts) {
|
||||||
var deviceLists = {};
|
copts.deviceLists = {};
|
||||||
|
//var deviceLists = {};
|
||||||
var activityTimeout = copts.activityTimeout || 2*60*1000;
|
var activityTimeout = copts.activityTimeout || 2*60*1000;
|
||||||
var pongTimeout = copts.pongTimeout || 10*1000;
|
var pongTimeout = copts.pongTimeout || 10*1000;
|
||||||
|
copts.Devices = Devices;
|
||||||
|
var onTcpConnection = require('./lib/unwrap-tls').createTcpConnectionHandler(copts);
|
||||||
|
|
||||||
function onWsConnection(ws, upgradeReq) {
|
function onWsConnection(ws, upgradeReq) {
|
||||||
console.log(ws);
|
console.log(ws);
|
||||||
|
@ -204,7 +154,7 @@ module.exports.create = function (copts) {
|
||||||
|
|
||||||
token.domains.forEach(function (domainname) {
|
token.domains.forEach(function (domainname) {
|
||||||
console.log('domainname', domainname);
|
console.log('domainname', domainname);
|
||||||
Devices.add(deviceLists, domainname, token);
|
Devices.add(copts.deviceLists, domainname, token);
|
||||||
});
|
});
|
||||||
remotes[jwtoken] = token;
|
remotes[jwtoken] = token;
|
||||||
console.log("added token '" + token.deviceId + "' to websocket", socketId);
|
console.log("added token '" + token.deviceId + "' to websocket", socketId);
|
||||||
|
@ -220,7 +170,7 @@ module.exports.create = function (copts) {
|
||||||
// Prevent any more browser connections being sent to this remote, and any existing
|
// Prevent any more browser connections being sent to this remote, and any existing
|
||||||
// connections from trying to send more data across the connection.
|
// connections from trying to send more data across the connection.
|
||||||
remote.domains.forEach(function (domainname) {
|
remote.domains.forEach(function (domainname) {
|
||||||
Devices.remove(deviceLists, domainname, remote);
|
Devices.remove(copts.deviceLists, domainname, remote);
|
||||||
});
|
});
|
||||||
remote.ws = null;
|
remote.ws = null;
|
||||||
remote.upgradeReq = null;
|
remote.upgradeReq = null;
|
||||||
|
@ -437,154 +387,9 @@ module.exports.create = function (copts) {
|
||||||
sendTunnelMsg(null, [1, 'hello', [unpacker._version], Object.keys(commandHandlers)], 'control');
|
sendTunnelMsg(null, [1, 'hello', [unpacker._version], Object.keys(commandHandlers)], 'control');
|
||||||
}
|
}
|
||||||
|
|
||||||
function pipeWs(servername, service, conn, remote) {
|
|
||||||
console.log('[pipeWs] servername:', servername, 'service:', service);
|
|
||||||
|
|
||||||
var browserAddr = packer.socketToAddr(conn);
|
|
||||||
browserAddr.service = service;
|
|
||||||
var cid = packer.addrToId(browserAddr);
|
|
||||||
conn.tunnelCid = cid;
|
|
||||||
console.log('[pipeWs] browser is', cid, 'home-cloud is', packer.socketToId(remote.upgradeReq.socket));
|
|
||||||
|
|
||||||
function sendWs(data, serviceOverride) {
|
|
||||||
if (remote.ws && (!conn.tunnelClosing || serviceOverride)) {
|
|
||||||
try {
|
|
||||||
remote.ws.send(packer.pack(browserAddr, data, serviceOverride), { binary: true });
|
|
||||||
// If we can't send data over the websocket as fast as this connection can send it to us
|
|
||||||
// (or there are a lot of connections trying to send over the same websocket) then we
|
|
||||||
// need to pause the connection for a little. We pause all connections if any are paused
|
|
||||||
// to make things more fair so a connection doesn't get stuck waiting for everyone else
|
|
||||||
// to finish because it got caught on the boundary. Also if serviceOverride is set it
|
|
||||||
// means the connection is over, so no need to pause it.
|
|
||||||
if (!serviceOverride && (remote.pausedConns.length || remote.ws.bufferedAmount > 1024*1024)) {
|
|
||||||
// console.log('pausing', cid, 'to allow web socket to catch up');
|
|
||||||
conn.pause();
|
|
||||||
remote.pausedConns.push(conn);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[pipeWs] error sending websocket message', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remote.clients[cid] = conn;
|
|
||||||
conn.on('data', function (chunk) {
|
|
||||||
console.log('[pipeWs] data from browser to tunneler', chunk.byteLength);
|
|
||||||
sendWs(chunk);
|
|
||||||
});
|
|
||||||
conn.on('error', function (err) {
|
|
||||||
console.warn('[pipeWs] browser connection error', err);
|
|
||||||
});
|
|
||||||
conn.on('close', function (hadErr) {
|
|
||||||
console.log('[pipeWs] browser connection closing');
|
|
||||||
sendWs(null, hadErr ? 'error': 'end');
|
|
||||||
delete remote.clients[cid];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTcpConnection(conn) {
|
|
||||||
// this works when I put it here, but I don't know if it's tls yet here
|
|
||||||
// httpsServer.emit('connection', socket);
|
|
||||||
//tls3000.emit('connection', socket);
|
|
||||||
|
|
||||||
//var tlsSocket = new tls.TLSSocket(socket, { secureContext: tls.createSecureContext(tlsOpts) });
|
|
||||||
//tlsSocket.on('data', function (chunk) {
|
|
||||||
// console.log('dummy', chunk.byteLength);
|
|
||||||
//});
|
|
||||||
|
|
||||||
//return;
|
|
||||||
conn.once('data', function (firstChunk) {
|
|
||||||
// BUG XXX: this assumes that the packet won't be chunked smaller
|
|
||||||
// than the 'hello' or the point of the 'Host' header.
|
|
||||||
// This is fairly reasonable, but there are edge cases where
|
|
||||||
// it does not hold (such as manual debugging with telnet)
|
|
||||||
// and so it should be fixed at some point in the future
|
|
||||||
|
|
||||||
// defer after return (instead of being in many places)
|
|
||||||
process.nextTick(function () {
|
|
||||||
conn.unshift(firstChunk);
|
|
||||||
});
|
|
||||||
|
|
||||||
var service = 'tcp';
|
|
||||||
var servername;
|
|
||||||
var str;
|
|
||||||
var m;
|
|
||||||
|
|
||||||
function tryTls() {
|
|
||||||
if (-1 !== copts.servernames.indexOf(servername)) {
|
|
||||||
console.log("Lock and load, admin interface time!");
|
|
||||||
copts.httpsTunnel(servername, conn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!servername) {
|
|
||||||
console.log("No SNI was given, so there's nothing we can do here");
|
|
||||||
copts.httpsInvalid(servername, conn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextDevice = Devices.next(deviceLists, servername);
|
|
||||||
if (!nextDevice) {
|
|
||||||
console.log("No devices match the given servername");
|
|
||||||
copts.httpsInvalid(servername, conn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])");
|
|
||||||
pipeWs(servername, service, conn, nextDevice);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
|
|
||||||
if (22 === firstChunk[0]) {
|
|
||||||
// TLS
|
|
||||||
service = 'https';
|
|
||||||
servername = (sni(firstChunk)||'').toLowerCase();
|
|
||||||
console.log("tls hello servername:", servername);
|
|
||||||
tryTls();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstChunk[0] > 32 && firstChunk[0] < 127) {
|
|
||||||
str = firstChunk.toString();
|
|
||||||
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
|
|
||||||
servername = (m && m[1].toLowerCase() || '').split(':')[0];
|
|
||||||
console.log('servername', servername);
|
|
||||||
if (/HTTP\//i.test(str)) {
|
|
||||||
service = 'http';
|
|
||||||
// TODO disallow http entirely
|
|
||||||
// /^\/\.well-known\/acme-challenge\//.test(str)
|
|
||||||
if (/well-known/.test(str)) {
|
|
||||||
// HTTP
|
|
||||||
if (Devices.exist(deviceLists, servername)) {
|
|
||||||
pipeWs(servername, service, conn, Devices.next(deviceLists, servername));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
copts.handleHttp(servername, conn);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// redirect to https
|
|
||||||
copts.handleInsecureHttp(servername, conn);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("Got unexpected connection", str);
|
|
||||||
conn.write(JSON.stringify({ error: {
|
|
||||||
message: "not sure what you were trying to do there..."
|
|
||||||
, code: 'E_INVALID_PROTOCOL' }
|
|
||||||
}));
|
|
||||||
conn.end();
|
|
||||||
});
|
|
||||||
conn.on('error', function (err) {
|
|
||||||
console.error('[error] tcp socket raw TODO forward and close');
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tcp: onTcpConnection
|
tcp: onTcpConnection
|
||||||
, ws: onWsConnection
|
, ws: onWsConnection
|
||||||
, isClientDomain: Devices.exist.bind(null, deviceLists)
|
, isClientDomain: Devices.exist.bind(null, copts.deviceLists)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue