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';
|
||||
|
||||
var sni = require('sni');
|
||||
var url = require('url');
|
||||
var PromiseA = require('bluebird');
|
||||
var jwt = require('jsonwebtoken');
|
||||
|
@ -12,65 +11,16 @@ function timeoutPromise(duration) {
|
|||
});
|
||||
}
|
||||
|
||||
var Devices = {};
|
||||
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;
|
||||
};
|
||||
var Devices = require('./lib/device-tracker');
|
||||
|
||||
module.exports.store = { Devices: Devices };
|
||||
module.exports.create = function (copts) {
|
||||
var deviceLists = {};
|
||||
copts.deviceLists = {};
|
||||
//var deviceLists = {};
|
||||
var activityTimeout = copts.activityTimeout || 2*60*1000;
|
||||
var pongTimeout = copts.pongTimeout || 10*1000;
|
||||
copts.Devices = Devices;
|
||||
var onTcpConnection = require('./lib/unwrap-tls').createTcpConnectionHandler(copts);
|
||||
|
||||
function onWsConnection(ws, upgradeReq) {
|
||||
console.log(ws);
|
||||
|
@ -204,7 +154,7 @@ module.exports.create = function (copts) {
|
|||
|
||||
token.domains.forEach(function (domainname) {
|
||||
console.log('domainname', domainname);
|
||||
Devices.add(deviceLists, domainname, token);
|
||||
Devices.add(copts.deviceLists, domainname, token);
|
||||
});
|
||||
remotes[jwtoken] = token;
|
||||
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
|
||||
// connections from trying to send more data across the connection.
|
||||
remote.domains.forEach(function (domainname) {
|
||||
Devices.remove(deviceLists, domainname, remote);
|
||||
Devices.remove(copts.deviceLists, domainname, remote);
|
||||
});
|
||||
remote.ws = null;
|
||||
remote.upgradeReq = null;
|
||||
|
@ -437,154 +387,9 @@ module.exports.create = function (copts) {
|
|||
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 {
|
||||
tcp: onTcpConnection
|
||||
, ws: onWsConnection
|
||||
, isClientDomain: Devices.exist.bind(null, deviceLists)
|
||||
, isClientDomain: Devices.exist.bind(null, copts.deviceLists)
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue