207 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			207 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| var sni = require('sni');
 | |
| var pipeWs = require('./pipe-ws.js');
 | |
| 
 | |
| module.exports.createTcpConnectionHandler = function (state) {
 | |
|   var Devices = state.Devices;
 | |
| 
 | |
|   return function onTcpConnection(conn, serviceport) {
 | |
|     serviceport = serviceport || conn.localPort;
 | |
|     // this works when I put it here, but I don't know if it's tls yet here
 | |
|     // httpsServer.emit('connection', socket);
 | |
|     //tls3000.emit('connection', socket);
 | |
| 
 | |
|     //var tlsSocket = new tls.TLSSocket(socket, { secureContext: tls.createSecureContext(tlsOpts) });
 | |
|     //tlsSocket.on('data', function (chunk) {
 | |
|     //  console.log('dummy', chunk.byteLength);
 | |
|     //});
 | |
| 
 | |
|     //return;
 | |
|     //conn.once('data', function (firstChunk) {
 | |
|     //});
 | |
|     conn.once('readable', function () {
 | |
|       var firstChunk = conn.read();
 | |
|       var service = 'tcp';
 | |
|       var servername;
 | |
|       var str;
 | |
|       var m;
 | |
| 
 | |
|       if (!firstChunk) {
 | |
|         try {
 | |
|           conn.end();
 | |
|         } catch(e) {
 | |
|           console.error("[lib/unwrap-tls.js] Error:", e);
 | |
|           conn.destroy();
 | |
|         }
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       //conn.pause();
 | |
|       conn.unshift(firstChunk);
 | |
| 
 | |
|       // BUG XXX: this assumes that the packet won't be chunked smaller
 | |
|       // than the 'hello' or the point of the 'Host' header.
 | |
|       // This is fairly reasonable, but there are edge cases where
 | |
|       // it does not hold (such as manual debugging with telnet)
 | |
|       // and so it should be fixed at some point in the future
 | |
| 
 | |
|       // defer after return (instead of being in many places)
 | |
|       function deferData(fn) {
 | |
|         if (fn) {
 | |
|           state[fn](servername, conn);
 | |
|         } else {
 | |
|           console.error("[SANITY ERROR] '" + fn + "' doesn't have a state handler");
 | |
|         }
 | |
|         /*
 | |
|         process.nextTick(function () {
 | |
|           conn.resume();
 | |
|         });
 | |
|         */
 | |
|       }
 | |
| 
 | |
|       var httpOutcomes = {
 | |
|         missingServername: function () {
 | |
|           console.log("[debug] [http] missing servername");
 | |
|           // TODO use a more specific error page
 | |
|           deferData('handleInsecureHttp');
 | |
|         }
 | |
|       , requiresSetup: function () {
 | |
|           console.log("[debug] [http] requires setup");
 | |
|           // TODO Insecure connections for setup will not work on secure domains (i.e. .app)
 | |
|           state.httpSetupServer.emit('connection', conn);
 | |
|         }
 | |
|       , isInternal: function () {
 | |
|           console.log("[debug] [http] is known internally (admin)");
 | |
|           if (/well-known/.test(str)) {
 | |
|             deferData('handleHttp');
 | |
|           } else {
 | |
|             deferData('handleInsecureHttp');
 | |
|           }
 | |
|         }
 | |
|       , isVhost: function () {
 | |
|           console.log("[debug] [http] is vhost (normal server)");
 | |
|           if (/well-known/.test(str)) {
 | |
|             deferData('handleHttp');
 | |
|           } else {
 | |
|             deferData('handleInsecureHttp');
 | |
|           }
 | |
|         }
 | |
|       , assumeExternal: function () {
 | |
|           console.log("[debug] [http] assume external");
 | |
|           var service = 'http';
 | |
| 
 | |
|           if (!Devices.exist(state.deviceLists, servername)) {
 | |
|             // It would be better to just re-read the host header rather
 | |
|             // than creating a whole server object, but this is a "rare"
 | |
|             // case and I'm feeling lazy right now.
 | |
|             console.log("[debug] [http] no device connected");
 | |
|             state.createHttpInvalid(servername).emit('connection', conn);
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           // TODO make https redirect configurable on a per-domain basis
 | |
|           // /^\/\.well-known\/acme-challenge\//.test(str)
 | |
|           if (/well-known/.test(str)) {
 | |
|             // HTTP
 | |
|             console.log("[debug] [http] passthru");
 | |
|             pipeWs(servername, service, Devices.next(state.deviceLists, servername), conn, serviceport);
 | |
|             return;
 | |
|           } else {
 | |
|             console.log("[debug] [http] redirect to https");
 | |
|             deferData('handleInsecureHttp');
 | |
|           }
 | |
|         }
 | |
|       };
 | |
|       var tlsOutcomes = {
 | |
|         missingServername: function () {
 | |
|           if (state.debug) { console.log("No SNI was given, so there's nothing we can do here"); }
 | |
|           deferData('httpsInvalid');
 | |
|         }
 | |
|       , requiresSetup: function () {
 | |
|           console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)");
 | |
|           deferData('httpsSetupServer');
 | |
|         }
 | |
|       , isInternal: function () {
 | |
|           if (state.debug) { console.log("[Admin]", servername); }
 | |
|           deferData('httpsTunnel');
 | |
|         }
 | |
|       , isVhost: function (vhost) {
 | |
|           if (state.debug) { console.log("[tcp] [vhost]", state.config.vhost, "=>", vhost); }
 | |
|           deferData('httpsVhost');
 | |
|         }
 | |
|       , assumeExternal: function () {
 | |
|          var nextDevice = Devices.next(state.deviceLists, servername);
 | |
|           if (!nextDevice) {
 | |
|             if (state.debug) { console.log("No devices match the given servername"); }
 | |
|             deferData('httpsInvalid');
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           if (state.debug) { console.log("pipeWs(servername, service, deviceLists['" + servername + "'], socket)"); }
 | |
|           pipeWs(servername, service, nextDevice, conn, serviceport);
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       function handleConnection(outcomes) {
 | |
|         var vhost;
 | |
| 
 | |
|         // No routing information available
 | |
|         if (!servername) { outcomes.missingServername(); return; }
 | |
|         // Server needs to be set up
 | |
|         if (!state.servernames.length) { outcomes.requiresSetup(); return; }
 | |
|         // This is one of the admin domains
 | |
|         if (-1 !== state.servernames.indexOf(servername)) { outcomes.isInternal(); return; }
 | |
| 
 | |
|         // TODO don't run an fs check if we already know this is working elsewhere
 | |
|         //if (!state.validHosts) { state.validHosts = {}; }
 | |
|         if (state.config.vhost) {
 | |
|           vhost = state.config.vhost.replace(/:hostname/, servername);
 | |
|           require('fs').readdir(vhost, function (err, nodes) {
 | |
|             if (state.debug && err) { console.log("VHOST error", err); }
 | |
|             if (err || !nodes) { outcomes.assumeExternal(); return; }
 | |
|             outcomes.isVhost(vhost);
 | |
|           });
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         outcomes.assumeExternal();
 | |
|       }
 | |
| 
 | |
|       // https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
 | |
|       if (22 === firstChunk[0]) {
 | |
|         // TLS
 | |
|         service = 'https';
 | |
|         servername = (sni(firstChunk)||'').toLowerCase().trim();
 | |
|         if (state.debug) { console.log("[tcp] tls hello from '" + servername + "'"); }
 | |
|         handleConnection(tlsOutcomes);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (firstChunk[0] > 32 && firstChunk[0] < 127) {
 | |
|         // (probably) HTTP
 | |
|         str = firstChunk.toString();
 | |
|         m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
 | |
|         servername = (m && m[1].toLowerCase() || '').split(':')[0];
 | |
|         if (state.debug) { console.log("[tcp] http hostname '" + servername + "'"); }
 | |
| 
 | |
|         if (/HTTP\//i.test(str)) {
 | |
|           handleConnection(httpOutcomes);
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       console.error("Got unexpected connection", str);
 | |
|       conn.write(JSON.stringify({ error: {
 | |
|         message: "not sure what you were trying to do there..."
 | |
|       , code: 'E_INVALID_PROTOCOL' }
 | |
|       }));
 | |
|       conn.end();
 | |
|     });
 | |
|     conn.on('error', function (err) {
 | |
|       console.error('[error] tcp socket raw TODO forward and close');
 | |
|       console.error(err);
 | |
|     });
 | |
|   };
 | |
| };
 |