132 lines
4.1 KiB
JavaScript
132 lines
4.1 KiB
JavaScript
'use strict';
|
|
|
|
function httpsTunnel(servername, conn) {
|
|
console.error('tunnel server received encrypted connection to', servername);
|
|
conn.end();
|
|
}
|
|
function handleHttp(servername, conn) {
|
|
console.error('tunnel server received un-encrypted connection to', servername);
|
|
conn.end([
|
|
'HTTP/1.1 404 Not Found'
|
|
, 'Date: ' + (new Date()).toUTCString()
|
|
, 'Connection: close'
|
|
, 'Content-Type: text/html'
|
|
, 'Content-Length: 9'
|
|
, ''
|
|
, 'Not Found'
|
|
].join('\r\n'));
|
|
}
|
|
function rejectNonWebsocket(req, res) {
|
|
// status code 426 = Upgrade Required
|
|
res.statusCode = 426;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.send({error: { message: 'Only websockets accepted for tunnel server' }});
|
|
}
|
|
|
|
var defaultConfig = {
|
|
servernames: []
|
|
, secret: null
|
|
};
|
|
var tunnelFuncs = {
|
|
// These functions should not be called because connections to the admin domains
|
|
// should already be decrypted, and connections to non-client domains should never
|
|
// be given to us in the first place.
|
|
httpsTunnel: httpsTunnel
|
|
, httpsInvalid: httpsTunnel
|
|
// These function should not be called because ACME challenges should be handled
|
|
// before admin domain connections are given to us, and the only non-encrypted
|
|
// client connections that should be given to us are ACME challenges.
|
|
, handleHttp: handleHttp
|
|
, handleInsecureHttp: handleHttp
|
|
};
|
|
|
|
module.exports.create = function (deps, config) {
|
|
var equal = require('deep-equal');
|
|
var enableDestroy = require('server-destroy');
|
|
var currentOpts = Object.assign({}, defaultConfig);
|
|
|
|
var httpServer, wsServer, stunneld;
|
|
function start() {
|
|
if (httpServer || wsServer || stunneld) {
|
|
throw new Error('trying to start already started tunnel server');
|
|
}
|
|
httpServer = require('http').createServer(rejectNonWebsocket);
|
|
enableDestroy(httpServer);
|
|
|
|
wsServer = new (require('ws').Server)({ server: httpServer });
|
|
|
|
var tunnelOpts = Object.assign({}, tunnelFuncs, currentOpts);
|
|
stunneld = require('stunneld').create(tunnelOpts);
|
|
wsServer.on('connection', stunneld.ws);
|
|
}
|
|
|
|
function stop() {
|
|
if (!httpServer || !wsServer || !stunneld) {
|
|
throw new Error('trying to stop unstarted tunnel server (or it got into semi-initialized state');
|
|
}
|
|
wsServer.close();
|
|
wsServer = null;
|
|
httpServer.destroy();
|
|
httpServer = null;
|
|
// Nothing to close here, just need to set it to null to allow it to be garbage-collected.
|
|
stunneld = null;
|
|
}
|
|
|
|
function updateConf() {
|
|
var newOpts = Object.assign({}, defaultConfig, config.tunnelServer);
|
|
if (!Array.isArray(newOpts.servernames)) {
|
|
newOpts.servernames = [];
|
|
}
|
|
var trimmedOpts = {
|
|
servernames: newOpts.servernames.slice().sort()
|
|
, secret: newOpts.secret
|
|
};
|
|
|
|
if (equal(trimmedOpts, currentOpts)) {
|
|
return;
|
|
}
|
|
currentOpts = trimmedOpts;
|
|
|
|
// Stop what's currently running, then if we are still supposed to be running then we
|
|
// can start it again with the updated options. It might be possible to make use of
|
|
// the existing http and ws servers when the config changes, but I'm not sure what
|
|
// state the actions needed to close all existing connections would put them in.
|
|
if (httpServer || wsServer || stunneld) {
|
|
stop();
|
|
}
|
|
if (currentOpts.servernames.length && currentOpts.secret) {
|
|
start();
|
|
}
|
|
}
|
|
process.nextTick(updateConf);
|
|
|
|
return {
|
|
isAdminDomain: function (domain) {
|
|
return currentOpts.servernames.indexOf(domain) !== -1;
|
|
}
|
|
, handleAdminConn: function (conn) {
|
|
if (!httpServer) {
|
|
console.error(new Error('handleAdminConn called with no active tunnel server'));
|
|
conn.end();
|
|
} else {
|
|
return httpServer.emit('connection', conn);
|
|
}
|
|
}
|
|
|
|
, isClientDomain: function (domain) {
|
|
if (!stunneld) { return false; }
|
|
return stunneld.isClientDomain(domain);
|
|
}
|
|
, handleClientConn: function (conn) {
|
|
if (!stunneld) {
|
|
console.error(new Error('handleClientConn called with no active tunnel server'));
|
|
conn.end();
|
|
} else {
|
|
return stunneld.tcp(conn);
|
|
}
|
|
}
|
|
|
|
, updateConf
|
|
};
|
|
};
|