diff --git a/lib/tcp/http.js b/lib/tcp/http.js index 3026ad9..00ea688 100644 --- a/lib/tcp/http.js +++ b/lib/tcp/http.js @@ -162,8 +162,8 @@ module.exports.create = function (deps, conf, tcpMods) { return false; } - if (deps.tunnelServer.isClientDomain(separatePort(headers.host).host)) { - deps.tunnelServer.handleClientConn(conn); + if (deps.stunneld.isClientDomain(separatePort(headers.host).host)) { + deps.stunneld.handleClientConn(conn); process.nextTick(function () { conn.unshift(opts.firstChunk); conn.resume(); @@ -214,8 +214,8 @@ module.exports.create = function (deps, conf, tcpMods) { return emitConnection(adminServer, conn, opts); } - if (deps.tunnelServer.isAdminDomain(host)) { - deps.tunnelServer.handleAdminConn(conn); + if (deps.stunneld.isAdminDomain(host)) { + deps.stunneld.handleAdminConn(conn); process.nextTick(function () { conn.unshift(opts.firstChunk); conn.resume(); diff --git a/lib/tcp/index.js b/lib/tcp/index.js index 6dc01a7..d5d88c7 100644 --- a/lib/tcp/index.js +++ b/lib/tcp/index.js @@ -229,7 +229,6 @@ module.exports.create = function (deps, config) { } }; deps.tunnelClients = require('../tunnel-client-manager').create(deps, config); - deps.tunnelServer = require('../tunnel-server-manager').create(deps, config); function updateListeners() { var current = listeners.list(); diff --git a/lib/tcp/tls.js b/lib/tcp/tls.js index e75355c..60868a0 100644 --- a/lib/tcp/tls.js +++ b/lib/tcp/tls.js @@ -291,8 +291,8 @@ module.exports.create = function (deps, config, tcpMods) { return; } - if (deps.tunnelServer.isClientDomain(opts.servername)) { - deps.tunnelServer.handleClientConn(socket); + if (deps.stunneld.isClientDomain(opts.servername)) { + deps.stunneld.handleClientConn(socket); if (!opts.hyperPeek) { process.nextTick(function () { socket.unshift(opts.firstChunk); diff --git a/lib/tunnel-server-manager.js b/lib/tunnel-server-manager.js index cdada29..a2963e3 100644 --- a/lib/tunnel-server-manager.js +++ b/lib/tunnel-server-manager.js @@ -1,61 +1,131 @@ 'use strict'; -module.exports.create = function (deps, config) { - if (!config.tunnelServer || !Array.isArray(config.tunnelServer.servernames) || !config.tunnelServer.secret) { - return { - isAdminDomain: function () { return false; } - , isClientDomain: function () { return false; } - }; - } +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 tunnelOpts = Object.assign({}, config.tunnelServer); - // This function should not be called because connections to the admin domains +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. - tunnelOpts.httpsTunnel = function (servername, conn) { - console.error('tunnel server received encrypted connection to', servername); - conn.end(); - }; - tunnelOpts.httpsInvalid = tunnelOpts.httpsTunnel; - // This function should not be called because ACME challenges should be handled + 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. - tunnelOpts.handleHttp = function (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')); - }; - tunnelOpts.handleInsecureHttp = tunnelOpts.handleHttp; +, handleHttp: handleHttp +, handleInsecureHttp: handleHttp +}; - var tunnelServer = require('stunneld').create(tunnelOpts); +module.exports.create = function (deps, config) { + var equal = require('deep-equal'); + var enableDestroy = require('server-destroy'); + var currentOpts = Object.assign({}, defaultConfig); - var httpServer = require('http').createServer(function (req, res) { - // status code 426 = Upgrade Required - res.statusCode = 426; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({error: { - message: 'Only websockets accepted for tunnel server' - }})); - }); - var wsServer = new (require('ws').Server)({ server: httpServer }); - wsServer.on('connection', tunnelServer.ws); + 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 config.tunnelServer.servernames.indexOf(domain) !== -1; + return currentOpts.servernames.indexOf(domain) !== -1; } , handleAdminConn: function (conn) { - httpServer.emit('connection', conn); + if (!httpServer) { + console.error(new Error('handleAdminConn called with no active tunnel server')); + conn.end(); + } else { + return httpServer.emit('connection', conn); + } } - , isClientDomain: tunnelServer.isClientDomain - , handleClientConn: tunnelServer.tcp + , 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 }; }; diff --git a/lib/worker.js b/lib/worker.js index 956674e..2e3629f 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -53,6 +53,7 @@ function create(conf) { , mdns: require('./mdns').create(deps, conf) , udp: require('./udp').create(deps, conf) , tcp: require('./tcp').create(deps, conf) + , stunneld: require('./tunnel-server-manager').create(deps, config) }; Object.assign(deps, modules); diff --git a/package-lock.json b/package-lock.json index e7151c4..804184b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2086,7 +2086,7 @@ } }, "stunnel": { - "version": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#cad0e561fbea5c5dbbf5fc10ed95833dd3573ebc", + "version": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#114847e31abe9a0c5f0598b892dd98b37fe9622e", "requires": { "bluebird": "3.5.0", "commander": "2.9.0", @@ -2098,7 +2098,7 @@ } }, "stunneld": { - "version": "git+https://git.daplie.com/Daplie/node-tunnel-server.git#54ca2782dde84b3d2c61a3257f7d859b7012ea59", + "version": "git+https://git.daplie.com/Daplie/node-tunnel-server.git#ae91fd5049251ed1f9fcd6806d7b9872454c67db", "requires": { "bluebird": "3.5.0", "cluster-store": "2.0.6", @@ -2108,8 +2108,15 @@ "localhost.daplie.me-certificates": "1.3.5", "redirect-https": "1.1.4", "sni": "1.0.0", - "tunnel-packer": "1.3.0", + "tunnel-packer": "1.4.0", "ws": "2.3.1" + }, + "dependencies": { + "tunnel-packer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tunnel-packer/-/tunnel-packer-1.4.0.tgz", + "integrity": "sha512-99GYAtKnbMVd87hQMxiR/Pq62jOWzOH/K6EOs87nU6U4p5uso+fZyYuO+upb+hhonXuNI/sZR/ByVxPFrnzMog==" + } } }, "terminal-forms.js": {