add dynamic tcp and cleanup

This commit is contained in:
AJ ONeal 2018-06-01 06:41:32 +00:00
parent df3c1c3b04
commit b086e1c0a5
5 changed files with 214 additions and 143 deletions

View File

@ -63,7 +63,7 @@ function applyConfig(config) {
} }
function approveDomains(opts, certs, cb) { function approveDomains(opts, certs, cb) {
console.log('[debug] approveDomains', opts.domains); if (state.debug) { console.log('[debug] approveDomains', opts.domains); }
// This is where you check your database and associated // This is where you check your database and associated
// email addresses with domains and agreements and such // email addresses with domains and agreements and such
@ -75,11 +75,12 @@ function applyConfig(config) {
return; return;
} }
if (state.config.vhost) { if (!state.validHosts) { state.validHosts = {}; }
console.log('[sni] vhost checking is turned on'); if (!state.validHosts[opts.domains[0]] && state.config.vhost) {
if (state.debug) { console.log('[sni] vhost checking is turned on'); }
var vhost = state.config.vhost.replace(/:hostname/, opts.domains[0]); var vhost = state.config.vhost.replace(/:hostname/, opts.domains[0]);
require('fs').readdir(vhost, function (err, nodes) { require('fs').readdir(vhost, function (err, nodes) {
console.log('[sni] checking fs vhost'); if (state.debug) { console.log('[sni] checking fs vhost', opts.domains[0], !err); }
if (err) { check(); return; } if (err) { check(); return; }
if (nodes) { approve(); } if (nodes) { approve(); }
}); });
@ -87,8 +88,10 @@ function applyConfig(config) {
} }
function approve() { function approve() {
state.validHosts[opts.domains[0]] = true;
opts.email = state.config.email; opts.email = state.config.email;
opts.agreeTos = state.config.agreeTos; opts.agreeTos = state.config.agreeTos;
opts.communityMember = state.config.communityMember || state.config.greenlock.communityMember;
opts.challenges = { opts.challenges = {
// TODO dns-01 // TODO dns-01
'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' }) 'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' })
@ -98,41 +101,28 @@ function applyConfig(config) {
} }
function check() { function check() {
console.log('[sni] checking servername'); if (state.debug) { console.log('[sni] checking servername'); }
if (-1 !== state.servernames.indexOf(opts.domain) || -1 !== (state._servernames||[]).indexOf(opts.domain)) { if (-1 !== state.servernames.indexOf(opts.domain) || -1 !== (state._servernames||[]).indexOf(opts.domain)) {
approve(); approve();
} else { } else {
cb(new Error("failed the approval chain '" + opts.domains[0] + "'")); cb(new Error("failed the approval chain '" + opts.domains[0] + "'"));
} }
console.log('Approve Domains cb');
}
check();
} }
/* check();
if (!config.email || !config.agreeTos) {
console.error("You didn't specify --email <EMAIL> and --agree-tos");
console.error("(required for ACME / Let's Encrypt / Greenlock TLS/SSL certs)");
console.error("");
process.exit(1);
} }
*/
state.greenlock = Greenlock.create({ state.greenlock = Greenlock.create({
version: state.config.greenlock.version || 'draft-11' version: state.config.greenlock.version || 'draft-11'
, server: state.config.greenlock.server || 'https://acme-v02.api.letsencrypt.org/directory' , server: state.config.greenlock.server || 'https://acme-v02.api.letsencrypt.org/directory'
//, server: 'https://acme-staging-v02.api.letsencrypt.org/directory'
, store: require('le-store-certbot').create({ debug: true, webrootPath: '/tmp/acme-challenges' }) , store: require('le-store-certbot').create({ debug: state.config.debug || state.config.greenlock.debug, webrootPath: '/tmp/acme-challenges' })
, approveDomains: approveDomains , approveDomains: approveDomains
, telemetry: state.config.telemetry || state.config.greenlock.telemetry
, configDir: state.config.greenlock.configDir , configDir: state.config.greenlock.configDir
, debug: state.config.debug || state.config.greenlock.debug , debug: state.config.debug || state.config.greenlock.debug
//, approvedDomains: program.servernames
}); });
require('../handlers').create(state); // adds directly to config for now... require('../handlers').create(state); // adds directly to config for now...
@ -147,14 +137,13 @@ function applyConfig(config) {
wss.on('connection', netConnHandlers.ws); wss.on('connection', netConnHandlers.ws);
state.ports.forEach(function (port) { state.ports.forEach(function (port) {
if (state.tcp[port]) { if (state.tcp[port]) {
console.error("skipping previously added port " + port); console.warn("[cli] skipping previously added port " + port);
return; return;
} }
state.tcp[port] = net.createServer(); state.tcp[port] = net.createServer();
state.tcp[port].listen(port, function () { state.tcp[port].listen(port, function () {
console.log('listening plain TCP on ' + port); console.info('[cli] Listening for TCP connections on', port);
}); });
//state.tcp[port].on('connection', function (conn) { netConnHandlers.tcp(conn, port); });
state.tcp[port].on('connection', netConnHandlers.tcp); state.tcp[port].on('connection', netConnHandlers.tcp);
}); });
//}); //});

View File

@ -19,7 +19,7 @@ module.exports.create = function (state) {
var setupTlsOpts = { var setupTlsOpts = {
SNICallback: function (servername, cb) { SNICallback: function (servername, cb) {
if (!setupSniCallback) { if (!setupSniCallback) {
console.error("No way to get https certificates..."); console.error("[setup.SNICallback] No way to get https certificates...");
cb(new Error("telebitd sni setup fail")); cb(new Error("telebitd sni setup fail"));
return; return;
} }
@ -29,7 +29,6 @@ module.exports.create = function (state) {
// Probably a reverse proxy on an internal network (or ACME challenge) // Probably a reverse proxy on an internal network (or ACME challenge)
function notFound(req, res) { function notFound(req, res) {
console.log('req.socket.encrypted', req.socket.encrypted);
res.statusCode = 404; res.statusCode = 404;
res.end("File not found.\n"); res.end("File not found.\n");
} }
@ -79,10 +78,10 @@ module.exports.create = function (state) {
// tlsServer.emit('connection', socket); // this didn't work either // tlsServer.emit('connection', socket); // this didn't work either
//console.log('chunkLen', firstChunk.byteLength); //console.log('chunkLen', firstChunk.byteLength);
console.log('httpsInvalid servername', servername); console.log('[httpsInvalid] servername', servername);
//state.tlsInvalidSniServer.emit('connection', wrapSocket(socket)); //state.tlsInvalidSniServer.emit('connection', wrapSocket(socket));
var tlsInvalidSniServer = tls.createServer(state.tlsOptions, function (tlsSocket) { var tlsInvalidSniServer = tls.createServer(state.tlsOptions, function (tlsSocket) {
console.log('tls connection'); console.log('[tlsInvalid] tls connection');
// things get a little messed up here // things get a little messed up here
var httpInvalidSniServer = http.createServer(function (req, res) { var httpInvalidSniServer = http.createServer(function (req, res) {
if (!servername) { if (!servername) {
@ -118,10 +117,8 @@ module.exports.create = function (state) {
var serveAdmin = require('serve-static')(__dirname + '/admin', { redirect: true }); var serveAdmin = require('serve-static')(__dirname + '/admin', { redirect: true });
var finalhandler = require('finalhandler'); var finalhandler = require('finalhandler');
state.httpTunnelServer = http.createServer(function (req, res) { state.httpTunnelServer = http.createServer(function (req, res) {
console.log('admin req.socket.encrypted', req.socket.encrypted);
res.setHeader('connection', 'close'); res.setHeader('connection', 'close');
serveAdmin(req, res, function () { serveAdmin(req, res, function () {
console.log("serveAdmin fail");
finalhandler(req, res) finalhandler(req, res)
}); });
}); });
@ -129,17 +126,13 @@ module.exports.create = function (state) {
tunnelAdminTlsOpts[key] = state.tlsOptions[key]; tunnelAdminTlsOpts[key] = state.tlsOptions[key];
}); });
if (state.greenlock && state.greenlock.tlsOptions) { if (state.greenlock && state.greenlock.tlsOptions) {
console.log('greenlock tlsOptions for SNICallback'); tunnelAdminTlsOpts.SNICallback = state.greenlock.tlsOptions.SNICallback;
tunnelAdminTlsOpts.SNICallback = function (servername, cb) {
console.log("time to handle '" + servername + "'");
state.greenlock.tlsOptions.SNICallback(servername, cb);
};
} else { } else {
console.log('custom or null tlsOptions for SNICallback'); console.log('[Admin] custom or null tlsOptions for SNICallback');
tunnelAdminTlsOpts.SNICallback = tunnelAdminTlsOpts.SNICallback || noSniCallback('admin'); tunnelAdminTlsOpts.SNICallback = tunnelAdminTlsOpts.SNICallback || noSniCallback('admin');
} }
state.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) { state.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) {
console.log('(Admin) tls connection'); if (state.debug) { console.log('[Admin] new tls-terminated connection'); }
// things get a little messed up here // things get a little messed up here
(state.httpTunnelServer || state.httpServer).emit('connection', tlsSocket); (state.httpTunnelServer || state.httpServer).emit('connection', tlsSocket);
}); });
@ -152,7 +145,7 @@ module.exports.create = function (state) {
// tlsServer.emit('connection', socket); // this didn't work either // tlsServer.emit('connection', socket); // this didn't work either
//console.log('chunkLen', firstChunk.byteLength); //console.log('chunkLen', firstChunk.byteLength);
console.log('httpsTunnel (Admin) servername', servername); if (state.debug) { console.log('[Admin] new raw tls connection for', servername); }
state.tlsTunnelServer.emit('connection', wrapSocket(socket)); state.tlsTunnelServer.emit('connection', wrapSocket(socket));
}; };
@ -162,28 +155,26 @@ module.exports.create = function (state) {
var serveSetup = require('serve-static')(__dirname + '/admin/setup', { redirect: true }); var serveSetup = require('serve-static')(__dirname + '/admin/setup', { redirect: true });
var finalhandler = require('finalhandler'); var finalhandler = require('finalhandler');
state.httpSetupServer = http.createServer(function (req, res) { state.httpSetupServer = http.createServer(function (req, res) {
console.log('req.socket.encrypted', req.socket.encrypted);
if (req.socket.encrypted) { if (req.socket.encrypted) {
serveSetup(req, res, finalhandler(req, res)); serveSetup(req, res, finalhandler(req, res));
return; return;
} }
console.log('try greenlock middleware');
(state.greenlock && state.greenlock.middleware(redirectHttpsAndClose) (state.greenlock && state.greenlock.middleware(redirectHttpsAndClose)
|| redirectHttpsAndClose)(req, res, function () { || redirectHttpsAndClose)(req, res, function () {
console.log('fallthrough to setup ui'); console.log('[Setup] fallthrough to setup ui');
serveSetup(req, res, finalhandler(req, res)); serveSetup(req, res, finalhandler(req, res));
}); });
}); });
state.tlsSetupServer = tls.createServer(setupTlsOpts, function (tlsSocket) { state.tlsSetupServer = tls.createServer(setupTlsOpts, function (tlsSocket) {
console.log('tls connection'); console.log('[Setup] terminated tls connection');
// things get a little messed up here // things get a little messed up here
state.httpSetupServer.emit('connection', tlsSocket); state.httpSetupServer.emit('connection', tlsSocket);
}); });
state.tlsSetupServer.on('tlsClientError', function () { state.tlsSetupServer.on('tlsClientError', function () {
console.error('tlsClientError SetupServer'); console.error('[Setup] tlsClientError SetupServer');
}); });
state.httpsSetupServer = function (servername, socket) { state.httpsSetupServer = function (servername, socket) {
console.log('httpsTunnel (Setup) servername', servername); console.log('[Setup] raw tls connection for', servername);
state._servernames = [servername]; state._servernames = [servername];
state.config.agreeTos = true; // TODO: BUG XXX BAD, make user accept state.config.agreeTos = true; // TODO: BUG XXX BAD, make user accept
setupSniCallback = state.greenlock.tlsOptions.SNICallback || noSniCallback('setup'); setupSniCallback = state.greenlock.tlsOptions.SNICallback || noSniCallback('setup');
@ -194,31 +185,30 @@ module.exports.create = function (state) {
// vhost // vhost
// //
state.httpVhost = http.createServer(function (req, res) { state.httpVhost = http.createServer(function (req, res) {
console.log('httpVhost (local)'); if (state.debug) { console.log('[vhost] encrypted?', req.socket.encrypted); }
console.log('req.socket.encrypted', req.socket.encrypted);
var finalhandler = require('finalhandler'); var finalhandler = require('finalhandler');
// TODO compare SNI to hostname? // TODO compare SNI to hostname?
var host = (req.headers.host||'').toLowerCase().trim(); var host = (req.headers.host||'').toLowerCase().trim();
var serveSetup = require('serve-static')(state.config.vhost.replace(/:hostname/g, host), { redirect: true }); var serveVhost = require('serve-static')(state.config.vhost.replace(/:hostname/g, host), { redirect: true });
if (req.socket.encrypted) { serveSetup(req, res, finalhandler(req, res)); return; } if (req.socket.encrypted) { serveVhost(req, res, finalhandler(req, res)); return; }
console.log('try greenlock middleware for vhost'); if (!state.greenlock) {
(state.greenlock && state.greenlock.middleware(redirectHttpsAndClose) console.error("Cannot vhost without greenlock options");
|| redirectHttpsAndClose)(req, res, function () { res.end("Cannot vhost without greenlock options");
console.log('fallthrough to vhost serving???'); }
serveSetup(req, res, finalhandler(req, res));
}); state.greenlock.middleware(redirectHttpsAndClose);
}); });
state.tlsVhost = tls.createServer( state.tlsVhost = tls.createServer(
{ SNICallback: function (servername, cb) { { SNICallback: function (servername, cb) {
console.log('tlsVhost debug SNICallback', servername); if (state.debug) { console.log('[vhost] SNICallback for', servername); }
tunnelAdminTlsOpts.SNICallback(servername, cb); tunnelAdminTlsOpts.SNICallback(servername, cb);
} }
} }
, function (tlsSocket) { , function (tlsSocket) {
console.log('tlsVhost (local)'); if (state.debug) { console.log('tlsVhost (local)'); }
state.httpVhost.emit('connection', tlsSocket); state.httpVhost.emit('connection', tlsSocket);
} }
); );
@ -226,7 +216,7 @@ module.exports.create = function (state) {
console.error('tlsClientError Vhost'); console.error('tlsClientError Vhost');
}); });
state.httpsVhost = function (servername, socket) { state.httpsVhost = function (servername, socket) {
console.log('httpsVhost (local)', servername); if (state.debug) { console.log('[vhost] httpsVhost (local) for', servername); }
state.tlsVhost.emit('connection', wrapSocket(socket)); state.tlsVhost.emit('connection', wrapSocket(socket));
}; };
}; };

54
lib/pipe-ws.js Normal file
View File

@ -0,0 +1,54 @@
'use strict';
var Packer = require('proxy-packer');
module.exports = function pipeWs(servername, service, conn, remote, serviceport) {
var browserAddr = Packer.socketToAddr(conn);
var cid = Packer.addrToId(browserAddr);
browserAddr.service = service;
browserAddr.serviceport = serviceport;
browserAddr.name = servername;
conn.tunnelCid = cid;
var rid = Packer.socketToId(remote.upgradeReq.socket);
//if (state.debug) { console.log('[pipeWs] client', cid, '=> remote', rid, 'for', servername, 'via', service); }
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] remote', rid, ' => client', cid, 'error sending websocket message', err);
}
}
}
remote.clients[cid] = conn;
conn.on('data', function (chunk) {
//if (state.debug) { console.log('[pipeWs] client', cid, ' => remote', rid, chunk.byteLength, 'bytes'); }
sendWs(chunk);
});
conn.on('error', function (err) {
console.warn('[pipeWs] client', cid, 'connection error:', err);
});
conn.on('close', function (hadErr) {
//if (state.debug) { console.log('[pipeWs] client', cid, 'closing'); }
sendWs(null, hadErr ? 'error': 'end');
delete remote.clients[cid];
});
};

View File

@ -1,57 +1,10 @@
'use strict'; 'use strict';
var Packer = require('proxy-packer');
var sni = require('sni'); var sni = require('sni');
var pipeWs = require('./pipe-ws.js');
function pipeWs(servername, service, conn, remote, serviceport) { module.exports.createTcpConnectionHandler = function (state) {
console.log('[pipeWs] servername:', servername, 'service:', service); var Devices = state.Devices;
var browserAddr = Packer.socketToAddr(conn);
browserAddr.service = service;
browserAddr.serviceport = serviceport;
browserAddr.name = servername;
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, serviceport) { return function onTcpConnection(conn, serviceport) {
serviceport = serviceport || conn.localPort; serviceport = serviceport || conn.localPort;
@ -78,7 +31,7 @@ module.exports.createTcpConnectionHandler = function (copts) {
// defer after return (instead of being in many places) // defer after return (instead of being in many places)
function deferData(fn) { function deferData(fn) {
if (fn) { if (fn) {
copts[fn](servername, conn) state[fn](servername, conn)
} }
process.nextTick(function () { process.nextTick(function () {
conn.resume(); conn.resume();
@ -93,51 +46,50 @@ module.exports.createTcpConnectionHandler = function (copts) {
function tryTls() { function tryTls() {
var vhost; var vhost;
console.log(""); if (!state.servernames.length) {
console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)");
if (!copts.servernames.length) {
console.log("https => admin => setup => (needs bogus tls certs to start?)");
deferData('httpsSetupServer'); deferData('httpsSetupServer');
return; return;
} }
if (-1 !== copts.servernames.indexOf(servername)) { if (-1 !== state.servernames.indexOf(servername)) {
console.log("Lock and load, admin interface time!"); if (state.debug) { console.log("[Admin]", servername); }
deferData('httpsTunnel'); deferData('httpsTunnel');
return; return;
} }
if (copts.config.nowww && /^www\./i.test(servername)) { if (state.config.nowww && /^www\./i.test(servername)) {
console.log("TODO: use www bare redirect"); console.log("TODO: use www bare redirect");
} }
function run() { function run() {
if (!servername) { if (!servername) {
console.log("No SNI was given, so there's nothing we can do here"); if (state.debug) { console.log("No SNI was given, so there's nothing we can do here"); }
deferData('httpsInvalid'); deferData('httpsInvalid');
return; return;
} }
var nextDevice = Devices.next(copts.deviceLists, servername); var nextDevice = Devices.next(state.deviceLists, servername);
if (!nextDevice) { if (!nextDevice) {
console.log("No devices match the given servername"); if (state.debug) { console.log("No devices match the given servername"); }
deferData('httpsInvalid'); deferData('httpsInvalid');
return; return;
} }
console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])"); if (state.debug) { console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])"); }
deferData(); deferData();
pipeWs(servername, service, conn, nextDevice, serviceport); pipeWs(servername, service, conn, nextDevice, serviceport);
} }
if (copts.config.vhost) { // TODO don't run an fs check if we already know this is working elsewhere
console.log("VHOST path", copts.config.vhost); //if (!state.validHosts) { state.validHosts = {}; }
vhost = copts.config.vhost.replace(/:hostname/, (servername||'')); if (state.config.vhost) {
console.log("VHOST name", vhost); vhost = state.config.vhost.replace(/:hostname/, (servername||''));
//copts.httpsVhost(servername, conn); if (state.debug) { console.log("[tcp] [vhost]", state.config.vhost, "=>", vhost); }
//state.httpsVhost(servername, conn);
//return; //return;
require('fs').readdir(vhost, function (err, nodes) { require('fs').readdir(vhost, function (err, nodes) {
console.log("VHOST error?", err); if (state.debug && err) { console.log("VHOST error", err); }
if (err) { run(); return; } if (err) { run(); return; }
if (nodes) { deferData('httpsVhost'); } if (nodes) { deferData('httpsVhost'); }
}); });
@ -152,7 +104,7 @@ module.exports.createTcpConnectionHandler = function (copts) {
// TLS // TLS
service = 'https'; service = 'https';
servername = (sni(firstChunk)||'').toLowerCase(); servername = (sni(firstChunk)||'').toLowerCase();
console.log("tls hello servername:", servername); if (state.debug) { console.log("[tcp] tls hello from '" + servername + "'"); }
tryTls(); tryTls();
return; return;
} }
@ -161,13 +113,13 @@ module.exports.createTcpConnectionHandler = function (copts) {
str = firstChunk.toString(); str = firstChunk.toString();
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
servername = (m && m[1].toLowerCase() || '').split(':')[0]; servername = (m && m[1].toLowerCase() || '').split(':')[0];
console.log('servername', servername); if (state.debug) { console.log("[tcp] http hostname '" + servername + "'"); }
if (/HTTP\//i.test(str)) { if (/HTTP\//i.test(str)) {
if (!copts.servernames.length) { if (!state.servernames.length) {
console.log('copts.httpSetupServer', copts.httpSetupServer); console.info("[tcp] No admin servername. Entering setup mode.");
deferData(); deferData();
copts.httpSetupServer.emit('connection', conn); state.httpSetupServer.emit('connection', conn);
return; return;
} }
@ -176,9 +128,9 @@ module.exports.createTcpConnectionHandler = function (copts) {
// /^\/\.well-known\/acme-challenge\//.test(str) // /^\/\.well-known\/acme-challenge\//.test(str)
if (/well-known/.test(str)) { if (/well-known/.test(str)) {
// HTTP // HTTP
if (Devices.exist(copts.deviceLists, servername)) { if (Devices.exist(state.deviceLists, servername)) {
deferData(); deferData();
pipeWs(servername, service, conn, Devices.next(copts.deviceLists, servername), serviceport); pipeWs(servername, service, conn, Devices.next(state.deviceLists, servername), serviceport);
return; return;
} }
deferData('handleHttp'); deferData('handleHttp');

View File

@ -12,6 +12,7 @@ function timeoutPromise(duration) {
} }
var Devices = require('./lib/device-tracker'); var Devices = require('./lib/device-tracker');
var pipeWs = require('./lib/pipe-ws.js');
module.exports.store = { Devices: Devices }; module.exports.store = { Devices: Devices };
module.exports.create = function (state) { module.exports.create = function (state) {
@ -22,8 +23,70 @@ module.exports.create = function (state) {
state.Devices = Devices; state.Devices = Devices;
var onTcpConnection = require('./lib/unwrap-tls').createTcpConnectionHandler(state); var onTcpConnection = require('./lib/unwrap-tls').createTcpConnectionHandler(state);
// TODO Use a Single TCP Handler
// Issues:
// * dynamic ports are dedicated to a device or cluster
// * servernames could come in on ports that belong to a different device
// * servernames could come in that belong to no device
// * this could lead to an attack / security vulnerability with ACME certificates
// Solutions
// * Restrict dynamic ports to a particular device
// * Restrict the use of servernames
function onDynTcpConn(conn) {
var serviceport = this.address().port;
console.log('[DynTcpConn] new connection on', serviceport);
var remote = Devices.next(state.deviceLists, serviceport)
if (!remote) {
conn.write("[Sanity Error] I've got a blank space baby, but nowhere to write your name.");
conn.end();
try {
this.close();
} catch(e) {
console.error("[DynTcpConn] failed to close server:", e);
}
return;
}
conn.once('data', function (firstChunk) {
console.log("[DynTcp] examining firstChunk", serviceport);
conn.pause();
conn.unshift(firstChunk);
var servername;
var hostname;
var str;
var m;
if (22 === firstChunk[0]) {
servername = (sni(firstChunk)||'').toLowerCase();
} else if (firstChunk[0] > 32 && firstChunk[0] < 127) {
str = firstChunk.toString();
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
hostname = (m && m[1].toLowerCase() || '').split(':')[0];
}
if (servername || hostname) {
if (servername) {
conn.write("TLS with sni is allowed only on standard ports. If you've registered '" + servername + "' use port 443.");
} else {
conn.write("HTTP with Host headers is not allowed on dynamic ports. If you've registered '" + hostname + "' use port 80.");
}
conn.end();
return;
}
// pipeWs(servername, servicename, client, remote, serviceport)
// remote.clients is managed as part of the piping process
console.log("[DynTcp] piping to remote", serviceport);
pipeWs(null, 'tcp', conn, remote, serviceport)
process.nextTick(function () { conn.resume(); });
});
}
function onWsConnection(ws, upgradeReq) { function onWsConnection(ws, upgradeReq) {
console.log(ws); if (state.debug) { console.log('[ws] connection'); }
var socketId = Packer.socketToId(upgradeReq.socket); var socketId = Packer.socketToId(upgradeReq.socket);
var remotes = {}; var remotes = {};
@ -131,6 +194,7 @@ module.exports.create = function (state) {
token.ws = ws; token.ws = ws;
token.upgradeReq = upgradeReq; token.upgradeReq = upgradeReq;
token.clients = {}; token.clients = {};
token.dynamicPorts = [];
token.pausedConns = []; token.pausedConns = [];
ws._socket.on('drain', function () { ws._socket.on('drain', function () {
@ -153,11 +217,26 @@ module.exports.create = function (state) {
}); });
token.domains.forEach(function (domainname) { token.domains.forEach(function (domainname) {
console.log('domainname', domainname);
Devices.add(state.deviceLists, domainname, token); Devices.add(state.deviceLists, domainname, token);
}); });
function handleTcpServer() {
var serviceport = this.address().port;
console.info('[DynTcpConn] Port', serviceport, 'now open for', token.deviceId);
token.dynamicPorts.push(serviceport);
Devices.add(state.deviceLists, serviceport, token);
}
try {
token.server = require('net').createServer(onDynTcpConn).listen(0, handleTcpServer);
} catch(e) {
// what a wonderful problem it will be the day that this bug needs to be fixed
// (i.e. there are enough users to run out of ports)
console.error("Error assigning a dynamic port to a new connection:", e);
}
remotes[jwtoken] = token; remotes[jwtoken] = token;
console.log("added token '" + token.deviceId + "' to websocket", socketId); console.info("[ws] authorized", socketId, "for", token.deviceId);
return null; return null;
} }
@ -172,15 +251,22 @@ module.exports.create = function (state) {
remote.domains.forEach(function (domainname) { remote.domains.forEach(function (domainname) {
Devices.remove(state.deviceLists, domainname, remote); Devices.remove(state.deviceLists, domainname, remote);
}); });
remote.dynamicPorts.forEach(function (portnumber) {
Devices.remove(state.deviceLists, portnumber, remote);
});
remote.ws = null; remote.ws = null;
remote.upgradeReq = null; remote.upgradeReq = null;
remote.server.close(function () {
console.log("[DynTcpConn] closing server for ", remote.server.address().port);
});
remote.server = null;
// Close all of the existing browser connections associated with this websocket connection. // Close all of the existing browser connections associated with this websocket connection.
Object.keys(remote.clients).forEach(function (cid) { Object.keys(remote.clients).forEach(function (cid) {
closeBrowserConn(cid); closeBrowserConn(cid);
}); });
delete remotes[jwtoken]; delete remotes[jwtoken];
console.log("removed token '" + remote.deviceId + "' from websocket", socketId); console.log("[ws] removed token '" + remote.deviceId + "' from", socketId);
return null; return null;
} }
@ -236,7 +322,7 @@ module.exports.create = function (state) {
// We only ever send one command and we send it once, so we just hard coded the ID as 1. // We only ever send one command and we send it once, so we just hard coded the ID as 1.
if (cmd[0] === -1) { if (cmd[0] === -1) {
if (cmd[1]) { if (cmd[1]) {
console.log('received error response to hello from', socketId, cmd[1]); console.warn('received error response to hello from', socketId, cmd[1]);
} }
} }
else { else {
@ -262,7 +348,7 @@ module.exports.create = function (state) {
, onmessage: function (tun) { , onmessage: function (tun) {
var cid = Packer.addrToId(tun); var cid = Packer.addrToId(tun);
console.log("remote '" + logName() + "' has data for '" + cid + "'", tun.data.byteLength); if (state.debug) { console.log("remote '" + logName() + "' has data for '" + cid + "'", tun.data.byteLength); }
var browserConn = getBrowserConn(cid); var browserConn = getBrowserConn(cid);
if (!browserConn) { if (!browserConn) {
@ -319,7 +405,7 @@ module.exports.create = function (state) {
} }
, onerror: function (tun) { , onerror: function (tun) {
var cid = Packer.addrToId(tun); var cid = Packer.addrToId(tun);
console.log('[TunnelError]', cid, tun.message); console.warn('[TunnelError]', cid, tun.message);
closeBrowserConn(cid); closeBrowserConn(cid);
} }
}; };
@ -343,7 +429,7 @@ module.exports.create = function (state) {
// Otherwise we check to see if the pong has also timed out, and if not we send a ping // Otherwise we check to see if the pong has also timed out, and if not we send a ping
// and call this function again when the pong will have timed out. // and call this function again when the pong will have timed out.
else if (silent < activityTimeout + pongTimeout) { else if (silent < activityTimeout + pongTimeout) {
console.log('pinging', logName()); if (state.debug) { console.log('pinging', logName()); }
try { try {
ws.ping(); ws.ping();
} catch (err) { } catch (err) {
@ -355,7 +441,7 @@ module.exports.create = function (state) {
// Last case means the ping we sent before didn't get a response soon enough, so we // Last case means the ping we sent before didn't get a response soon enough, so we
// need to close the websocket connection. // need to close the websocket connection.
else { else {
console.log('home cloud', logName(), 'connection timed out'); console.warn('home cloud', logName(), 'connection timed out');
ws.close(1013, 'connection timeout'); ws.close(1013, 'connection timeout');
} }
} }
@ -367,14 +453,14 @@ module.exports.create = function (state) {
ws.on('pong', refreshTimeout); ws.on('pong', refreshTimeout);
ws.on('message', function forwardMessage(chunk) { ws.on('message', function forwardMessage(chunk) {
refreshTimeout(); refreshTimeout();
console.log('message from home cloud to tunneler to browser', chunk.byteLength); if (state.debug) { console.log('[ws] device => client : demultiplexing message ', chunk.byteLength, 'bytes'); }
//console.log(chunk.toString()); //console.log(chunk.toString());
unpacker.fns.addChunk(chunk); unpacker.fns.addChunk(chunk);
}); });
function hangup() { function hangup() {
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.log('home cloud', logName(), 'connection closing'); console.log('[ws] device hangup', logName(), 'connection closing');
Object.keys(remotes).forEach(function (jwtoken) { Object.keys(remotes).forEach(function (jwtoken) {
removeToken(jwtoken); removeToken(jwtoken);
}); });