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