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)); |     serveAdmin(req, res, finalhandler(req, res)); | ||||||
|   }; |   }; | ||||||
|   state.httpTunnelServer = http.createServer(function (req, res) { |   state.httpTunnelServer = http.createServer(function (req, res) { | ||||||
|     //res.setHeader('connection', 'close');
 |     res.setHeader('connection', 'close'); | ||||||
|     if (state.extensions.webadmin) { |     if (state.extensions.webadmin) { | ||||||
|       state.extensions.webadmin(state, req, res); |       state.extensions.webadmin(state, req, res); | ||||||
|     } else { |     } 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() { |       return result || socketId; | ||||||
|         var result = Object.keys(remotes).map(function (jwtoken) { |     } | ||||||
|           return remotes[jwtoken].deviceId; |  | ||||||
|         }).join(';'); |  | ||||||
| 
 | 
 | ||||||
|         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) { |       PromiseA.resolve().then(function () { | ||||||
|         ws.send(Packer.pack(addr, data, service), {binary: true}); |         var conn = remote.clients[cid]; | ||||||
|       } |         conn.tunnelClosing = true; | ||||||
|  |         conn.end(); | ||||||
| 
 | 
 | ||||||
|       function getBrowserConn(cid) { |         // If no data is buffered for writing then we don't need to wait for it to drain.
 | ||||||
|         var browserConn; |         if (!conn.bufferSize) { | ||||||
|         Object.keys(remotes).some(function (jwtoken) { |           return timeoutPromise(500); | ||||||
|           if (remotes[jwtoken].clients[cid]) { |         } | ||||||
|             browserConn = remotes[jwtoken].clients[cid]; |         // Otherwise we want the connection to be able to finish, but we also want to impose
 | ||||||
|             return true; |         // 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) { |       function onAuth(token) { | ||||||
|         var remote; |         var err; | ||||||
|         Object.keys(remotes).some(function (jwtoken) { |         if (!token) { | ||||||
|           if (remotes[jwtoken].clients[cid]) { |           err = new Error("invalid access token"); | ||||||
|             remote = remotes[jwtoken]; |           err.code = "E_INVALID_TOKEN"; | ||||||
|             return true; |           return state.Promise.reject(err); | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|         if (!remote) { |  | ||||||
|           return; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         PromiseA.resolve().then(function () { |         if (!Array.isArray(token.domains)) { | ||||||
|           var conn = remote.clients[cid]; |           if ('string' === typeof token.name) { | ||||||
|           conn.tunnelClosing = true; |             token.domains = [ token.name ]; | ||||||
|           conn.end(); |           } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|           // If no data is buffered for writing then we don't need to wait for it to drain.
 |         if (!Array.isArray(token.domains) || !token.domains.length) { | ||||||
|           if (!conn.bufferSize) { |           err = new Error("invalid domains array"); | ||||||
|             return timeoutPromise(500); |           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.
 |           token.pausedConns.forEach(function (conn) { | ||||||
|           return new PromiseA(function (resolve) { |             if (!conn.manualPause) { | ||||||
|             var timeoutId = setTimeout(resolve, 60*1000); |               // console.log('resuming', conn.tunnelCid, 'now that the web socket has caught up');
 | ||||||
|             conn.once('drain', function () { |               conn.resume(); | ||||||
|               clearTimeout(timeoutId); |             } | ||||||
|               setTimeout(resolve, 500); |  | ||||||
|             }); |  | ||||||
|           }); |           }); | ||||||
|         }).then(function () { |           token.pausedConns.length = 0; | ||||||
|           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); |  | ||||||
|         }); |         }); | ||||||
|       } |  | ||||||
| 
 | 
 | ||||||
|       function addToken(jwtoken) { |         token.domains.forEach(function (domainname) { | ||||||
|  |           Devices.add(state.deviceLists, domainname, token); | ||||||
|  |         }); | ||||||
| 
 | 
 | ||||||
|         function onAuth(token) { |         console.log('[DEBUG] got to firstToken check'); | ||||||
|           var err; |  | ||||||
|           if (!token) { |  | ||||||
|             err = new Error("invalid access token"); |  | ||||||
|             err.code = "E_INVALID_TOKEN"; |  | ||||||
|             return state.Promise.reject(err); |  | ||||||
|           } |  | ||||||
| 
 | 
 | ||||||
|           if (!Array.isArray(token.domains)) { |         if (!firstToken || firstToken === jwtoken) { | ||||||
|             if ('string' === typeof token.name) { |           firstToken = jwtoken; | ||||||
|               token.domains = [ token.name ]; |           token.dynamicPorts = []; | ||||||
|             } |           token.dynamicNames = []; | ||||||
|           } |  | ||||||
| 
 | 
 | ||||||
|           if (!Array.isArray(token.domains) || !token.domains.length) { |           function onDynTcpReady() { | ||||||
|             err = new Error("invalid domains array"); |             var serviceport = this.address().port; | ||||||
|             err.code = "E_INVALID_NAME"; |             console.info('[DynTcpConn] Port', serviceport, 'now open for', token.deviceId); | ||||||
|             return state.Promise.reject(err); |             token.dynamicPorts.push(serviceport); | ||||||
|           } |             Devices.add(state.deviceLists, serviceport, token); | ||||||
|           if (token.domains.some(function (name) { return typeof name !== 'string'; })) { |             var hri = require('human-readable-ids').hri; | ||||||
|             err = new Error("invalid domain name(s)"); |             var hrname = hri.random() + '.telebit.cloud'; | ||||||
|             err.code = "E_INVALID_NAME"; |             token.dynamicNames.push(hrname); | ||||||
|             return state.Promise.reject(err); |             // TODO restrict to authenticated device
 | ||||||
|           } |             // TODO pull servername from config
 | ||||||
| 
 |             // TODO remove hrname on disconnect
 | ||||||
|           // Add the custom properties we need to manage this remote, then add it to all the relevant
 |             Devices.add(state.deviceLists, hrname, token); | ||||||
|           // domains and the list of all this websocket's remotes.
 |             sendTunnelMsg( | ||||||
|           token.deviceId = (token.device && (token.device.id || token.device.hostname)) || token.domains.join(','); |               null | ||||||
|           token.ws = ws; |             , [ 2 | ||||||
|           token.upgradeReq = upgradeReq; |               , 'grant' | ||||||
|           token.clients = {}; |               , [ ['ssh+https', hrname, 443 ] | ||||||
| 
 |                 , ['ssh', 'ssh.telebit.cloud', serviceport ] | ||||||
|           token.pausedConns = []; |                 , ['tcp', 'tcp.telebit.cloud', serviceport] | ||||||
|           ws._socket.on('drain', function () { |                 , ['https', hrname ] | ||||||
|             // 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 ] |  | ||||||
|                   ] |  | ||||||
|                 ] |                 ] | ||||||
|               , 'control' |               ] | ||||||
|               ); |             , '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); |  | ||||||
|             } |  | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           remotes[jwtoken] = token; |           try { | ||||||
|           console.info("[ws] authorized", socketId, "for", token.deviceId); |             token.server = require('net').createServer(onDynTcpConn).listen(0, onDynTcpReady); | ||||||
|           return null; |             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]) { |         remotes[jwtoken] = token; | ||||||
|           // return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
 |         console.info("[ws] authorized", socketId, "for", token.deviceId); | ||||||
|           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; |         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 = { |       var commandHandlers = { | ||||||
|         add_token: addToken |         add_token: addToken | ||||||
|       , auth: addToken |       , auth: addToken | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user