Merge branch 'master' into commercial
This commit is contained in:
		
						commit
						ab35fdc40e
					
				| @ -120,7 +120,7 @@ module.exports.create = function (state) { | ||||
|     serveAdmin(req, res, finalhandler(req, res)); | ||||
|   }; | ||||
|   state.httpTunnelServer = http.createServer(function (req, res) { | ||||
|     //res.setHeader('connection', 'close');
 | ||||
|     res.setHeader('connection', 'close'); | ||||
|     if (state.extensions.webadmin) { | ||||
|       state.extensions.webadmin(state, req, res); | ||||
|     } else { | ||||
|  | ||||
							
								
								
									
										391
									
								
								lib/relay.js
									
									
									
									
									
								
							
							
						
						
									
										391
									
								
								lib/relay.js
									
									
									
									
									
								
							| @ -113,228 +113,227 @@ module.exports.create = function (state) { | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     function next() { | ||||
|     function logName() { | ||||
|       var result = Object.keys(remotes).map(function (jwtoken) { | ||||
|         return remotes[jwtoken].deviceId; | ||||
|       }).join(';'); | ||||
| 
 | ||||
|       function logName() { | ||||
|         var result = Object.keys(remotes).map(function (jwtoken) { | ||||
|           return remotes[jwtoken].deviceId; | ||||
|         }).join(';'); | ||||
|       return result || socketId; | ||||
|     } | ||||
| 
 | ||||
|         return result || socketId; | ||||
|     function sendTunnelMsg(addr, data, service) { | ||||
|       ws.send(Packer.pack(addr, data, service), {binary: true}); | ||||
|     } | ||||
| 
 | ||||
|     function getBrowserConn(cid) { | ||||
|       var browserConn; | ||||
|       Object.keys(remotes).some(function (jwtoken) { | ||||
|         if (remotes[jwtoken].clients[cid]) { | ||||
|           browserConn = remotes[jwtoken].clients[cid]; | ||||
|           return true; | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       return browserConn; | ||||
|     } | ||||
| 
 | ||||
|     function closeBrowserConn(cid) { | ||||
|       var remote; | ||||
|       Object.keys(remotes).some(function (jwtoken) { | ||||
|         if (remotes[jwtoken].clients[cid]) { | ||||
|           remote = remotes[jwtoken]; | ||||
|           return true; | ||||
|         } | ||||
|       }); | ||||
|       if (!remote) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       function sendTunnelMsg(addr, data, service) { | ||||
|         ws.send(Packer.pack(addr, data, service), {binary: true}); | ||||
|       } | ||||
|       PromiseA.resolve().then(function () { | ||||
|         var conn = remote.clients[cid]; | ||||
|         conn.tunnelClosing = true; | ||||
|         conn.end(); | ||||
| 
 | ||||
|       function getBrowserConn(cid) { | ||||
|         var browserConn; | ||||
|         Object.keys(remotes).some(function (jwtoken) { | ||||
|           if (remotes[jwtoken].clients[cid]) { | ||||
|             browserConn = remotes[jwtoken].clients[cid]; | ||||
|             return true; | ||||
|           } | ||||
|         // 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 (remote.clients[cid]) { | ||||
|           console.warn(cid, 'browser connection still present after calling `end`'); | ||||
|           remote.clients[cid].destroy(); | ||||
|           return timeoutPromise(500); | ||||
|         } | ||||
|       }).then(function () { | ||||
|         if (remote.clients[cid]) { | ||||
|           console.error(cid, 'browser connection still present after calling `destroy`'); | ||||
|           delete remote.clients[cid]; | ||||
|         } | ||||
|       }).catch(function (err) { | ||||
|         console.warn('failed to close browser connection', cid, err); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|         return browserConn; | ||||
|       } | ||||
|     function addToken(jwtoken) { | ||||
| 
 | ||||
|       function closeBrowserConn(cid) { | ||||
|         var remote; | ||||
|         Object.keys(remotes).some(function (jwtoken) { | ||||
|           if (remotes[jwtoken].clients[cid]) { | ||||
|             remote = remotes[jwtoken]; | ||||
|             return true; | ||||
|           } | ||||
|         }); | ||||
|         if (!remote) { | ||||
|           return; | ||||
|       function onAuth(token) { | ||||
|         var err; | ||||
|         if (!token) { | ||||
|           err = new Error("invalid access token"); | ||||
|           err.code = "E_INVALID_TOKEN"; | ||||
|           return state.Promise.reject(err); | ||||
|         } | ||||
| 
 | ||||
|         PromiseA.resolve().then(function () { | ||||
|           var conn = remote.clients[cid]; | ||||
|           conn.tunnelClosing = true; | ||||
|           conn.end(); | ||||
|         if (!Array.isArray(token.domains)) { | ||||
|           if ('string' === typeof token.name) { | ||||
|             token.domains = [ token.name ]; | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|           // If no data is buffered for writing then we don't need to wait for it to drain.
 | ||||
|           if (!conn.bufferSize) { | ||||
|             return timeoutPromise(500); | ||||
|         if (!Array.isArray(token.domains) || !token.domains.length) { | ||||
|           err = new Error("invalid domains array"); | ||||
|           err.code = "E_INVALID_NAME"; | ||||
|           return state.Promise.reject(err); | ||||
|         } | ||||
|         if (token.domains.some(function (name) { return typeof name !== 'string'; })) { | ||||
|           err = new Error("invalid domain name(s)"); | ||||
|           err.code = "E_INVALID_NAME"; | ||||
|           return state.Promise.reject(err); | ||||
|         } | ||||
| 
 | ||||
|         // 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 remotes.
 | ||||
|         token.deviceId = (token.device && (token.device.id || token.device.hostname)) || token.domains.join(','); | ||||
|         token.ws = ws; | ||||
|         token.upgradeReq = upgradeReq; | ||||
|         token.clients = {}; | ||||
| 
 | ||||
|         token.pausedConns = []; | ||||
|         ws._socket.on('drain', function () { | ||||
|           // the websocket library has it's own buffer apart from node's socket buffer, but that one
 | ||||
|           // is much more difficult to watch, so we watch for the lower level buffer to drain and
 | ||||
|           // then check to see if the upper level buffer is still too full to write to. Note that
 | ||||
|           // the websocket library buffer has something to do with compression, so I'm not requiring
 | ||||
|           // that to be 0 before we start up again.
 | ||||
|           if (ws.bufferedAmount > 128*1024) { | ||||
|             return; | ||||
|           } | ||||
|           // 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); | ||||
|             }); | ||||
| 
 | ||||
|           token.pausedConns.forEach(function (conn) { | ||||
|             if (!conn.manualPause) { | ||||
|               // console.log('resuming', conn.tunnelCid, 'now that the web socket has caught up');
 | ||||
|               conn.resume(); | ||||
|             } | ||||
|           }); | ||||
|         }).then(function () { | ||||
|           if (remote.clients[cid]) { | ||||
|             console.warn(cid, 'browser connection still present after calling `end`'); | ||||
|             remote.clients[cid].destroy(); | ||||
|             return timeoutPromise(500); | ||||
|           } | ||||
|         }).then(function () { | ||||
|           if (remote.clients[cid]) { | ||||
|             console.error(cid, 'browser connection still present after calling `destroy`'); | ||||
|             delete remote.clients[cid]; | ||||
|           } | ||||
|         }).catch(function (err) { | ||||
|           console.warn('failed to close browser connection', cid, err); | ||||
|           token.pausedConns.length = 0; | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       function addToken(jwtoken) { | ||||
|         token.domains.forEach(function (domainname) { | ||||
|           Devices.add(state.deviceLists, domainname, token); | ||||
|         }); | ||||
| 
 | ||||
|         function onAuth(token) { | ||||
|           var err; | ||||
|           if (!token) { | ||||
|             err = new Error("invalid access token"); | ||||
|             err.code = "E_INVALID_TOKEN"; | ||||
|             return state.Promise.reject(err); | ||||
|           } | ||||
|         console.log('[DEBUG] got to firstToken check'); | ||||
| 
 | ||||
|           if (!Array.isArray(token.domains)) { | ||||
|             if ('string' === typeof token.name) { | ||||
|               token.domains = [ token.name ]; | ||||
|             } | ||||
|           } | ||||
|         if (!firstToken || firstToken === jwtoken) { | ||||
|           firstToken = jwtoken; | ||||
|           token.dynamicPorts = []; | ||||
|           token.dynamicNames = []; | ||||
| 
 | ||||
|           if (!Array.isArray(token.domains) || !token.domains.length) { | ||||
|             err = new Error("invalid domains array"); | ||||
|             err.code = "E_INVALID_NAME"; | ||||
|             return state.Promise.reject(err); | ||||
|           } | ||||
|           if (token.domains.some(function (name) { return typeof name !== 'string'; })) { | ||||
|             err = new Error("invalid domain name(s)"); | ||||
|             err.code = "E_INVALID_NAME"; | ||||
|             return state.Promise.reject(err); | ||||
|           } | ||||
| 
 | ||||
|           // 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 remotes.
 | ||||
|           token.deviceId = (token.device && (token.device.id || token.device.hostname)) || token.domains.join(','); | ||||
|           token.ws = ws; | ||||
|           token.upgradeReq = upgradeReq; | ||||
|           token.clients = {}; | ||||
| 
 | ||||
|           token.pausedConns = []; | ||||
|           ws._socket.on('drain', function () { | ||||
|             // the websocket library has it's own buffer apart from node's socket buffer, but that one
 | ||||
|             // is much more difficult to watch, so we watch for the lower level buffer to drain and
 | ||||
|             // then check to see if the upper level buffer is still too full to write to. Note that
 | ||||
|             // the websocket library buffer has something to do with compression, so I'm not requiring
 | ||||
|             // that to be 0 before we start up again.
 | ||||
|             if (ws.bufferedAmount > 128*1024) { | ||||
|               return; | ||||
|             } | ||||
| 
 | ||||
|             token.pausedConns.forEach(function (conn) { | ||||
|               if (!conn.manualPause) { | ||||
|                 // console.log('resuming', conn.tunnelCid, 'now that the web socket has caught up');
 | ||||
|                 conn.resume(); | ||||
|               } | ||||
|             }); | ||||
|             token.pausedConns.length = 0; | ||||
|           }); | ||||
| 
 | ||||
|           token.domains.forEach(function (domainname) { | ||||
|             Devices.add(state.deviceLists, domainname, token); | ||||
|           }); | ||||
| 
 | ||||
|           console.log('[DEBUG] got to firstToken check'); | ||||
| 
 | ||||
|           if (!firstToken || firstToken === jwtoken) { | ||||
|             firstToken = jwtoken; | ||||
|             token.dynamicPorts = []; | ||||
|             token.dynamicNames = []; | ||||
| 
 | ||||
|             function onDynTcpReady() { | ||||
|               var serviceport = this.address().port; | ||||
|               console.info('[DynTcpConn] Port', serviceport, 'now open for', token.deviceId); | ||||
|               token.dynamicPorts.push(serviceport); | ||||
|               Devices.add(state.deviceLists, serviceport, token); | ||||
|               var hri = require('human-readable-ids').hri; | ||||
|               var hrname = hri.random() + '.telebit.cloud'; | ||||
|               token.dynamicNames.push(hrname); | ||||
|               // TODO restrict to authenticated device
 | ||||
|               // TODO pull servername from config
 | ||||
|               // TODO remove hrname on disconnect
 | ||||
|               Devices.add(state.deviceLists, hrname, token); | ||||
|               sendTunnelMsg( | ||||
|                 null | ||||
|               , [ 2 | ||||
|                 , 'grant' | ||||
|                 , [ ['ssh+https', hrname, 443 ] | ||||
|                   , ['ssh', 'ssh.telebit.cloud', serviceport ] | ||||
|                   , ['tcp', 'tcp.telebit.cloud', serviceport] | ||||
|                   , ['https', hrname ] | ||||
|                   ] | ||||
|           function onDynTcpReady() { | ||||
|             var serviceport = this.address().port; | ||||
|             console.info('[DynTcpConn] Port', serviceport, 'now open for', token.deviceId); | ||||
|             token.dynamicPorts.push(serviceport); | ||||
|             Devices.add(state.deviceLists, serviceport, token); | ||||
|             var hri = require('human-readable-ids').hri; | ||||
|             var hrname = hri.random() + '.telebit.cloud'; | ||||
|             token.dynamicNames.push(hrname); | ||||
|             // TODO restrict to authenticated device
 | ||||
|             // TODO pull servername from config
 | ||||
|             // TODO remove hrname on disconnect
 | ||||
|             Devices.add(state.deviceLists, hrname, token); | ||||
|             sendTunnelMsg( | ||||
|               null | ||||
|             , [ 2 | ||||
|               , 'grant' | ||||
|               , [ ['ssh+https', hrname, 443 ] | ||||
|                 , ['ssh', 'ssh.telebit.cloud', serviceport ] | ||||
|                 , ['tcp', 'tcp.telebit.cloud', serviceport] | ||||
|                 , ['https', hrname ] | ||||
|                 ] | ||||
|               , 'control' | ||||
|               ); | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|               token.server = require('net').createServer(onDynTcpConn).listen(0, onDynTcpReady); | ||||
|               token.server.on('error', function (e) { | ||||
|                 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); | ||||
|             } | ||||
|               ] | ||||
|             , 'control' | ||||
|             ); | ||||
|           } | ||||
| 
 | ||||
|           remotes[jwtoken] = token; | ||||
|           console.info("[ws] authorized", socketId, "for", token.deviceId); | ||||
|           return null; | ||||
|           try { | ||||
|             token.server = require('net').createServer(onDynTcpConn).listen(0, onDynTcpReady); | ||||
|             token.server.on('error', function (e) { | ||||
|               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); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         if (remotes[jwtoken]) { | ||||
|           // return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
 | ||||
|           return state.Promise.resolve(null); | ||||
|         } | ||||
| 
 | ||||
|         return state.authenticate({ auth: jwtoken }).then(onAuth); | ||||
|       } | ||||
| 
 | ||||
|       function removeToken(jwtoken) { | ||||
|         var remote = remotes[jwtoken]; | ||||
|         if (!remote) { | ||||
|           return { message: 'specified token not present', code: 'E_INVALID_TOKEN'}; | ||||
|         } | ||||
| 
 | ||||
|         // Prevent any more browser connections being sent to this remote, and any existing
 | ||||
|         // connections from trying to send more data across the connection.
 | ||||
|         remote.domains.forEach(function (domainname) { | ||||
|           Devices.remove(state.deviceLists, domainname, remote); | ||||
|         }); | ||||
|         remote.dynamicPorts.forEach(function (portnumber) { | ||||
|           Devices.remove(state.deviceLists, portnumber, remote); | ||||
|         }); | ||||
|         remote.ws = null; | ||||
|         remote.upgradeReq = null; | ||||
|         if (remote.server) { | ||||
|           remote.serverPort = remote.server.address().port; | ||||
|           remote.server.close(function () { | ||||
|             console.log("[DynTcpConn] closing server for ", remote.serverPort); | ||||
|             remote.serverPort = null; | ||||
|           }); | ||||
|           remote.server = null; | ||||
|         } | ||||
| 
 | ||||
|         // Close all of the existing browser connections associated with this websocket connection.
 | ||||
|         Object.keys(remote.clients).forEach(function (cid) { | ||||
|           closeBrowserConn(cid); | ||||
|         }); | ||||
|         delete remotes[jwtoken]; | ||||
|         console.log("[ws] removed token '" + remote.deviceId + "' from", socketId); | ||||
|         remotes[jwtoken] = token; | ||||
|         console.info("[ws] authorized", socketId, "for", token.deviceId); | ||||
|         return null; | ||||
|       } | ||||
| 
 | ||||
|       if (remotes[jwtoken]) { | ||||
|         // return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
 | ||||
|         return state.Promise.resolve(null); | ||||
|       } | ||||
| 
 | ||||
|       return state.authenticate({ auth: jwtoken }).then(onAuth); | ||||
|     } | ||||
| 
 | ||||
|     function removeToken(jwtoken) { | ||||
|       var remote = remotes[jwtoken]; | ||||
|       if (!remote) { | ||||
|         return { message: 'specified token not present', code: 'E_INVALID_TOKEN'}; | ||||
|       } | ||||
| 
 | ||||
|       // Prevent any more browser connections being sent to this remote, and any existing
 | ||||
|       // connections from trying to send more data across the connection.
 | ||||
|       remote.domains.forEach(function (domainname) { | ||||
|         Devices.remove(state.deviceLists, domainname, remote); | ||||
|       }); | ||||
|       remote.dynamicPorts.forEach(function (portnumber) { | ||||
|         Devices.remove(state.deviceLists, portnumber, remote); | ||||
|       }); | ||||
|       remote.ws = null; | ||||
|       remote.upgradeReq = null; | ||||
|       if (remote.server) { | ||||
|         remote.serverPort = remote.server.address().port; | ||||
|         remote.server.close(function () { | ||||
|           console.log("[DynTcpConn] closing server for ", remote.serverPort); | ||||
|           remote.serverPort = null; | ||||
|         }); | ||||
|         remote.server = null; | ||||
|       } | ||||
| 
 | ||||
|       // Close all of the existing browser connections associated with this websocket connection.
 | ||||
|       Object.keys(remote.clients).forEach(function (cid) { | ||||
|         closeBrowserConn(cid); | ||||
|       }); | ||||
|       delete remotes[jwtoken]; | ||||
|       console.log("[ws] removed token '" + remote.deviceId + "' from", socketId); | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     function next() { | ||||
|       var commandHandlers = { | ||||
|         add_token: addToken | ||||
|       , auth: addToken | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user