Compare commits
	
		
			No commits in common. "531337bbc90b7e2343e2d5816f97fb71e07a09d5" and "5380a519bdb63ec8bc57196fa93a2069fcddc60c" have entirely different histories.
		
	
	
		
			531337bbc9
			...
			5380a519bd
		
	
		
							
								
								
									
										20
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.md
									
									
									
									
									
								
							| @ -173,23 +173,3 @@ The user and group `telebit` should be created. | |||||||
| # Linux | # Linux | ||||||
| sudo setcap 'cap_net_bind_service=+ep' $(which node) | sudo setcap 'cap_net_bind_service=+ep' $(which node) | ||||||
| ``` | ``` | ||||||
| 
 |  | ||||||
| API |  | ||||||
| === |  | ||||||
| 
 |  | ||||||
| The authentication method is abstract so that it can easily be implemented for various users and use cases. |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| // bin/telebit-relay.js |  | ||||||
| state.authenticate()                  // calls either state.extensions.authenticate or state.defaults.authenticate |  | ||||||
|                                       // which, in turn, calls Server.onAuth() |  | ||||||
| 
 |  | ||||||
| state.extensions = require('../lib/extensions'); |  | ||||||
| state.extensions.authenticate({ |  | ||||||
|   state: state                        // lib/relay.js in-memory state |  | ||||||
| , auth: 'xyz.abc.123'                 // arbitrary token, typically a JWT (default handler) |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| // lib/relay.js |  | ||||||
| Server.onAuth(state, srv, rawAuth, validatedTokenData); |  | ||||||
| ``` |  | ||||||
|  | |||||||
| @ -1,28 +1,25 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| var Devices = module.exports; | var Devices = module.exports; | ||||||
| Devices.addPort = function (store, serverport, newDevice) { | Devices.add = function (store, servername, newDevice) { | ||||||
|   // TODO make special
 |   if (!store._devices) { store._devices = {}; } | ||||||
|   return Devices.add(store, serverport, newDevice, true); |  | ||||||
| }; |  | ||||||
| Devices.add = function (store, servername, newDevice, isPort) { |  | ||||||
|   if (isPort) { |  | ||||||
|     if (!store._ports) { store._ports = {}; } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // add domain (also handles ports at the moment)
 |  | ||||||
|   if (!store._domains) { store._domains = {}; } |   if (!store._domains) { store._domains = {}; } | ||||||
|   if (!store._domains[servername]) { store._domains[servername] = []; } |   if (!store._domains[servername]) { | ||||||
|   store._domains[servername].push(newDevice); |     store._domains[servername] = []; | ||||||
|  |   } | ||||||
|  |   var devices = store._domains[servername]; | ||||||
|  |   devices.push(newDevice); | ||||||
| 
 | 
 | ||||||
|   // add device
 |  | ||||||
|   // TODO only use a device id 
 |   // TODO only use a device id 
 | ||||||
|   var devId = newDevice.id || servername; |   var devId = newDevice.id || servername; | ||||||
|   if (!store._devices) { store._devices = {}; } |  | ||||||
|   if (!store._devices[devId]) { |   if (!store._devices[devId]) { | ||||||
|     store._devices[devId] = newDevice; |     store._devices[devId] = newDevice; | ||||||
|     if (!store._devices[devId].domainsMap) { store._devices[devId].domainsMap = {}; } |     if (!store._devices[devId].domains) { | ||||||
|     if (!store._devices[devId].domainsMap[servername]) { store._devices[devId].domainsMap[servername] = true; } |       store._devices[devId].domains = {}; | ||||||
|  |     } | ||||||
|  |     if (!store._devices[devId].domains[servername]) { | ||||||
|  |       store._devices[devId].domains[servername] = true; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| Devices.alias = function (store, servername, alias) { | Devices.alias = function (store, servername, alias) { | ||||||
| @ -49,8 +46,8 @@ Devices.remove = function (store, servername, device) { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // unlink this domain from this device
 |   // unlink this domain from this device
 | ||||||
|   var domainsMap = store._devices[devices[index].id || servername].domainsMap; |   var domains = store._devices[devices[index].id || servername].domains; | ||||||
|   delete domainsMap[servername]; |   delete domains[servername]; | ||||||
|   /* |   /* | ||||||
|   // remove device if no domains remain
 |   // remove device if no domains remain
 | ||||||
|   // nevermind, a device can hang around in limbo for a bit
 |   // nevermind, a device can hang around in limbo for a bit
 | ||||||
| @ -79,21 +76,9 @@ Devices.close = function (store, device) { | |||||||
|   // TODO double check that all domains are removed
 |   // TODO double check that all domains are removed
 | ||||||
|   if (id) { delete store._devices[id]; } |   if (id) { delete store._devices[id]; } | ||||||
| }; | }; | ||||||
| Devices.bySocket = function (store, socketId) { |  | ||||||
|   var dev; |  | ||||||
|   Object.keys(store._devices).some(function (k) { |  | ||||||
|     if (store._devices[k].socketId === socketId) { |  | ||||||
|       dev = store._devices[k]; |  | ||||||
|       return dev; |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   return dev; |  | ||||||
| }; |  | ||||||
| Devices.list = function (store, servername) { | Devices.list = function (store, servername) { | ||||||
|   console.log('[dontkeepme] servername', servername); |  | ||||||
|   // efficient lookup first
 |   // efficient lookup first
 | ||||||
|   if (store._domains[servername] && store._domains[servername].length) { |   if (store._domains[servername] && store._domains[servername].length) { | ||||||
|     // aliases have ._primary which is the name of the original
 |  | ||||||
|     return store._domains[servername]._primary && store._domains[store._domains[servername]._primary] || store._domains[servername]; |     return store._domains[servername]._primary && store._domains[store._domains[servername]._primary] || store._domains[servername]; | ||||||
|   } |   } | ||||||
|   // There wasn't an exact match so check any of the wildcard domains, sorted longest
 |   // There wasn't an exact match so check any of the wildcard domains, sorted longest
 | ||||||
|  | |||||||
| @ -136,22 +136,11 @@ module.exports.create = function (state) { | |||||||
|     console.log('[Admin] custom or null tlsOptions for SNICallback'); |     console.log('[Admin] custom or null tlsOptions for SNICallback'); | ||||||
|     tunnelAdminTlsOpts.SNICallback = tunnelAdminTlsOpts.SNICallback || noSniCallback('admin'); |     tunnelAdminTlsOpts.SNICallback = tunnelAdminTlsOpts.SNICallback || noSniCallback('admin'); | ||||||
|   } |   } | ||||||
|   var MPROXY = Buffer.from("MPROXY"); |  | ||||||
|   state.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) { |   state.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) { | ||||||
|     if (state.debug) { console.log('[Admin] new tls-terminated connection'); } |     if (state.debug) { console.log('[Admin] new tls-terminated connection'); } | ||||||
|     tlsSocket.once('readable', function () { |  | ||||||
|       var firstChunk = tlsSocket.read(); |  | ||||||
|       tlsSocket.unshift(firstChunk); |  | ||||||
| 
 |  | ||||||
|       if (0 === MPROXY.compare(firstChunk.slice(0, 4))) { |  | ||||||
|         tlsSocket.end("MPROXY isn't supported yet"); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|     // things get a little messed up here
 |     // things get a little messed up here
 | ||||||
|     (state.httpTunnelServer || state.httpServer).emit('connection', tlsSocket); |     (state.httpTunnelServer || state.httpServer).emit('connection', tlsSocket); | ||||||
|   }); |   }); | ||||||
|   }); |  | ||||||
|   state.tlsTunnelServer.on('tlsClientError', function () { |   state.tlsTunnelServer.on('tlsClientError', function () { | ||||||
|     console.error('tlsClientError TunnelServer client error'); |     console.error('tlsClientError TunnelServer client error'); | ||||||
|   }); |   }); | ||||||
|  | |||||||
							
								
								
									
										555
									
								
								lib/relay.js
									
									
									
									
									
								
							
							
						
						
									
										555
									
								
								lib/relay.js
									
									
									
									
									
								
							| @ -1,16 +1,555 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
|  | var url = require('url'); | ||||||
|  | var PromiseA = require('bluebird'); | ||||||
|  | var sni = require('sni'); | ||||||
| var Packer = require('proxy-packer'); | var Packer = require('proxy-packer'); | ||||||
| var Packer = require('proxy-packer'); | var PortServers = {}; | ||||||
| var PromiseA; | 
 | ||||||
| try { | function timeoutPromise(duration) { | ||||||
|   PromiseA = require('bluebird'); |   return new PromiseA(function (resolve) { | ||||||
| } catch(e) { |     setTimeout(resolve, duration); | ||||||
|   PromiseA = global.Promise; |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var Devices = require('./device-tracker'); | var Devices = require('./device-tracker'); | ||||||
| var Server = require('./server.js'); | var pipeWs = require('./pipe-ws.js'); | ||||||
|  | 
 | ||||||
|  | var Server = { | ||||||
|  |   _initCommandHandlers: function (state, srv) { | ||||||
|  |     var commandHandlers = { | ||||||
|  |       add_token: function addToken(newAuth) { | ||||||
|  |         return Server.addToken(state, srv, newAuth); | ||||||
|  |       } | ||||||
|  |     , delete_token: function (token) { | ||||||
|  |         return state.Promise.resolve(function () { | ||||||
|  |           var err; | ||||||
|  | 
 | ||||||
|  |           if (token !== '*') { | ||||||
|  |             err = Server.removeToken(state, srv, token); | ||||||
|  |             if (err) { return state.Promise.reject(err); } | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           Object.keys(srv.grants).some(function (jwtoken) { | ||||||
|  |             err = Server.removeToken(state, srv, jwtoken); | ||||||
|  |             return err; | ||||||
|  |           }); | ||||||
|  |           if (err) { return state.Promise.reject(err); } | ||||||
|  | 
 | ||||||
|  |           return null; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |     commandHandlers.auth = commandHandlers.add_token; | ||||||
|  |     commandHandlers.authn = commandHandlers.add_token; | ||||||
|  |     commandHandlers.authz = commandHandlers.add_token; | ||||||
|  |     srv._commandHandlers = commandHandlers; | ||||||
|  |   } | ||||||
|  | , _initPackerHandlers: function (state, srv) { | ||||||
|  |     var packerHandlers = { | ||||||
|  |       oncontrol: function (tun) { | ||||||
|  |         var cmd; | ||||||
|  |         try { | ||||||
|  |           cmd = JSON.parse(tun.data.toString()); | ||||||
|  |         } catch (e) {} | ||||||
|  |         if (!Array.isArray(cmd) || typeof cmd[0] !== 'number') { | ||||||
|  |           var msg = 'received bad command "' + tun.data.toString() + '"'; | ||||||
|  |           console.warn(msg, 'from websocket', srv.socketId); | ||||||
|  |           Server.sendTunnelMsg(srv, null, [0, {message: msg, code: 'E_BAD_COMMAND'}], 'control'); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (cmd[0] < 0) { | ||||||
|  |           // We only ever send one command and we send it once, so we just hard coded the ID as 1.
 | ||||||
|  |           if (cmd[0] === -1) { | ||||||
|  |             if (cmd[1]) { | ||||||
|  |               console.warn('received error response to hello from', srv.socketId, cmd[1]); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           else { | ||||||
|  |             console.warn('received response to unknown command', cmd, 'from', srv.socketId); | ||||||
|  |           } | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (cmd[0] === 0) { | ||||||
|  |           console.warn('received dis-associated error from', srv.socketId, cmd[1]); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         function onSuccess() { | ||||||
|  |           Server.sendTunnelMsg(srv, null, [-cmd[0], null], 'control'); | ||||||
|  |         } | ||||||
|  |         function onError(err) { | ||||||
|  |           Server.sendTunnelMsg(srv, null, [-cmd[0], err], 'control'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!srv._commandHandlers[cmd[1]]) { | ||||||
|  |           onError({ message: 'unknown command "'+cmd[1]+'"', code: 'E_UNKNOWN_COMMAND' }); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         console.log('command:', cmd[1], cmd.slice(2)); | ||||||
|  |         return srv._commandHandlers[cmd[1]].apply(null, cmd.slice(2)).then(onSuccess, onError); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     , onconnection: function (tun) { | ||||||
|  |         // I don't think this event can happen since this relay
 | ||||||
|  |         // is acting the part of the client, but just in case...
 | ||||||
|  |         // (in fact it should probably be explicitly disallowed)
 | ||||||
|  |         console.error("[SANITY FAIL] reverse connection start"); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     , onmessage: function (tun) { | ||||||
|  |         var cid = Packer.addrToId(tun); | ||||||
|  |         if (state.debug) { console.log("remote '" + Server.logName(state, srv) + "' has data for '" + cid + "'", tun.data.byteLength); } | ||||||
|  | 
 | ||||||
|  |         var browserConn = Server.getBrowserConn(state, srv, cid); | ||||||
|  |         if (!browserConn) { | ||||||
|  |           Server.sendTunnelMsg(srv, tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error'); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         browserConn.write(tun.data); | ||||||
|  |         // tunnelRead is how many bytes we've read from the tunnel, and written to the browser.
 | ||||||
|  |         browserConn.tunnelRead = (browserConn.tunnelRead || 0) + tun.data.byteLength; | ||||||
|  |         // If we have more than 1MB buffered data we need to tell the other side to slow down.
 | ||||||
|  |         // Once we've finished sending what we have we can tell the other side to keep going.
 | ||||||
|  |         // If we've already sent the 'pause' message though don't send it again, because we're
 | ||||||
|  |         // probably just dealing with data queued before our message got to them.
 | ||||||
|  |         if (!browserConn.remotePaused && browserConn.bufferSize > 1024*1024) { | ||||||
|  |           Server.sendTunnelMsg(srv, tun, browserConn.tunnelRead, 'pause'); | ||||||
|  |           browserConn.remotePaused = true; | ||||||
|  | 
 | ||||||
|  |           browserConn.once('drain', function () { | ||||||
|  |             Server.sendTunnelMsg(srv, tun, browserConn.tunnelRead, 'resume'); | ||||||
|  |             browserConn.remotePaused = false; | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     , onpause: function (tun) { | ||||||
|  |         var cid = Packer.addrToId(tun); | ||||||
|  |         console.log('[TunnelPause]', cid); | ||||||
|  |         var browserConn = Server.getBrowserConn(state, srv, cid); | ||||||
|  |         if (browserConn) { | ||||||
|  |           browserConn.manualPause = true; | ||||||
|  |           browserConn.pause(); | ||||||
|  |         } else { | ||||||
|  |           Server.sendTunnelMsg(srv, tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error'); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     , onresume: function (tun) { | ||||||
|  |         var cid = Packer.addrToId(tun); | ||||||
|  |         console.log('[TunnelResume]', cid); | ||||||
|  |         var browserConn = Server.getBrowserConn(state, srv, cid); | ||||||
|  |         if (browserConn) { | ||||||
|  |           browserConn.manualPause = false; | ||||||
|  |           browserConn.resume(); | ||||||
|  |         } else { | ||||||
|  |           Server.sendTunnelMsg(srv, tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error'); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     , onend: function (tun) { | ||||||
|  |         var cid = Packer.addrToId(tun); | ||||||
|  |         console.log('[TunnelEnd]', cid); | ||||||
|  |         Server.closeBrowserConn(state, srv, cid); | ||||||
|  |       } | ||||||
|  |     , onerror: function (tun) { | ||||||
|  |         var cid = Packer.addrToId(tun); | ||||||
|  |         console.warn('[TunnelError]', cid, tun.message); | ||||||
|  |         Server.closeBrowserConn(state, srv, cid); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |     srv._packerHandlers = packerHandlers; | ||||||
|  |     srv.unpacker = Packer.create(srv._packerHandlers); | ||||||
|  |   } | ||||||
|  | , _initSocketHandlers: function (state, srv) { | ||||||
|  |     function refreshTimeout() { | ||||||
|  |       srv.lastActivity = Date.now(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function checkTimeout() { | ||||||
|  |       // Determine how long the connection has been "silent", ie no activity.
 | ||||||
|  |       var silent = Date.now() - srv.lastActivity; | ||||||
|  | 
 | ||||||
|  |       // If we have had activity within the last activityTimeout then all we need to do is
 | ||||||
|  |       // call this function again at the soonest time when the connection could be timed out.
 | ||||||
|  |       if (silent < state.activityTimeout) { | ||||||
|  |         srv.timeoutId = setTimeout(checkTimeout, state.activityTimeout - silent); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Otherwise we check to see if the pong has also timed out, and if not we send a ping
 | ||||||
|  |       // and call this function again when the pong will have timed out.
 | ||||||
|  |       else if (silent < state.activityTimeout + state.pongTimeout) { | ||||||
|  |         if (state.debug) { console.log('pinging', Server.logName(state, srv)); } | ||||||
|  |         try { | ||||||
|  |           srv.ws.ping(); | ||||||
|  |         } catch (err) { | ||||||
|  |           console.warn('failed to ping home cloud', Server.logName(state, srv)); | ||||||
|  |         } | ||||||
|  |         srv.timeoutId = setTimeout(checkTimeout, state.pongTimeout); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Last case means the ping we sent before didn't get a response soon enough, so we
 | ||||||
|  |       // need to close the websocket connection.
 | ||||||
|  |       else { | ||||||
|  |         console.warn('home cloud', Server.logName(state, srv), 'connection timed out'); | ||||||
|  |         srv.ws.close(1013, 'connection timeout'); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function forwardMessage(chunk) { | ||||||
|  |       refreshTimeout(); | ||||||
|  |       if (state.debug) { console.log('[ws] device => client : demultiplexing message ', chunk.byteLength, 'bytes'); } | ||||||
|  |       //console.log(chunk.toString());
 | ||||||
|  |       srv.unpacker.fns.addChunk(chunk); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function hangup() { | ||||||
|  |       clearTimeout(srv.timeoutId); | ||||||
|  |       console.log('[ws] device hangup', Server.logName(state, srv), 'connection closing'); | ||||||
|  |       // remove the allowed domains from the list (but leave the socket)
 | ||||||
|  |       Object.keys(srv.grants).forEach(function (jwtoken) { | ||||||
|  |         Server.removeToken(state, srv, jwtoken); | ||||||
|  |       }); | ||||||
|  |       srv.ws.terminate(); | ||||||
|  |       // remove the socket from the list, period
 | ||||||
|  |       Devices.close(state.deviceLists, srv); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     srv.lastActivity = Date.now(); | ||||||
|  |     srv.timeoutId = null; | ||||||
|  |     srv.timeoutId = setTimeout(checkTimeout, state.activityTimeout); | ||||||
|  | 
 | ||||||
|  |     // Note that our websocket library automatically handles pong responses on ping requests
 | ||||||
|  |     // before it even emits the event.
 | ||||||
|  |     srv.ws.on('ping', refreshTimeout); | ||||||
|  |     srv.ws.on('pong', refreshTimeout); | ||||||
|  |     srv.ws.on('message', forwardMessage); | ||||||
|  |     srv.ws.on('close', hangup); | ||||||
|  |     srv.ws.on('error', hangup); | ||||||
|  |   } | ||||||
|  | , init: function init(state, srv) { | ||||||
|  |     Server._initCommandHandlers(state, srv); | ||||||
|  |     Server._initPackerHandlers(state, srv); | ||||||
|  |     Server._initSocketHandlers(state, srv); | ||||||
|  | 
 | ||||||
|  |     // Status Code '1' for Status 'hello'
 | ||||||
|  |     Server.sendTunnelMsg(srv, null, [1, 'hello', [srv.unpacker._version], Object.keys(srv._commandHandlers)], 'control'); | ||||||
|  |   } | ||||||
|  | , sendTunnelMsg: function sendTunnelMsg(srv, addr, data, service) { | ||||||
|  |     if (data && !Buffer.isBuffer()) { | ||||||
|  |       data = Buffer.from(JSON.stringify(data)); | ||||||
|  |     } | ||||||
|  |     srv.ws.send(Packer.packHeader(addr, data, service), {binary: true}); | ||||||
|  |     srv.ws.send(data, {binary: true}); | ||||||
|  |   } | ||||||
|  | , logName: function logName(state, srv) { | ||||||
|  |     var result = Object.keys(srv.grants).map(function (jwtoken) { | ||||||
|  |       return srv.grants[jwtoken].currentDesc; | ||||||
|  |     }).join(';'); | ||||||
|  | 
 | ||||||
|  |     return result || srv.socketId; | ||||||
|  |   } | ||||||
|  | , onAuth: function onAuth(state, srv, newAuth, grant) { | ||||||
|  |     console.log('\n[relay.js] onAuth'); | ||||||
|  |     console.log(newAuth); | ||||||
|  |     console.log(grant); | ||||||
|  |     //var stringauth;
 | ||||||
|  |     var err; | ||||||
|  |     if (!grant || 'object' !== typeof grant) { | ||||||
|  |       console.log('[relay.js] invalid token', grant); | ||||||
|  |       err = new Error("invalid access token"); | ||||||
|  |       err.code = "E_INVALID_TOKEN"; | ||||||
|  |       return state.Promise.reject(err); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ('string' !== typeof newAuth) { | ||||||
|  |       newAuth = JSON.stringify(newAuth); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     console.log('check for upgrade token'); | ||||||
|  |     if (grant.jwt && newAuth !== grant.jwt) { | ||||||
|  |       console.log('new token to send back'); | ||||||
|  |       // Access Token
 | ||||||
|  |       Server.sendTunnelMsg( | ||||||
|  |         srv | ||||||
|  |       , null | ||||||
|  |       , [ 3 | ||||||
|  |         , 'access_token' | ||||||
|  |         , { jwt: grant.jwt } | ||||||
|  |         ] | ||||||
|  |       , 'control' | ||||||
|  |       ); | ||||||
|  |       // these aren't needed internally once they're sent
 | ||||||
|  |       grant.jwt = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |     if (!Array.isArray(grant.domains) || !grant.domains.length) { | ||||||
|  |       err = new Error("invalid domains array"); | ||||||
|  |       err.code = "E_INVALID_NAME"; | ||||||
|  |       return state.Promise.reject(err); | ||||||
|  |     } | ||||||
|  |     */ | ||||||
|  |     if (grant.domains.some(function (name) { return typeof name !== 'string'; })) { | ||||||
|  |       console.log('bad domain names'); | ||||||
|  |       err = new Error("invalid domain name(s)"); | ||||||
|  |       err.code = "E_INVALID_NAME"; | ||||||
|  |       return state.Promise.reject(err); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     console.log('strolling through pleasantries'); | ||||||
|  |     // Add the custom properties we need to manage this remote, then add it to all the relevant
 | ||||||
|  |     // domains and the list of all this websocket's grants.
 | ||||||
|  |     grant.domains.forEach(function (domainname) { | ||||||
|  |       console.log('add', domainname, 'to device lists'); | ||||||
|  |       srv.domainsMap[domainname] = true; | ||||||
|  |       Devices.add(state.deviceLists, domainname, srv); | ||||||
|  |     }); | ||||||
|  |     srv.domains = Object.keys(srv.domainsMap); | ||||||
|  |     srv.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || srv.domains.join(','); | ||||||
|  |     grant.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || grant.domains.join(','); | ||||||
|  |     grant.srv = srv; | ||||||
|  |     //grant.ws = srv.ws;
 | ||||||
|  |     //grant.upgradeReq = srv.upgradeReq;
 | ||||||
|  |     grant.clients = {}; | ||||||
|  | 
 | ||||||
|  |     if (!grant.ports) { grant.ports = []; } | ||||||
|  | 
 | ||||||
|  |     function openPort(serviceport) { | ||||||
|  |       function tcpListener(conn) { | ||||||
|  |         Server.onDynTcpConn(state, srv, srv.portsMap[serviceport], conn); | ||||||
|  |       } | ||||||
|  |       serviceport = parseInt(serviceport, 10) || 0; | ||||||
|  |       if (!serviceport) { | ||||||
|  |         // TODO error message about bad port
 | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       if (PortServers[serviceport]) { | ||||||
|  |         console.log('reuse', serviceport, 'for this connection'); | ||||||
|  |         //grant.ports = [];
 | ||||||
|  |         srv.portsMap[serviceport] = PortServers[serviceport]; | ||||||
|  |         srv.portsMap[serviceport].on('connection', tcpListener); | ||||||
|  |         srv.portsMap[serviceport].tcpListener = tcpListener; | ||||||
|  |         Devices.add(state.deviceLists, serviceport, srv); | ||||||
|  |       } else { | ||||||
|  |         try { | ||||||
|  |           console.log('use new', serviceport, 'for this connection'); | ||||||
|  |           srv.portsMap[serviceport] = PortServers[serviceport] = require('net').createServer(tcpListener); | ||||||
|  |           srv.portsMap[serviceport].tcpListener = tcpListener; | ||||||
|  |           srv.portsMap[serviceport].listen(serviceport, function () { | ||||||
|  |             console.info('[DynTcpConn] Port', serviceport, 'now open for', grant.currentDesc); | ||||||
|  |             Devices.add(state.deviceLists, serviceport, srv); | ||||||
|  |           }); | ||||||
|  |           srv.portsMap[serviceport].on('error', function (e) { | ||||||
|  |             // TODO try again with random port
 | ||||||
|  |             console.error("Server Error assigning a dynamic port to a new connection:", e); | ||||||
|  |           }); | ||||||
|  |         } catch(e) { | ||||||
|  |           // what a wonderful problem it will be the day that this bug needs to be fixed
 | ||||||
|  |           // (i.e. there are enough users to run out of ports)
 | ||||||
|  |           console.error("Error assigning a dynamic port to a new connection:", e); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     grant.ports.forEach(openPort); | ||||||
|  | 
 | ||||||
|  |     srv.grants[newAuth] = grant; | ||||||
|  |     console.info("[ws] authorized", srv.socketId, "for", grant.currentDesc); | ||||||
|  | 
 | ||||||
|  |     console.log('notify of grants', grant.domains, grant.ports); | ||||||
|  |     Server.sendTunnelMsg( | ||||||
|  |       srv | ||||||
|  |     , null | ||||||
|  |     , [ 2 | ||||||
|  |       , 'grant' | ||||||
|  |       , [ ['ssh+https', grant.domains[0], 443 ] | ||||||
|  |         , ['ssh', 'ssh.' + state.config.sharedDomain, grant.ports ] | ||||||
|  |         , ['tcp', 'tcp.' + state.config.sharedDomain, grant.ports ] | ||||||
|  |         , ['https', grant.domains[0] ] | ||||||
|  |         ] | ||||||
|  |       ] | ||||||
|  |     , 'control' | ||||||
|  |     ); | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | , onDynTcpConn: function onDynTcpConn(state, srv, server, conn) { | ||||||
|  |     var serviceport = server.address().port; | ||||||
|  |     console.log('[DynTcpConn] new connection on', serviceport); | ||||||
|  |     var nextDevice = Devices.next(state.deviceLists, serviceport); | ||||||
|  | 
 | ||||||
|  |     if (!nextDevice) { | ||||||
|  |       conn.write("[Sanity Error] I've got a blank space baby, but nowhere to write your name."); | ||||||
|  |       conn.end(); | ||||||
|  |       try { | ||||||
|  |         server.close(); | ||||||
|  |       } catch(e) { | ||||||
|  |         console.error("[DynTcpConn] failed to close server:", e); | ||||||
|  |       } | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // When using raw TCP we're already paired to the client by port
 | ||||||
|  |     // and we can begin connecting right away, but we'll wait just a sec
 | ||||||
|  |     // to reject known bad connections
 | ||||||
|  |     var sendConnection = setTimeout(function () { | ||||||
|  |       conn.removeListener('data', peekFirstPacket) | ||||||
|  |       console.log("[debug tcp conn] Connecting possible telnet client to device..."); | ||||||
|  |       pipeWs(null, 'tcp', nextDevice, conn, serviceport); | ||||||
|  |     }, 350); | ||||||
|  |     function peekFirstPacket(firstChunk) { | ||||||
|  |       clearTimeout(sendConnection); | ||||||
|  |       if (state.debug) { console.log("[DynTcp]", serviceport, "examining firstChunk from", Packer.socketToId(conn)); } | ||||||
|  |       conn.pause(); | ||||||
|  |       //conn.unshift(firstChunk);
 | ||||||
|  |       conn._handle.onread(firstChunk.length, firstChunk); | ||||||
|  | 
 | ||||||
|  |       var servername; | ||||||
|  |       var hostname; | ||||||
|  |       var str; | ||||||
|  |       var m; | ||||||
|  | 
 | ||||||
|  |       if (22 === firstChunk[0]) { | ||||||
|  |         servername = (sni(firstChunk)||'').toLowerCase(); | ||||||
|  |       } else if (firstChunk[0] > 32 && firstChunk[0] < 127) { | ||||||
|  |         str = firstChunk.toString(); | ||||||
|  |         m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); | ||||||
|  |         hostname = (m && m[1].toLowerCase() || '').split(':')[0]; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (servername || hostname) { | ||||||
|  |         if (servername) { | ||||||
|  |           conn.write("TLS with sni is allowed only on standard ports. If you've registered '" + servername + "' use port 443."); | ||||||
|  |         } else { | ||||||
|  |           conn.write("HTTP with Host headers is not allowed on dynamic ports. If you've registered '" + hostname + "' use port 80."); | ||||||
|  |         } | ||||||
|  |         conn.end(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // pipeWs(servername, servicename, srv, client, serviceport)
 | ||||||
|  |       // remote.clients is managed as part of the piping process
 | ||||||
|  |       if (state.debug) { console.log("[DynTcp]", serviceport, "piping to srv (via loadbal)"); } | ||||||
|  |       pipeWs(null, 'tcp', nextDevice, conn, serviceport); | ||||||
|  | 
 | ||||||
|  |       process.nextTick(function () { conn.resume(); }); | ||||||
|  |     } | ||||||
|  |     conn.once('data', peekFirstPacket); | ||||||
|  |   } | ||||||
|  | , addToken: function addToken(state, srv, newAuth) { | ||||||
|  |     console.log("addToken", newAuth); | ||||||
|  |     if (srv.grants[newAuth]) { | ||||||
|  |       console.log("addToken - duplicate"); | ||||||
|  |       // return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
 | ||||||
|  |       return state.Promise.resolve(null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return state.authenticate({ auth: newAuth }).then(function (authnToken) { | ||||||
|  | 
 | ||||||
|  |       console.log('\n[relay.js] newAuth'); | ||||||
|  |       console.log(newAuth); | ||||||
|  | 
 | ||||||
|  |       console.log('\n[relay.js] authnToken'); | ||||||
|  |       console.log(authnToken); | ||||||
|  | 
 | ||||||
|  |       if (authnToken.id) { | ||||||
|  |         state.srvs[authnToken.id] = state.srvs[authnToken.id] || {}; | ||||||
|  |         state.srvs[authnToken.id].updateAuth = function (validToken) { | ||||||
|  |           return Server.onAuth(state, srv, newAuth, validToken); | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // will return rejection if necessary
 | ||||||
|  |       return state.srvs[authnToken.id].updateAuth(authnToken); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | , removeToken: function removeToken(state, srv, jwtoken) { | ||||||
|  |     var grant = srv.grants[jwtoken]; | ||||||
|  |     if (!grant) { | ||||||
|  |       return { message: 'specified token not present', code: 'E_INVALID_TOKEN'}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Prevent any more browser connections for this grant being sent to this srv,
 | ||||||
|  |     // and any existing connections from trying to send more data across the connection.
 | ||||||
|  |     grant.domains.forEach(function (domainname) { | ||||||
|  |       Devices.remove(state.deviceLists, domainname, srv); | ||||||
|  |     }); | ||||||
|  |     grant.ports.forEach(function (portnumber) { | ||||||
|  |       Devices.remove(state.deviceLists, portnumber, srv); | ||||||
|  |       if (!srv.portsMap[portnumber]) { return; } | ||||||
|  |       try { | ||||||
|  |         srv.portsMap[portnumber].close(function () { | ||||||
|  |           console.log("[DynTcpConn] closing server for ", portnumber); | ||||||
|  |           delete srv.portsMap[portnumber]; | ||||||
|  |           delete PortServers[portnumber]; | ||||||
|  |         }); | ||||||
|  |       } catch(e) { /*ignore*/ } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Close all of the existing browser connections associated with this websocket connection.
 | ||||||
|  |     Object.keys(grant.clients).forEach(function (cid) { | ||||||
|  |       Server.closeBrowserConn(state, srv, cid); | ||||||
|  |     }); | ||||||
|  |     delete srv.grants[jwtoken]; | ||||||
|  |     console.log("[ws] removed token '" + grant.currentDesc + "' from", srv.socketId); | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | , getBrowserConn: function getBrowserConn(state, srv, cid) { | ||||||
|  |     return srv.clients[cid]; | ||||||
|  |   } | ||||||
|  | , closeBrowserConn: function closeBrowserConn(state, srv, cid) { | ||||||
|  |     if (!srv.clients[cid]) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     PromiseA.resolve().then(function () { | ||||||
|  |       var conn = srv.clients[cid]; | ||||||
|  |       conn.tunnelClosing = true; | ||||||
|  |       conn.end(); | ||||||
|  | 
 | ||||||
|  |       // If no data is buffered for writing then we don't need to wait for it to drain.
 | ||||||
|  |       if (!conn.bufferSize) { | ||||||
|  |         return timeoutPromise(500); | ||||||
|  |       } | ||||||
|  |       // Otherwise we want the connection to be able to finish, but we also want to impose
 | ||||||
|  |       // a time limit for it to drain, since it shouldn't have more than 1MB buffered.
 | ||||||
|  |       return new PromiseA(function (resolve) { | ||||||
|  |         var timeoutId = setTimeout(resolve, 60*1000); | ||||||
|  |         conn.once('drain', function () { | ||||||
|  |           clearTimeout(timeoutId); | ||||||
|  |           setTimeout(resolve, 500); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }).then(function () { | ||||||
|  |       if (srv.clients[cid]) { | ||||||
|  |         console.warn(cid, 'browser connection still present after calling `end`'); | ||||||
|  |         srv.clients[cid].destroy(); | ||||||
|  |         return timeoutPromise(500); | ||||||
|  |       } | ||||||
|  |     }).then(function () { | ||||||
|  |       if (srv.clients[cid]) { | ||||||
|  |         console.error(cid, 'browser connection still present after calling `destroy`'); | ||||||
|  |         delete srv.clients[cid]; | ||||||
|  |       } | ||||||
|  |     }).catch(function (err) { | ||||||
|  |       console.warn('failed to close browser connection', cid, err); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | , parseAuth: function parseAuth(state, srv) { | ||||||
|  |     var authn = (srv.upgradeReq.headers.authorization||'').split(/\s+/); | ||||||
|  |     if (authn[0] && 'basic' === authn[0].toLowerCase()) { | ||||||
|  |       try { | ||||||
|  |         authn = new Buffer(authn[1], 'base64').toString('ascii').split(':'); | ||||||
|  |         return authn[1]; | ||||||
|  |       } catch (err) { } | ||||||
|  |     } | ||||||
|  |     return url.parse(srv.upgradeReq.url, true).query.access_token; | ||||||
|  |   } | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| module.exports.store = { Devices: Devices }; | module.exports.store = { Devices: Devices }; | ||||||
| module.exports.create = function (state) { | module.exports.create = function (state) { | ||||||
| @ -49,8 +588,6 @@ module.exports.create = function (state) { | |||||||
|     srv.domainsMap = {}; |     srv.domainsMap = {}; | ||||||
|     srv.portsMap = {}; |     srv.portsMap = {}; | ||||||
|     srv.pausedConns = []; |     srv.pausedConns = []; | ||||||
|     srv.domains = []; |  | ||||||
|     srv.ports = []; |  | ||||||
| 
 | 
 | ||||||
|     if (state.debug) { console.log('[ws] connection', srv.socketId); } |     if (state.debug) { console.log('[ws] connection', srv.socketId); } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										579
									
								
								lib/server.js
									
									
									
									
									
								
							
							
						
						
									
										579
									
								
								lib/server.js
									
									
									
									
									
								
							| @ -1,579 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var url = require('url'); |  | ||||||
| var sni = require('sni'); |  | ||||||
| var Packer = require('proxy-packer'); |  | ||||||
| var PromiseA; |  | ||||||
| try { |  | ||||||
|   PromiseA = require('bluebird'); |  | ||||||
| } catch(e) { |  | ||||||
|   PromiseA = global.Promise; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function timeoutPromise(duration) { |  | ||||||
|   return new PromiseA(function (resolve) { |  | ||||||
|     setTimeout(resolve, duration); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| var Devices = require('./device-tracker'); |  | ||||||
| var pipeWs = require('./pipe-ws.js'); |  | ||||||
| var PortServers = {}; |  | ||||||
| var Server = { |  | ||||||
|   _initCommandHandlers: function (state, srv) { |  | ||||||
|     var commandHandlers = { |  | ||||||
|       add_token: function addToken(newAuth) { |  | ||||||
|         return Server.addToken(state, srv, newAuth); |  | ||||||
|       } |  | ||||||
|     , delete_token: function (token) { |  | ||||||
|         return state.Promise.resolve(function () { |  | ||||||
|           var err; |  | ||||||
| 
 |  | ||||||
|           if (token !== '*') { |  | ||||||
|             err = Server.removeToken(state, srv, token); |  | ||||||
|             if (err) { return state.Promise.reject(err); } |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           Object.keys(srv.grants).some(function (jwtoken) { |  | ||||||
|             err = Server.removeToken(state, srv, jwtoken); |  | ||||||
|             return err; |  | ||||||
|           }); |  | ||||||
|           if (err) { return state.Promise.reject(err); } |  | ||||||
| 
 |  | ||||||
|           return null; |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|     commandHandlers.auth = commandHandlers.add_token; |  | ||||||
|     commandHandlers.authn = commandHandlers.add_token; |  | ||||||
|     commandHandlers.authz = commandHandlers.add_token; |  | ||||||
|     srv._commandHandlers = commandHandlers; |  | ||||||
|   } |  | ||||||
| , _initPackerHandlers: function (state, srv) { |  | ||||||
|     var packerHandlers = { |  | ||||||
|       oncontrol: function (tun) { |  | ||||||
|         var cmd; |  | ||||||
|         try { |  | ||||||
|           cmd = JSON.parse(tun.data.toString()); |  | ||||||
|         } catch (e) {} |  | ||||||
|         if (!Array.isArray(cmd) || typeof cmd[0] !== 'number') { |  | ||||||
|           var msg = 'received bad command "' + tun.data.toString() + '"'; |  | ||||||
|           console.warn(msg, 'from websocket', srv.socketId); |  | ||||||
|           Server.sendTunnelMsg(srv, null, [0, {message: msg, code: 'E_BAD_COMMAND'}], 'control'); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (cmd[0] < 0) { |  | ||||||
|           // We only ever send one command and we send it once, so we just hard coded the ID as 1.
 |  | ||||||
|           if (cmd[0] === -1) { |  | ||||||
|             if (cmd[1]) { |  | ||||||
|               console.warn('received error response to hello from', srv.socketId, cmd[1]); |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|           else { |  | ||||||
|             console.warn('received response to unknown command', cmd, 'from', srv.socketId); |  | ||||||
|           } |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (cmd[0] === 0) { |  | ||||||
|           console.warn('received dis-associated error from', srv.socketId, cmd[1]); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function onSuccess() { |  | ||||||
|           Server.sendTunnelMsg(srv, null, [-cmd[0], null], 'control'); |  | ||||||
|         } |  | ||||||
|         function onError(err) { |  | ||||||
|           Server.sendTunnelMsg(srv, null, [-cmd[0], err], 'control'); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (!srv._commandHandlers[cmd[1]]) { |  | ||||||
|           onError({ message: 'unknown command "'+cmd[1]+'"', code: 'E_UNKNOWN_COMMAND' }); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         console.log('command:', cmd[1], cmd.slice(2)); |  | ||||||
|         return srv._commandHandlers[cmd[1]].apply(null, cmd.slice(2)).then(onSuccess, onError); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|     , onconnection: function (/*tun*/) { |  | ||||||
|         // I don't think this event can happen since this relay
 |  | ||||||
|         // is acting the part of the client, but just in case...
 |  | ||||||
|         // (in fact it should probably be explicitly disallowed)
 |  | ||||||
|         console.error("[SANITY FAIL] reverse connection start"); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|     , onmessage: function (tun) { |  | ||||||
|         var cid = Packer.addrToId(tun); |  | ||||||
|         if (state.debug) { console.log("remote '" + Server.logName(state, srv) + "' has data for '" + cid + "'", tun.data.byteLength); } |  | ||||||
| 
 |  | ||||||
|         var browserConn = Server.getBrowserConn(state, srv, cid); |  | ||||||
|         if (!browserConn) { |  | ||||||
|           Server.sendTunnelMsg(srv, tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error'); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         browserConn.write(tun.data); |  | ||||||
|         // tunnelRead is how many bytes we've read from the tunnel, and written to the browser.
 |  | ||||||
|         browserConn.tunnelRead = (browserConn.tunnelRead || 0) + tun.data.byteLength; |  | ||||||
|         // If we have more than 1MB buffered data we need to tell the other side to slow down.
 |  | ||||||
|         // Once we've finished sending what we have we can tell the other side to keep going.
 |  | ||||||
|         // If we've already sent the 'pause' message though don't send it again, because we're
 |  | ||||||
|         // probably just dealing with data queued before our message got to them.
 |  | ||||||
|         if (!browserConn.remotePaused && browserConn.bufferSize > 1024*1024) { |  | ||||||
|           Server.sendTunnelMsg(srv, tun, browserConn.tunnelRead, 'pause'); |  | ||||||
|           browserConn.remotePaused = true; |  | ||||||
| 
 |  | ||||||
|           browserConn.once('drain', function () { |  | ||||||
|             Server.sendTunnelMsg(srv, tun, browserConn.tunnelRead, 'resume'); |  | ||||||
|             browserConn.remotePaused = false; |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|     , onpause: function (tun) { |  | ||||||
|         var cid = Packer.addrToId(tun); |  | ||||||
|         console.log('[TunnelPause]', cid); |  | ||||||
|         var browserConn = Server.getBrowserConn(state, srv, cid); |  | ||||||
|         if (browserConn) { |  | ||||||
|           browserConn.manualPause = true; |  | ||||||
|           browserConn.pause(); |  | ||||||
|         } else { |  | ||||||
|           Server.sendTunnelMsg(srv, tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error'); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|     , onresume: function (tun) { |  | ||||||
|         var cid = Packer.addrToId(tun); |  | ||||||
|         console.log('[TunnelResume]', cid); |  | ||||||
|         var browserConn = Server.getBrowserConn(state, srv, cid); |  | ||||||
|         if (browserConn) { |  | ||||||
|           browserConn.manualPause = false; |  | ||||||
|           browserConn.resume(); |  | ||||||
|         } else { |  | ||||||
|           Server.sendTunnelMsg(srv, tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error'); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|     , onend: function (tun) { |  | ||||||
|         var cid = Packer.addrToId(tun); |  | ||||||
|         console.log('[TunnelEnd]', cid); |  | ||||||
|         Server.closeBrowserConn(state, srv, cid); |  | ||||||
|       } |  | ||||||
|     , onerror: function (tun) { |  | ||||||
|         var cid = Packer.addrToId(tun); |  | ||||||
|         console.warn('[TunnelError]', cid, tun.message); |  | ||||||
|         Server.closeBrowserConn(state, srv, cid); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|     srv._packerHandlers = packerHandlers; |  | ||||||
|     srv.unpacker = Packer.create(srv._packerHandlers); |  | ||||||
|   } |  | ||||||
| , _initSocketHandlers: function (state, srv) { |  | ||||||
|     function refreshTimeout() { |  | ||||||
|       srv.lastActivity = Date.now(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function checkTimeout() { |  | ||||||
|       // Determine how long the connection has been "silent", ie no activity.
 |  | ||||||
|       var silent = Date.now() - srv.lastActivity; |  | ||||||
| 
 |  | ||||||
|       // If we have had activity within the last activityTimeout then all we need to do is
 |  | ||||||
|       // call this function again at the soonest time when the connection could be timed out.
 |  | ||||||
|       if (silent < state.activityTimeout) { |  | ||||||
|         srv.timeoutId = setTimeout(checkTimeout, state.activityTimeout - silent); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // Otherwise we check to see if the pong has also timed out, and if not we send a ping
 |  | ||||||
|       // and call this function again when the pong will have timed out.
 |  | ||||||
|       else if (silent < state.activityTimeout + state.pongTimeout) { |  | ||||||
|         if (state.debug) { console.log('pinging', Server.logName(state, srv)); } |  | ||||||
|         try { |  | ||||||
|           srv.ws.ping(); |  | ||||||
|         } catch (err) { |  | ||||||
|           console.warn('failed to ping home cloud', Server.logName(state, srv)); |  | ||||||
|         } |  | ||||||
|         srv.timeoutId = setTimeout(checkTimeout, state.pongTimeout); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // Last case means the ping we sent before didn't get a response soon enough, so we
 |  | ||||||
|       // need to close the websocket connection.
 |  | ||||||
|       else { |  | ||||||
|         console.warn('home cloud', Server.logName(state, srv), 'connection timed out'); |  | ||||||
|         srv.ws.close(1013, 'connection timeout'); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function forwardMessage(chunk) { |  | ||||||
|       refreshTimeout(); |  | ||||||
|       if (state.debug) { console.log('[ws] device => client : demultiplexing message ', chunk.byteLength, 'bytes'); } |  | ||||||
|       //console.log(chunk.toString());
 |  | ||||||
|       srv.unpacker.fns.addChunk(chunk); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function hangup() { |  | ||||||
|       clearTimeout(srv.timeoutId); |  | ||||||
|       console.log('[ws] device hangup', Server.logName(state, srv), 'connection closing'); |  | ||||||
|       // remove the allowed domains from the list (but leave the socket)
 |  | ||||||
|       Object.keys(srv.grants).forEach(function (jwtoken) { |  | ||||||
|         Server.removeToken(state, srv, jwtoken); |  | ||||||
|       }); |  | ||||||
|       srv.ws.terminate(); |  | ||||||
|       // remove the socket from the list, period
 |  | ||||||
|       Devices.close(state.deviceLists, srv); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     srv.lastActivity = Date.now(); |  | ||||||
|     srv.timeoutId = null; |  | ||||||
|     srv.timeoutId = setTimeout(checkTimeout, state.activityTimeout); |  | ||||||
| 
 |  | ||||||
|     // Note that our websocket library automatically handles pong responses on ping requests
 |  | ||||||
|     // before it even emits the event.
 |  | ||||||
|     srv.ws.on('ping', refreshTimeout); |  | ||||||
|     srv.ws.on('pong', refreshTimeout); |  | ||||||
|     srv.ws.on('message', forwardMessage); |  | ||||||
|     srv.ws.on('close', hangup); |  | ||||||
|     srv.ws.on('error', hangup); |  | ||||||
|   } |  | ||||||
| , init: function init(state, srv) { |  | ||||||
|     Server._initCommandHandlers(state, srv); |  | ||||||
|     Server._initPackerHandlers(state, srv); |  | ||||||
|     Server._initSocketHandlers(state, srv); |  | ||||||
| 
 |  | ||||||
|     // Status Code '1' for Status 'hello'
 |  | ||||||
|     Server.sendTunnelMsg(srv, null, [1, 'hello', [srv.unpacker._version], Object.keys(srv._commandHandlers)], 'control'); |  | ||||||
|   } |  | ||||||
| , sendTunnelMsg: function sendTunnelMsg(srv, addr, data, service) { |  | ||||||
|     if (data && !Buffer.isBuffer()) { |  | ||||||
|       data = Buffer.from(JSON.stringify(data)); |  | ||||||
|     } |  | ||||||
|     srv.ws.send(Packer.packHeader(addr, data, service), {binary: true}); |  | ||||||
|     srv.ws.send(data, {binary: true}); |  | ||||||
|   } |  | ||||||
| , logName: function logName(state, srv) { |  | ||||||
|     var result = Object.keys(srv.grants).map(function (jwtoken) { |  | ||||||
|       return srv.grants[jwtoken].currentDesc; |  | ||||||
|     }).join(';'); |  | ||||||
| 
 |  | ||||||
|     return result || srv.socketId; |  | ||||||
|   } |  | ||||||
| , onAuth: function onAuth(state, srv, rawAuth, grant) { |  | ||||||
|     console.log('\n[relay.js] onAuth'); |  | ||||||
|     console.log(rawAuth); |  | ||||||
|     //console.log(grant);
 |  | ||||||
|     //var stringauth;
 |  | ||||||
|     var err; |  | ||||||
|     if (!grant || 'object' !== typeof grant) { |  | ||||||
|       console.log('[relay.js] invalid token', grant); |  | ||||||
|       err = new Error("invalid access token"); |  | ||||||
|       err.code = "E_INVALID_TOKEN"; |  | ||||||
|       return state.Promise.reject(err); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // deprecated (for json object on connect)
 |  | ||||||
|     if ('string' !== typeof rawAuth) { |  | ||||||
|       rawAuth = JSON.stringify(rawAuth); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // TODO don't fire the onAuth event on non-authz updates
 |  | ||||||
|     if (!grant.jwt && !(grant.domains||[]).length && !(grant.ports||[]).length) { |  | ||||||
|       console.log("[onAuth] nothing to offer at all"); |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     console.log('[onAuth] check for upgrade token'); |  | ||||||
|     //console.log(grant);
 |  | ||||||
|     if (grant.jwt) { |  | ||||||
|       if (rawAuth !== grant.jwt) { |  | ||||||
|         console.log('[onAuth] token is new'); |  | ||||||
|       } |  | ||||||
|       // TODO only send token when new
 |  | ||||||
|       if (true) { |  | ||||||
|         // Access Token
 |  | ||||||
|         console.log('[onAuth] sending back token'); |  | ||||||
|         Server.sendTunnelMsg( |  | ||||||
|           srv |  | ||||||
|         , null |  | ||||||
|         , [ 3 |  | ||||||
|           , 'access_token' |  | ||||||
|           , { jwt: grant.jwt } |  | ||||||
|           ] |  | ||||||
|         , 'control' |  | ||||||
|         ); |  | ||||||
|         // these aren't needed internally once they're sent
 |  | ||||||
|         grant.jwt = null; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /* |  | ||||||
|     if (!Array.isArray(grant.domains) || !grant.domains.length) { |  | ||||||
|       err = new Error("invalid domains array"); |  | ||||||
|       err.code = "E_INVALID_NAME"; |  | ||||||
|       return state.Promise.reject(err); |  | ||||||
|     } |  | ||||||
|     */ |  | ||||||
|     if (grant.domains.some(function (name) { return typeof name !== 'string'; })) { |  | ||||||
|       console.log('bad domain names'); |  | ||||||
|       err = new Error("invalid domain name(s)"); |  | ||||||
|       err.code = "E_INVALID_NAME"; |  | ||||||
|       return state.Promise.reject(err); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     console.log('[onAuth] strolling through pleasantries'); |  | ||||||
|     // Add the custom properties we need to manage this remote, then add it to all the relevant
 |  | ||||||
|     // domains and the list of all this websocket's grants.
 |  | ||||||
|     grant.domains.forEach(function (domainname) { |  | ||||||
|       console.log('add', domainname, 'to device lists'); |  | ||||||
|       srv.domainsMap[domainname] = true; |  | ||||||
|       Devices.add(state.deviceLists, domainname, srv); |  | ||||||
|       // TODO allow subs to go to individual devices
 |  | ||||||
|       Devices.alias(state.deviceLists, domainname, '*.' + domainname); |  | ||||||
|     }); |  | ||||||
|     srv.domains = Object.keys(srv.domainsMap); |  | ||||||
|     srv.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || srv.domains.join(','); |  | ||||||
|     grant.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || grant.domains.join(','); |  | ||||||
|     //grant.srv = srv;
 |  | ||||||
|     //grant.ws = srv.ws;
 |  | ||||||
|     //grant.upgradeReq = srv.upgradeReq;
 |  | ||||||
|     grant.clients = {}; |  | ||||||
| 
 |  | ||||||
|     if (!grant.ports) { grant.ports = []; } |  | ||||||
| 
 |  | ||||||
|     function openPort(serviceport) { |  | ||||||
|       function tcpListener(conn) { |  | ||||||
|         Server.onDynTcpConn(state, srv, srv.portsMap[serviceport], conn); |  | ||||||
|       } |  | ||||||
|       serviceport = parseInt(serviceport, 10) || 0; |  | ||||||
|       if (!serviceport) { |  | ||||||
|         // TODO error message about bad port
 |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       if (PortServers[serviceport]) { |  | ||||||
|         console.log('reuse', serviceport, 'for this connection'); |  | ||||||
|         //grant.ports = [];
 |  | ||||||
|         srv.portsMap[serviceport] = PortServers[serviceport]; |  | ||||||
|         srv.portsMap[serviceport].on('connection', tcpListener); |  | ||||||
|         srv.portsMap[serviceport].tcpListener = tcpListener; |  | ||||||
|         Devices.addPort(state.deviceLists, serviceport, srv); |  | ||||||
|       } else { |  | ||||||
|         try { |  | ||||||
|           console.log('use new', serviceport, 'for this connection'); |  | ||||||
|           srv.portsMap[serviceport] = PortServers[serviceport] = require('net').createServer(tcpListener); |  | ||||||
|           srv.portsMap[serviceport].tcpListener = tcpListener; |  | ||||||
|           srv.portsMap[serviceport].listen(serviceport, function () { |  | ||||||
|             console.info('[DynTcpConn] Port', serviceport, 'now open for', grant.currentDesc); |  | ||||||
|             Devices.addPort(state.deviceLists, serviceport, srv); |  | ||||||
|           }); |  | ||||||
|           srv.portsMap[serviceport].on('error', function (e) { |  | ||||||
|             // TODO try again with random port
 |  | ||||||
|             console.error("Server Error assigning a dynamic port to a new connection:", e); |  | ||||||
|           }); |  | ||||||
|         } catch(e) { |  | ||||||
|           // what a wonderful problem it will be the day that this bug needs to be fixed
 |  | ||||||
|           // (i.e. there are enough users to run out of ports)
 |  | ||||||
|           console.error("Error assigning a dynamic port to a new connection:", e); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     grant.ports.forEach(openPort); |  | ||||||
| 
 |  | ||||||
|     console.info("[ws] authorized", srv.socketId, "for", grant.currentDesc); |  | ||||||
|     console.log('notify of grants', grant.domains, grant.ports); |  | ||||||
|     srv.grants[rawAuth] = grant; |  | ||||||
|     Server.sendTunnelMsg( |  | ||||||
|       srv |  | ||||||
|     , null |  | ||||||
|     , [ 2 |  | ||||||
|       , 'grant' |  | ||||||
|       , [ ['ssh+https', grant.domains[0], 443 ] |  | ||||||
|           // TODO the shared domain should be token specific
 |  | ||||||
|         , ['ssh', 'ssh.' + state.config.sharedDomain, [grant.ports[0]] ] |  | ||||||
|         , ['tcp', 'tcp.' + state.config.sharedDomain, [grant.ports[0]] ] |  | ||||||
|         , ['https', grant.domains[0] ] |  | ||||||
|         ] |  | ||||||
|       ] |  | ||||||
|     , 'control' |  | ||||||
|     ); |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
| , onDynTcpConn: function onDynTcpConn(state, srv, server, conn) { |  | ||||||
|     var serviceport = server.address().port; |  | ||||||
|     console.log('[DynTcpConn] new connection on', serviceport); |  | ||||||
|     var nextDevice = Devices.next(state.deviceLists, serviceport); |  | ||||||
| 
 |  | ||||||
|     if (!nextDevice) { |  | ||||||
|       conn.write("[Sanity Error] I've got a blank space baby, but nowhere to write your name."); |  | ||||||
|       conn.end(); |  | ||||||
|       try { |  | ||||||
|         server.close(); |  | ||||||
|       } catch(e) { |  | ||||||
|         console.error("[DynTcpConn] failed to close server:", e); |  | ||||||
|       } |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // When using raw TCP we're already paired to the client by port
 |  | ||||||
|     // and we can begin connecting right away, but we'll wait just a sec
 |  | ||||||
|     // to reject known bad connections
 |  | ||||||
|     var sendConnection = setTimeout(function () { |  | ||||||
|       conn.removeListener('data', peekFirstPacket); |  | ||||||
|       console.log("[debug tcp conn] Connecting possible telnet client to device..."); |  | ||||||
|       pipeWs(null, 'tcp', nextDevice, conn, serviceport); |  | ||||||
|     }, 350); |  | ||||||
|     function peekFirstPacket(firstChunk) { |  | ||||||
|       clearTimeout(sendConnection); |  | ||||||
|       if (state.debug) { console.log("[DynTcp]", serviceport, "examining firstChunk from", Packer.socketToId(conn)); } |  | ||||||
|       conn.pause(); |  | ||||||
|       //conn.unshift(firstChunk);
 |  | ||||||
|       conn._handle.onread(firstChunk.length, firstChunk); |  | ||||||
| 
 |  | ||||||
|       var servername; |  | ||||||
|       var hostname; |  | ||||||
|       var str; |  | ||||||
|       var m; |  | ||||||
| 
 |  | ||||||
|       if (22 === firstChunk[0]) { |  | ||||||
|         servername = (sni(firstChunk)||'').toLowerCase(); |  | ||||||
|       } else if (firstChunk[0] > 32 && firstChunk[0] < 127) { |  | ||||||
|         str = firstChunk.toString(); |  | ||||||
|         m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); |  | ||||||
|         hostname = (m && m[1].toLowerCase() || '').split(':')[0]; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (servername || hostname) { |  | ||||||
|         if (servername) { |  | ||||||
|           conn.write("TLS with sni is allowed only on standard ports. If you've registered '" + servername + "' use port 443."); |  | ||||||
|         } else { |  | ||||||
|           conn.write("HTTP with Host headers is not allowed on dynamic ports. If you've registered '" + hostname + "' use port 80."); |  | ||||||
|         } |  | ||||||
|         conn.end(); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // pipeWs(servername, servicename, srv, client, serviceport)
 |  | ||||||
|       // remote.clients is managed as part of the piping process
 |  | ||||||
|       if (state.debug) { console.log("[DynTcp]", serviceport, "piping to srv (via loadbal)"); } |  | ||||||
|       pipeWs(null, 'tcp', nextDevice, conn, serviceport); |  | ||||||
| 
 |  | ||||||
|       process.nextTick(function () { conn.resume(); }); |  | ||||||
|     } |  | ||||||
|     conn.once('data', peekFirstPacket); |  | ||||||
|   } |  | ||||||
| , addToken: function addToken(state, srv, rawAuth) { |  | ||||||
|     console.log("[addToken]", rawAuth); |  | ||||||
|     if (srv.grants[rawAuth]) { |  | ||||||
|       console.log("addToken - duplicate"); |  | ||||||
|       // return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
 |  | ||||||
|       return state.Promise.resolve(null); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // [Extension] [Auth] This is where authentication is either handed off to
 |  | ||||||
|     //                    an extension or the default authencitation handler.
 |  | ||||||
|     return state.authenticate({ auth: rawAuth }).then(function (validatedTokenData) { |  | ||||||
|       console.log('\n[relay.js] rawAuth'); |  | ||||||
|       console.log(rawAuth); |  | ||||||
| 
 |  | ||||||
|       console.log('\n[relay.js] authnToken'); |  | ||||||
|       console.log(validatedTokenData); |  | ||||||
| 
 |  | ||||||
|       // For tracking state between token exchanges
 |  | ||||||
|       // and tacking on extra attributes (i.e. for extensions)
 |  | ||||||
|       // TODO close on delete
 |  | ||||||
|       if (!state.srvs[validatedTokenData.id]) { |  | ||||||
|         state.srvs[validatedTokenData.id] = {}; |  | ||||||
|       } |  | ||||||
|       if (!state.srvs[validatedTokenData.id].updateAuth) { |  | ||||||
|         // be sure to always pass latest srv since the connection may change
 |  | ||||||
|         // and reuse the same token
 |  | ||||||
|         state.srvs[validatedTokenData.id].updateAuth = function (srv, validatedTokenData) { |  | ||||||
|           return Server.onAuth(state, srv, rawAuth, validatedTokenData); |  | ||||||
|         }; |  | ||||||
|       } |  | ||||||
|       state.srvs[validatedTokenData.id].updateAuth(srv, validatedTokenData); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| , removeToken: function removeToken(state, srv, jwtoken) { |  | ||||||
|     var grant = srv.grants[jwtoken]; |  | ||||||
|     if (!grant) { |  | ||||||
|       return { message: 'specified token not present', code: 'E_INVALID_TOKEN'}; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Prevent any more browser connections for this grant being sent to this srv,
 |  | ||||||
|     // and any existing connections from trying to send more data across the connection.
 |  | ||||||
|     grant.domains.forEach(function (domainname) { |  | ||||||
|       Devices.remove(state.deviceLists, domainname, srv); |  | ||||||
|     }); |  | ||||||
|     grant.ports.forEach(function (portnumber) { |  | ||||||
|       Devices.remove(state.deviceLists, portnumber, srv); |  | ||||||
|       if (!srv.portsMap[portnumber]) { return; } |  | ||||||
|       try { |  | ||||||
|         srv.portsMap[portnumber].close(function () { |  | ||||||
|           console.log("[DynTcpConn] closing server for ", portnumber); |  | ||||||
|           delete srv.portsMap[portnumber]; |  | ||||||
|           delete PortServers[portnumber]; |  | ||||||
|         }); |  | ||||||
|       } catch(e) { /*ignore*/ } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // Close all of the existing browser connections associated with this websocket connection.
 |  | ||||||
|     Object.keys(grant.clients).forEach(function (cid) { |  | ||||||
|       Server.closeBrowserConn(state, srv, cid); |  | ||||||
|     }); |  | ||||||
|     delete srv.grants[jwtoken]; |  | ||||||
|     console.log("[ws] removed token '" + grant.currentDesc + "' from", srv.socketId); |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
| , getBrowserConn: function getBrowserConn(state, srv, cid) { |  | ||||||
|     return srv.clients[cid]; |  | ||||||
|   } |  | ||||||
| , closeBrowserConn: function closeBrowserConn(state, srv, cid) { |  | ||||||
|     if (!srv.clients[cid]) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     PromiseA.resolve().then(function () { |  | ||||||
|       var conn = srv.clients[cid]; |  | ||||||
|       conn.tunnelClosing = true; |  | ||||||
|       conn.end(); |  | ||||||
| 
 |  | ||||||
|       // If no data is buffered for writing then we don't need to wait for it to drain.
 |  | ||||||
|       if (!conn.bufferSize) { |  | ||||||
|         return timeoutPromise(500); |  | ||||||
|       } |  | ||||||
|       // Otherwise we want the connection to be able to finish, but we also want to impose
 |  | ||||||
|       // a time limit for it to drain, since it shouldn't have more than 1MB buffered.
 |  | ||||||
|       return new PromiseA(function (resolve) { |  | ||||||
|         var timeoutId = setTimeout(resolve, 60*1000); |  | ||||||
|         conn.once('drain', function () { |  | ||||||
|           clearTimeout(timeoutId); |  | ||||||
|           setTimeout(resolve, 500); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }).then(function () { |  | ||||||
|       if (srv.clients[cid]) { |  | ||||||
|         console.warn(cid, 'browser connection still present after calling `end`'); |  | ||||||
|         srv.clients[cid].destroy(); |  | ||||||
|         return timeoutPromise(500); |  | ||||||
|       } |  | ||||||
|     }).then(function () { |  | ||||||
|       if (srv.clients[cid]) { |  | ||||||
|         console.error(cid, 'browser connection still present after calling `destroy`'); |  | ||||||
|         delete srv.clients[cid]; |  | ||||||
|       } |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       console.warn('failed to close browser connection', cid, err); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| , parseAuth: function parseAuth(state, srv) { |  | ||||||
|     var authn = (srv.upgradeReq.headers.authorization||'').split(/\s+/); |  | ||||||
|     if (authn[0] && 'basic' === authn[0].toLowerCase()) { |  | ||||||
|       try { |  | ||||||
|         authn = new Buffer(authn[1], 'base64').toString('ascii').split(':'); |  | ||||||
|         return authn[1]; |  | ||||||
|       } catch (err) { } |  | ||||||
|     } |  | ||||||
|     return url.parse(srv.upgradeReq.url, true).query.access_token; |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| module.exports = Server; |  | ||||||
| @ -18,16 +18,13 @@ module.exports.createTcpConnectionHandler = function (state) { | |||||||
|     //});
 |     //});
 | ||||||
| 
 | 
 | ||||||
|     //return;
 |     //return;
 | ||||||
|     //conn.once('data', function (firstChunk) {
 |     conn.once('data', function (firstChunk) { | ||||||
|     //});
 |  | ||||||
|     conn.once('readable', function () { |  | ||||||
|       var firstChunk = conn.read(); |  | ||||||
|       var service = 'tcp'; |       var service = 'tcp'; | ||||||
|       var servername; |       var servername; | ||||||
|       var str; |       var str; | ||||||
|       var m; |       var m; | ||||||
| 
 | 
 | ||||||
|       //conn.pause();
 |       conn.pause(); | ||||||
|       conn.unshift(firstChunk); |       conn.unshift(firstChunk); | ||||||
| 
 | 
 | ||||||
|       // BUG XXX: this assumes that the packet won't be chunked smaller
 |       // BUG XXX: this assumes that the packet won't be chunked smaller
 | ||||||
| @ -41,11 +38,9 @@ module.exports.createTcpConnectionHandler = function (state) { | |||||||
|         if (fn) { |         if (fn) { | ||||||
|           state[fn](servername, conn); |           state[fn](servername, conn); | ||||||
|         } |         } | ||||||
|         /* |  | ||||||
|         process.nextTick(function () { |         process.nextTick(function () { | ||||||
|           conn.resume(); |           conn.resume(); | ||||||
|         }); |         }); | ||||||
|         */ |  | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       function tryTls() { |       function tryTls() { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user