Merge branch 'greenlock'

# Conflicts:
#	bin/stunneld.js
This commit is contained in:
tigerbot 2017-04-05 10:46:17 -06:00
commit 5503714b53
4 changed files with 339 additions and 105 deletions

View File

@ -6,6 +6,15 @@ var pkg = require('../package.json');
var program = require('commander'); var program = require('commander');
var stunneld = require('../wstunneld.js'); var stunneld = require('../wstunneld.js');
var greenlock = require('greenlock');
function collectServernames(val, memo) {
val.split(/,/).forEach(function (servername) {
memo.push(servername.toLowerCase());
});
return memo;
}
function collectProxies(val, memo) { function collectProxies(val, memo) {
var vals = val.split(/,/g); var vals = val.split(/,/g);
@ -62,16 +71,15 @@ function collectPorts(val, memo) {
program program
.version(pkg.version) .version(pkg.version)
.option('--agree-tos', "Accept the Daplie and Let's Encrypt Terms of Service")
.option('--email <EMAIL>', "Email to use for Daplie and Let's Encrypt accounts")
.option('--serve <URL>', 'comma separated list of <proto>:<//><servername>:<port> to which matching incoming http and https should forward (reverse proxy). Ex: https://john.example.com,tls:*:1337', collectProxies, [ ]) .option('--serve <URL>', 'comma separated list of <proto>:<//><servername>:<port> to which matching incoming http and https should forward (reverse proxy). Ex: https://john.example.com,tls:*:1337', collectProxies, [ ])
.option('--ports <PORT>', 'comma separated list of ports on which to listen. Ex: 80,443,1337', collectPorts, [ ]) .option('--ports <PORT>', 'comma separated list of ports on which to listen. Ex: 80,443,1337', collectPorts, [ ])
.option('--servernames <STRING>', 'comma separated list of servernames to use for the admin interface. Ex: tunnel.example.com,tunnel.example.net', collectServernames, [ ])
.option('--secret <STRING>', 'the same secret used by stunneld (used for JWT authentication)') .option('--secret <STRING>', 'the same secret used by stunneld (used for JWT authentication)')
.parse(process.argv) .parse(process.argv)
; ;
if (!program.serve.length) {
throw new Error("must specify at least one server");
}
var portsMap = {}; var portsMap = {};
var servernamesMap = {}; var servernamesMap = {};
program.serve.forEach(function (proxy) { program.serve.forEach(function (proxy) {
@ -80,27 +88,108 @@ program.serve.forEach(function (proxy) {
portsMap[proxy.port] = true; portsMap[proxy.port] = true;
} }
}); });
program.servernames.forEach(function (name) {
servernamesMap[name] = true;
});
program.ports.forEach(function (port) { program.ports.forEach(function (port) {
portsMap[port] = true; portsMap[port] = true;
}); });
var opts = {}; program.servernames = Object.keys(servernamesMap);
opts.servernames = Object.keys(servernamesMap); if (!program.servernames.length) {
opts.ports = Object.keys(portsMap); throw new Error('must specify at least one server or servername');
if (!opts.ports.length) {
opts.ports = [ 80, 443 ];
} }
if (program.secret) { program.ports = Object.keys(portsMap);
opts.secret = program.secret; if (!opts.ports.length) {
} else { program.ports = [ 80, 443 ];
}
if (!program.secret) {
// TODO randomly generate and store in file? // TODO randomly generate and store in file?
console.warn("[SECURITY] using default --secret 'shhhhh'"); console.warn("[SECURITY] you must provide --secret '" + require('crypto').randomBytes(16).toString('hex') + "'");
opts.secret = 'shhhhh'; process.exit(1);
return;
} }
// TODO letsencrypt // TODO letsencrypt
opts.tlsOptions = require('localhost.daplie.com-certificates').merge({}); program.tlsOptions = require('localhost.daplie.com-certificates').merge({});
function approveDomains(opts, certs, cb) {
// This is where you check your database and associated
// email addresses with domains and agreements and such
// The domains being approved for the first time are listed in opts.domains
// Certs being renewed are listed in certs.altnames
if (certs) {
opts.domains = certs.altnames;
}
else {
if (-1 !== program.servernames.indexOf(opts.domain)) {
opts.email = program.email;
opts.agreeTos = program.agreeTos;
}
}
// NOTE: you can also change other options such as `challengeType` and `challenge`
// opts.challengeType = 'http-01';
// opts.challenge = require('le-challenge-fs').create({});
cb(null, { options: opts, certs: certs });
}
if (!program.email || !program.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("");
}
else {
program.greenlock = greenlock.create({
//server: 'staging'
server: 'https://acme-v01.api.letsencrypt.org/directory'
, challenges: {
// TODO dns-01
'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' })
}
, store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' })
, email: program.email
, agreeTos: program.agreeTos
, approveDomains: approveDomains
//, approvedDomains: program.servernames
});
}
//program.tlsOptions.SNICallback = program.greenlock.httpsOptions.SNICallback;
/*
program.middleware = program.greenlock.middleware(function (req, res) {
res.end('Hello, World!');
});
*/
require('../handlers').create(program); // adds directly to program for now...
//require('cluster-store').create().then(function (store) {
//program.store = store;
var net = require('net');
var netConnHandlers = stunneld.create(program); // { tcp, ws }
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({ server: (program.httpTunnelServer || program.httpServer) });
wss.on('connection', netConnHandlers.ws);
program.ports.forEach(function (port) {
var tcp3000 = net.createServer();
tcp3000.listen(port, function () {
console.log('listening on ' + port);
});
tcp3000.on('connection', netConnHandlers.tcp);
});
//});
stunneld.create(opts);
}()); }());

114
handlers.js Normal file
View File

@ -0,0 +1,114 @@
'use strict';
var http = require('http');
var tls = require('tls');
var packerStream = require('tunnel-packer').Stream;
var redirectHttps = require('redirect-https')();
module.exports.create = function (program) {
var tunnelAdminTlsOpts = {};
// Probably a reverse proxy on an internal network (or ACME challenge)
function notFound(req, res) {
console.log('req.socket.encrypted', req.socket.encrypted);
res.statusCode = 404;
res.end("File not found.\n");
}
program.httpServer = http.createServer(
program.greenlock && program.greenlock.middleware(notFound)
|| notFound
);
program.handleHttp = function (servername, socket) {
console.log("handleHttp('" + servername + "', socket)");
socket.__my_servername = servername;
program.httpServer.emit('connection', socket);
};
// Probably something that needs to be redirected to https
function redirectHttpsAndClose(req, res) {
res.setHeader('Connection', 'close');
redirectHttps(req, res);
}
program.httpInsecureServer = http.createServer(
program.greenlock && program.greenlock.middleware(redirectHttpsAndClose)
|| redirectHttpsAndClose
);
program.handleInsecureHttp = function (servername, socket) {
console.log("handleInsecureHttp('" + servername + "', socket)");
socket.__my_servername = servername;
program.httpInsecureServer.emit('connection', socket);
};
//
// SNI is not recogonized / cannot be handled
//
program.httpInvalidSniServer = http.createServer(function (req, res) {
res.end("You're doing strange things that make me feel uncomfortable. Please don't touch me there any more.");
});
program.tlsInvalidSniServer = tls.createServer(program.tlsOptions, function (tlsSocket) {
console.log('tls connection');
// things get a little messed up here
program.httpInvalidSniServer.emit('connection', tlsSocket);
});
program.httpsInvalid = function (servername, socket) {
// none of these methods work:
// httpsServer.emit('connection', socket); // this didn't work
// tlsServer.emit('connection', socket); // this didn't work either
//console.log('chunkLen', firstChunk.byteLength);
var myDuplex = packerStream.create(socket);
console.log('httpsInvalid servername', servername);
program.tlsInvalidSniServer.emit('connection', myDuplex);
socket.on('data', function (chunk) {
console.log('[' + Date.now() + '] socket data', chunk.byteLength);
myDuplex.push(chunk);
});
socket.on('error', function (err) {
console.error('[error] httpsInvalid TODO close');
console.error(err);
});
};
//
// To ADMIN / CONTROL PANEL of the Tunnel Server Itself
//
program.httpTunnelServer = http.createServer(function (req, res) {
console.log('req.socket.encrypted', req.socket.encrypted);
res.end('Hello, World!');
});
Object.keys(program.tlsOptions).forEach(function (key) {
tunnelAdminTlsOpts[key] = program.tlsOptions[key];
});
tunnelAdminTlsOpts.SNICallback = (program.greenlock && program.greenlock.httpsOptions && function (servername, cb) {
console.log("time to handle '" + servername + "'");
program.greenlock.httpsOptions.SNICallback(servername, cb);
}) || tunnelAdminTlsOpts.SNICallback;
program.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) {
console.log('tls connection');
// things get a little messed up here
(program.httpTunnelServer || program.httpServer).emit('connection', tlsSocket);
});
program.httpsTunnel = function (servername, socket) {
// none of these methods work:
// httpsServer.emit('connection', socket); // this didn't work
// tlsServer.emit('connection', socket); // this didn't work either
//console.log('chunkLen', firstChunk.byteLength);
var myDuplex = packerStream.create(socket);
console.log('httpsTunnel (Admin) servername', servername);
program.tlsTunnelServer.emit('connection', myDuplex);
socket.on('data', function (chunk) {
console.log('[' + Date.now() + '] socket data', chunk.byteLength);
myDuplex.push(chunk);
});
socket.on('error', function (err) {
console.error('[error] httpsTunnel (Admin) TODO close');
console.error(err);
});
};
};

View File

@ -48,6 +48,7 @@
"dependencies": { "dependencies": {
"cluster-store": "^2.0.4", "cluster-store": "^2.0.4",
"commander": "^2.9.0", "commander": "^2.9.0",
"greenlock": "^2.1.12",
"jsonwebtoken": "^7.1.9", "jsonwebtoken": "^7.1.9",
"localhost.daplie.com-certificates": "^1.2.3", "localhost.daplie.com-certificates": "^1.2.3",
"redirect-https": "^1.1.0", "redirect-https": "^1.1.0",

View File

@ -1,23 +1,86 @@
'use strict'; 'use strict';
var net = require('net');
var tls = require('tls');
var http = require('http');
var sni = require('sni'); var sni = require('sni');
var url = require('url'); var url = require('url');
var jwt = require('jsonwebtoken'); var jwt = require('jsonwebtoken');
var packer = require('tunnel-packer'); var packer = require('tunnel-packer');
var WebSocketServer = require('ws').Server;
var Devices = {};
Devices.replace = function (store, servername, newDevice) {
var devices = Devices.list(store, servername);
var oldDevice;
if (!devices.some(function (device, i) {
if ((device.deviceId && device.deviceId === newDevice.deviceId)
|| (device.servername && device.servername === newDevice.servername)) {
oldDevice = devices[i];
devices[i] = newDevice;
return true;
}
})) {
devices.push(newDevice);
store[servername] = devices;
}
return oldDevice;
};
Devices.remove = function (store, servername, newDevice) {
var devices = Devices.list(store, servername);
var oldDevice;
devices.some(function (device, i) {
if ((device.deviceId && device.deviceId === newDevice.deviceId)
|| (device.servername && device.servername === newDevice.servername)) {
oldDevice = devices.splice(i, 1);
return true;
}
});
return oldDevice;
};
Devices.list = function (store, servername) {
return store[servername] || [];
};
Devices.exist = function (store, servername) {
return (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.create = function (copts) { module.exports.create = function (copts) {
var deviceLists = {};
function onWsConnection(ws) { function onWsConnection(ws) {
var location = url.parse(ws.upgradeReq.url, true); var location = url.parse(ws.upgradeReq.url, true);
//var token = jwt.decode(location.query.access_token); var authn = (ws.upgradeReq.headers.authorization||'').split(/\s+/);
var jwtoken;
var token; var token;
try { try {
token = jwt.verify(location.query.access_token, secret); if (authn[0]) {
if ('basic' === authn[0].toLowerCase()) {
authn = new Buffer(authn[1], 'base64').toString('ascii').split(':');
}
/*
if (-1 !== [ 'bearer', 'jwk' ].indexOf(authn[0].toLowerCase())) {
jwtoken = authn[1];
}
*/
}
jwtoken = authn[1] || location.query.access_token;
} catch(e) {
jwtoken = null;
}
try {
token = jwt.verify(jwtoken, copts.secret);
} catch(e) { } catch(e) {
token = null; token = null;
} }
@ -36,6 +99,8 @@ module.exports.create = function (copts) {
return; return;
} }
//console.log('[wstunneld.js] DEBUG', token);
if (!Array.isArray(token.domains)) { if (!Array.isArray(token.domains)) {
if ('string' === typeof token.name) { if ('string' === typeof token.name) {
token.domains = [ token.name ]; token.domains = [ token.name ];
@ -50,13 +115,13 @@ module.exports.create = function (copts) {
var remote; var remote;
token.domains.some(function (domainname) { token.domains.some(function (domainname) {
remote = remotes[domainname]; remote = Devices.next(deviceLists, domainname);
return remote; return remote;
}); });
remote = remote || {}; remote = remote || {};
token.domains.forEach(function (domainname) { token.domains.forEach(function (domainname) {
console.log('domainname', domainname); console.log('domainname', domainname);
remotes[domainname] = remote; Devices.replace(deviceLists, domainname, remote);
}); });
var handlers = { var handlers = {
onmessage: function (opts) { onmessage: function (opts) {
@ -100,7 +165,8 @@ module.exports.create = function (copts) {
}; };
// TODO allow more than one remote per servername // TODO allow more than one remote per servername
remote.ws = ws; remote.ws = ws;
remote.servername = token.domains.join(','); remote.servername = (token.device && token.device.hostname) || token.domains.join(',');
remote.deviceId = (token.device && token.device.id) || null;
remote.id = packer.socketToId(ws.upgradeReq.socket); remote.id = packer.socketToId(ws.upgradeReq.socket);
console.log("remote.id", remote.id); console.log("remote.id", remote.id);
// TODO allow tls to be decrypted by server if client is actually a browser // TODO allow tls to be decrypted by server if client is actually a browser
@ -110,12 +176,14 @@ module.exports.create = function (copts) {
remote.clients = {}; remote.clients = {};
remote.handle = { address: null, handle: null }; remote.handle = { address: null, handle: null };
remote.unpacker = packer.create(handlers); remote.unpacker = packer.create(handlers);
ws.on('message', function (chunk) { remote.domains = token.domains;
function forwardMessage(chunk) {
console.log('message from home cloud to tunneler to browser', chunk.byteLength); console.log('message from home cloud to tunneler to browser', chunk.byteLength);
//console.log(chunk.toString()); //console.log(chunk.toString());
remote.unpacker.fns.addChunk(chunk); remote.unpacker.fns.addChunk(chunk);
}); }
ws.on('close', function () { function hangup() {
// the remote will handle closing its local connections // the remote will handle closing its local connections
Object.keys(remote.clients).forEach(function (cid) { Object.keys(remote.clients).forEach(function (cid) {
try { try {
@ -124,46 +192,23 @@ module.exports.create = function (copts) {
// ignore // ignore
} }
}); });
}); token.domains.forEach(function (domainname) {
ws.on('error', function () { Devices.remove(deviceLists, domainname, remote);
// ignore });
// the remote will retry if it wants to }
}); function die() {
hangup();
}
//store.set(token.name, remote.handle); ws.on('message', forwardMessage);
ws.on('close', hangup);
ws.on('error', die);
} }
function connectHttp(servername, socket) {
console.log("connectHttp('" + servername + "', socket)");
socket.__my_servername = servername;
redirectServer.emit('connection', socket);
}
function connectHttps(servername, socket) {
// none of these methods work:
// httpsServer.emit('connection', socket); // this didn't work
// tlsServer.emit('connection', socket); // this didn't work either
//console.log('chunkLen', firstChunk.byteLength);
var myDuplex = packer.Stream.create(socket);
console.log('connectHttps servername', servername);
tls3000.emit('connection', myDuplex);
socket.on('data', function (chunk) {
console.log('[' + Date.now() + '] socket data', chunk.byteLength);
myDuplex.push(chunk);
});
socket.on('error', function (err) {
console.error('[error] connectHttps TODO close');
console.error(err);
});
}
function pipeWs(servername, service, browser, remote) { function pipeWs(servername, service, browser, remote) {
console.log('pipeWs'); console.log('pipeWs');
//var remote = remotes[servername]; //var remote = deviceLists[servername];
var ws = remote.ws; var ws = remote.ws;
//var address = packer.socketToAddr(ws.upgradeReq.socket); //var address = packer.socketToAddr(ws.upgradeReq.socket);
var baddress = packer.socketToAddr(browser); var baddress = packer.socketToAddr(browser);
@ -259,14 +304,29 @@ module.exports.create = function (copts) {
var m; var m;
function tryTls() { function tryTls() {
if (!servername || (-1 !== selfnames.indexOf(servername)) || !remotes[servername]) { var nextDevice;
console.log('this is a server or an unknown');
connectHttps(servername, browser); if (-1 !== copts.servernames.indexOf(servername)) {
console.log("Lock and load, admin interface time!");
copts.httpsTunnel(servername, browser);
return; return;
} }
console.log("pipeWs(servername, service, socket, remotes['" + servername + "'])"); if (!servername) {
pipeWs(servername, service, browser, remotes[servername]); console.log("No SNI was given, so there's nothing we can do here");
copts.httpsInvalid(servername, browser);
return;
}
nextDevice = Devices.next(deviceLists, servername);
if (!nextDevice) {
console.log("No devices match the given servername");
copts.httpsInvalid(servername, browser);
return;
}
console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])");
pipeWs(servername, service, browser, nextDevice);
} }
// https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155 // https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
@ -286,17 +346,19 @@ module.exports.create = function (copts) {
console.log('servername', servername); console.log('servername', servername);
if (/HTTP\//i.test(str)) { if (/HTTP\//i.test(str)) {
service = 'http'; service = 'http';
if (/^\/\.well-known\/acme-challenge\//.test(str)) { // TODO disallow http entirely
// /^\/\.well-known\/acme-challenge\//.test(str)
if (/well-known/.test(str)) {
// HTTP // HTTP
if (remotes[servername]) { if (Devices.exist(deviceLists, servername)) {
pipeWs(servername, service, browser, remotes[servername]); pipeWs(servername, service, browser, Devices.next(deviceLists, servername));
return; return;
} }
connectHttp(servername, browser); copts.handleHttp(servername, browser);
} }
else { else {
// redirect to https // redirect to https
connectHttp(servername, browser); copts.handleInsecureHttp(servername, browser);
} }
return; return;
} }
@ -316,37 +378,5 @@ module.exports.create = function (copts) {
} }
var tlsOpts = copts.tlsOptions; return { tcp: onTcpConnection, ws: onWsConnection };
//var store = copts.store;
var remotes = {};
var selfnames = copts.servernames;
var secret = copts.secret;
var redirectHttps = require('redirect-https')();
var redirectServer = http.createServer(function (req, res) {
res.setHeader('Connection', 'close');
redirectHttps(req, res);
});
var httpServer = http.createServer(function (req, res) {
console.log('req.socket.encrypted', req.socket.encrypted);
res.end('Hello, World!');
});
var tls3000 = tls.createServer(tlsOpts, function (tlsSocket) {
console.log('tls connection');
// things get a little messed up here
httpServer.emit('connection', tlsSocket);
});
var wss = new WebSocketServer({ server: httpServer });
wss.on('connection', onWsConnection);
copts.ports.forEach(function (port) {
var tcp3000 = net.createServer();
tcp3000.listen(port, function () {
console.log('listening on ' + port);
});
tcp3000.on('connection', onTcpConnection);
});
}; };