WIP refactor for greenlock

This commit is contained in:
AJ ONeal 2017-04-04 22:31:58 -04:00
parent 594a0786c9
commit 176e1c06a3
4 changed files with 264 additions and 94 deletions

View File

@ -7,6 +7,15 @@ var pkg = require('../package.json');
var program = require('commander');
var url = require('url');
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) {
var vals = val.split(/,/g);
@ -68,8 +77,11 @@ program
.action(function (url) {
program.url = url;
})
.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('--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)')
.parse(process.argv)
;
@ -115,18 +127,82 @@ if (!program.serve.length) {
throw new Error("must specify at least on server");
}
// TODO letsencrypt
program.tlsOptions = require('localhost.daplie.com-certificates').merge({});
if (!program.secret) {
// TODO randomly generate and store in file?
console.warn("[SECURITY] using default --secret 'shhhhh'");
program.secret = 'shhhhh';
console.warn("[SECURITY] you must provide --secret '" + require('crypto').randomBytes(16).toString('hex') + "'");
process.exit(1);
return;
}
// TODO letsencrypt
program.tlsOptions = require('localhost.daplie.com-certificates').merge({});
/*
program.tlsOptions.SNICallback = program.greenlock.SNICallback;
program.middleware = program.greenlock.middleware(function (req, res) {
res.end('Hello, World!');
});
*/
/*
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 {
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 });
}
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: program.servernames // approveDomains
});
*/
require('../handlers').create(program); // adds directly to program for now...
//require('cluster-store').create().then(function (store) {
//program.store = store;
stunneld.create(program);
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);
});
//});
}());

74
handlers.js Normal file
View File

@ -0,0 +1,74 @@
'use strict';
var http = require('http');
var tls = require('tls');
var packerStream = require('tunnel-packer').Stream;
module.exports.create = function (program) {
program.httpServer = http.createServer(function (req, res) {
console.log('req.socket.encrypted', req.socket.encrypted);
res.end("Look! I can do a thing!");
});
program.httpTunnelServer = http.createServer(function (req, res) {
console.log('req.socket.encrypted', req.socket.encrypted);
res.end('Hello, World!');
});
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.tlsTunnelServer = tls.createServer(program.tlsOptions, function (tlsSocket) {
console.log('tls connection');
// things get a little messed up here
(program.httpTunnelServer || program.httpServer).emit('connection', tlsSocket);
});
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.handleInsecureHttp = function (servername, socket) {
console.log("handleInsecureHttp('" + servername + "', socket)");
socket.__my_servername = servername;
program.httpInsecureServer.emit('connection', socket);
};
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);
});
};
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 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 TODO close');
console.error(err);
});
};
};

View File

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

View File

@ -1,23 +1,84 @@
'use strict';
var net = require('net');
var tls = require('tls');
var http = require('http');
var sni = require('sni');
var url = require('url');
var jwt = require('jsonwebtoken');
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);
if (devices._index >= devices.length) {
devices._index = 0;
}
devices._index = (devices._index || 0) + 1;
return devices[devices._index];
};
module.exports.store = { Devices: Devices };
module.exports.create = function (copts) {
var deviceLists = {};
function onWsConnection(ws) {
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;
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) {
token = null;
}
@ -36,6 +97,8 @@ module.exports.create = function (copts) {
return;
}
//console.log('[wstunneld.js] DEBUG', token);
if (!Array.isArray(token.domains)) {
if ('string' === typeof token.name) {
token.domains = [ token.name ];
@ -50,13 +113,13 @@ module.exports.create = function (copts) {
var remote;
token.domains.some(function (domainname) {
remote = remotes[domainname];
remote = Devices.next(deviceLists, domainname);
return remote;
});
remote = remote || {};
token.domains.forEach(function (domainname) {
console.log('domainname', domainname);
remotes[domainname] = remote;
Devices.replace(deviceLists, domainname, remote);
});
var handlers = {
onmessage: function (opts) {
@ -100,7 +163,8 @@ module.exports.create = function (copts) {
};
// TODO allow more than one remote per servername
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);
console.log("remote.id", remote.id);
// TODO allow tls to be decrypted by server if client is actually a browser
@ -110,12 +174,14 @@ module.exports.create = function (copts) {
remote.clients = {};
remote.handle = { address: null, handle: null };
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(chunk.toString());
remote.unpacker.fns.addChunk(chunk);
});
ws.on('close', function () {
}
function hangup() {
// the remote will handle closing its local connections
Object.keys(remote.clients).forEach(function (cid) {
try {
@ -124,46 +190,23 @@ module.exports.create = function (copts) {
// ignore
}
});
});
ws.on('error', function () {
// ignore
// the remote will retry if it wants to
});
token.domains.forEach(function (domainname) {
Devices.remove(deviceLists, domainname, remote);
});
}
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) {
console.log('pipeWs');
//var remote = remotes[servername];
//var remote = deviceLists[servername];
var ws = remote.ws;
//var address = packer.socketToAddr(ws.upgradeReq.socket);
var baddress = packer.socketToAddr(browser);
@ -259,14 +302,22 @@ module.exports.create = function (copts) {
var m;
function tryTls() {
if (!servername || (-1 !== selfnames.indexOf(servername)) || !remotes[servername]) {
console.log('this is a server or an unknown');
connectHttps(servername, browser);
var nextDevice;
if (-1 !== copts.servernames.indexOf(servername)) {
copts.httpsTunnel(servername, browser);
return;
}
console.log("pipeWs(servername, service, socket, remotes['" + servername + "'])");
pipeWs(servername, service, browser, remotes[servername]);
nextDevice = Devices.next(deviceLists, servername);
if (!servername || !nextDevice) {
console.log('this is a server or an unknown');
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
@ -286,17 +337,17 @@ module.exports.create = function (copts) {
console.log('servername', servername);
if (/HTTP\//i.test(str)) {
service = 'http';
if (/^\/\.well-known\/acme-challenge\//.test(str)) {
if (/well-known/.test(str)) {
// HTTP
if (remotes[servername]) {
pipeWs(servername, service, browser, remotes[servername]);
if (Devices.exist(deviceLists, servername)) {
pipeWs(servername, service, browser, Devices.next(deviceLists, servername));
return;
}
connectHttp(servername, browser);
copts.handleInsecureHttp(servername, browser);
}
else {
// redirect to https
connectHttp(servername, browser);
copts.handleInsecureHttp(servername, browser);
}
return;
}
@ -316,37 +367,5 @@ module.exports.create = function (copts) {
}
var tlsOpts = copts.tlsOptions;
//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);
});
return { tcp: onTcpConnection, ws: onWsConnection };
};