'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 }; };