| 
									
										
										
										
											2018-06-11 14:52:01 -06:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var common = module.exports; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var path = require('path'); | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  | var url = require('url'); | 
					
						
							| 
									
										
										
										
											2018-06-29 15:18:32 -06:00
										 |  |  | var fs = require('fs'); | 
					
						
							| 
									
										
										
										
											2018-06-11 14:52:01 -06:00
										 |  |  | var mkdirp = require('mkdirp'); | 
					
						
							|  |  |  | var os = require('os'); | 
					
						
							|  |  |  | var homedir = os.homedir(); | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  | var urequest = require('@coolaj86/urequest'); | 
					
						
							| 
									
										
										
										
											2018-06-11 14:52:01 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-29 04:15:23 -06:00
										 |  |  | common._NOTIFICATIONS = { | 
					
						
							|  |  |  |   'newsletter': [ 'newsletter', 'communityMember' ] | 
					
						
							|  |  |  | , 'important': [ 'communityMember' ] | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | common.CONFIG_KEYS = [ | 
					
						
							|  |  |  |   'newsletter' | 
					
						
							|  |  |  | , 'communityMember' | 
					
						
							|  |  |  | , 'telemetry' | 
					
						
							|  |  |  | , 'sshAuto' | 
					
						
							|  |  |  | , 'email' | 
					
						
							|  |  |  | , 'agreeTos' | 
					
						
							|  |  |  | , 'relay' | 
					
						
							|  |  |  | , 'token' | 
					
						
							|  |  |  | , 'pretoken' | 
					
						
							|  |  |  | , 'secret' | 
					
						
							|  |  |  | ]; | 
					
						
							|  |  |  | //, '_servernames' // list instead of object
 | 
					
						
							|  |  |  | //, '_ports'       // list instead of object
 | 
					
						
							|  |  |  | //, '_otp'         // otp should not be saved
 | 
					
						
							|  |  |  | //, '_token'       // temporary token
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-29 15:18:32 -06:00
										 |  |  | common.getPort = function (config, cb) { | 
					
						
							|  |  |  |   var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port'); | 
					
						
							|  |  |  |   if (cb) { | 
					
						
							|  |  |  |     return fs.readFile(portfile, 'utf8', function (err, text) { | 
					
						
							|  |  |  |       cb(err, parseInt((text||'').trim(), 10) || null); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } else { | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       return parseInt(fs.readFileSync(portfile, 'utf8').trim(), 10) || null; | 
					
						
							|  |  |  |     } catch(e) { | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | common.setPort = function (config, num, cb) { | 
					
						
							|  |  |  |   var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port'); | 
					
						
							|  |  |  |   var numstr = (num || '').toString(); | 
					
						
							|  |  |  |   if (cb) { | 
					
						
							|  |  |  |     return fs.writeFile(portfile, numstr, 'utf8', function (err) { | 
					
						
							|  |  |  |       cb(err); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } else { | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       return fs.writeFileSync(portfile, numstr, 'utf8'); | 
					
						
							|  |  |  |     } catch(e) { | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | common.removePort = function (config, cb) { | 
					
						
							|  |  |  |   var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port'); | 
					
						
							|  |  |  |   if (cb) { | 
					
						
							|  |  |  |     return fs.unlink(portfile, function (err, text) { | 
					
						
							|  |  |  |       cb(err, (text||'').trim()); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } else { | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       return fs.unlinkSync(portfile); | 
					
						
							|  |  |  |     } catch(e) { | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | common.pipename = function (config) { | 
					
						
							| 
									
										
										
										
											2018-06-14 01:26:32 -06:00
										 |  |  |   var _ipc = { | 
					
						
							| 
									
										
										
										
											2018-06-28 20:35:58 -06:00
										 |  |  |     path: (config.sock || common.DEFAULT_SOCK_PATH) | 
					
						
							| 
									
										
										
										
											2018-06-14 01:26:32 -06:00
										 |  |  |   , comment: (/^win/i.test(os.platform()) ? 'windows pipe' : 'unix socket') | 
					
						
							|  |  |  |   , type: (/^win/i.test(os.platform()) ? 'pipe' : 'socket') | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  |   if ('pipe' === _ipc.type) { | 
					
						
							| 
									
										
										
										
											2018-06-14 02:39:34 -06:00
										 |  |  |     _ipc.path = '\\\\?\\pipe' + _ipc.path.replace(/\//, '\\'); | 
					
						
							| 
									
										
										
										
											2018-06-11 14:52:01 -06:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-06-29 15:18:32 -06:00
										 |  |  |   return _ipc; | 
					
						
							| 
									
										
										
										
											2018-06-11 14:52:01 -06:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2018-06-28 20:35:58 -06:00
										 |  |  | common.DEFAULT_SOCK_PATH = path.join(homedir, '.local/share/telebit/var/run', 'telebit.sock'); | 
					
						
							|  |  |  | common.DEFAULT_CONFIG_PATH = path.join(homedir, '.config/telebit', 'telebitd.yml'); | 
					
						
							| 
									
										
										
										
											2018-06-11 14:52:01 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-16 01:11:02 +00:00
										 |  |  | common.parseUrl = function (hostname) { | 
					
						
							|  |  |  |   var location = url.parse(hostname); | 
					
						
							|  |  |  |   if (!location.protocol || /\./.test(location.protocol)) { | 
					
						
							|  |  |  |     hostname = 'https://' + hostname; | 
					
						
							|  |  |  |     location = url.parse(hostname); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   hostname = location.hostname + (location.port ? ':' + location.port : ''); | 
					
						
							|  |  |  |   hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname; | 
					
						
							|  |  |  |   return hostname; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2018-06-20 09:07:35 +00:00
										 |  |  | common.parseHostname = function (hostname) { | 
					
						
							|  |  |  |   var location = url.parse(hostname); | 
					
						
							|  |  |  |   if (!location.protocol || /\./.test(location.protocol)) { | 
					
						
							|  |  |  |     hostname = 'https://' + hostname; | 
					
						
							|  |  |  |     location = url.parse(hostname); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   //hostname = location.hostname + (location.port ? ':' + location.port : '');
 | 
					
						
							|  |  |  |   //hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
 | 
					
						
							|  |  |  |   return location.hostname; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2018-06-16 01:11:02 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | common.apiDirectory = '_apis/telebit.cloud/index.json'; | 
					
						
							| 
									
										
										
										
											2018-06-20 09:07:35 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | common.otp = function getOtp() { | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |   return Math.round(Math.random() * 9999).toString().padStart(4, '0'); | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2018-06-28 20:35:58 -06:00
										 |  |  | common.signToken = function (state) { | 
					
						
							|  |  |  |   var jwt = require('jsonwebtoken'); | 
					
						
							|  |  |  |   var tokenData = { | 
					
						
							|  |  |  |     domains: Object.keys(state.config.servernames || {}).filter(function (name) { | 
					
						
							|  |  |  |       return /\./.test(name); | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |   , ports: Object.keys(state.config.ports || {}).filter(function (port) { | 
					
						
							|  |  |  |       port = parseInt(port, 10); | 
					
						
							|  |  |  |       return port > 0 && port <= 65535; | 
					
						
							|  |  |  |     }) | 
					
						
							| 
									
										
										
										
											2018-06-29 04:15:23 -06:00
										 |  |  |   , aud: state._relayUrl | 
					
						
							| 
									
										
										
										
											2018-06-28 20:35:58 -06:00
										 |  |  |   , iss: Math.round(Date.now() / 1000) | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return jwt.sign(tokenData, state.config.secret); | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  | common.api = {}; | 
					
						
							|  |  |  | common.api.directory = function (state, next) { | 
					
						
							| 
									
										
										
										
											2018-06-29 04:15:23 -06:00
										 |  |  |   state._relayUrl = common.parseUrl(state.relay); | 
					
						
							|  |  |  |   urequest({ url: state._relayUrl + common.apiDirectory, json: true }, function (err, resp, dir) { | 
					
						
							| 
									
										
										
										
											2018-06-28 20:35:58 -06:00
										 |  |  |     if (!dir) { dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; } | 
					
						
							|  |  |  |     state._apiDirectory = dir; | 
					
						
							|  |  |  |     next(err, dir); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | common.api._parseWss = function (state, dir) { | 
					
						
							|  |  |  |   if (!dir || !dir.api_host) { | 
					
						
							|  |  |  |     dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-06-29 04:15:23 -06:00
										 |  |  |   state._relayHostname = common.parseHostname(state.relay); | 
					
						
							|  |  |  |   return dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state._relayHostname) + dir.tunnel.pathname; | 
					
						
							| 
									
										
										
										
											2018-06-28 20:35:58 -06:00
										 |  |  | }; | 
					
						
							|  |  |  | common.api.wss = function (state, cb) { | 
					
						
							|  |  |  |   common.api.directory(state, function (err, dir) { | 
					
						
							|  |  |  |     cb(err, common.api._parseWss(state, dir)); | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |   }); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | common.api.token = function (state, handlers) { | 
					
						
							|  |  |  |   common.api.directory(state, function (err, dir) { | 
					
						
							| 
									
										
										
										
											2018-06-21 11:20:11 +00:00
										 |  |  |     // directory, requested, connect, tunnelUrl, offer, granted, end
 | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |     function afterDir() { | 
					
						
							|  |  |  |       //console.log('[debug] after dir');
 | 
					
						
							| 
									
										
										
										
											2018-06-28 20:35:58 -06:00
										 |  |  |       state.wss = common.api._parseWss(state, dir); | 
					
						
							| 
									
										
										
										
											2018-06-21 11:20:11 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |       handlers.tunnelUrl(state.wss, function () { | 
					
						
							|  |  |  |         //console.log('[debug] after tunnelUrl');
 | 
					
						
							| 
									
										
										
										
											2018-06-29 04:15:23 -06:00
										 |  |  |         if (state.config.secret /* && !state.config.token */) { | 
					
						
							|  |  |  |           state.config._token = common.signToken(state); | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-06-29 04:15:23 -06:00
										 |  |  |         state.token = state.token || state.config.token || state.config._token; | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |         if (state.token) { | 
					
						
							|  |  |  |           //console.log('[debug] token via token or secret');
 | 
					
						
							| 
									
										
										
										
											2018-06-29 04:15:23 -06:00
										 |  |  |           // { token, pretoken }
 | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |           handlers.connect(state.token, function () { | 
					
						
							|  |  |  |             handlers.end(null, function () {}); | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-06-21 11:20:11 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // backwards compat (TODO remove)
 | 
					
						
							|  |  |  |         if (err || !dir || !dir.pair_request) { | 
					
						
							|  |  |  |           //console.log('[debug] no dir, connect');
 | 
					
						
							|  |  |  |           handlers.error(new Error("No token found or generated, and no pair_request api found.")); | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-06-22 23:55:18 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |         // TODO sign token with own private key, including public key and thumbprint
 | 
					
						
							|  |  |  |         //      (much like ACME JOSE account)
 | 
					
						
							| 
									
										
										
										
											2018-06-29 04:15:23 -06:00
										 |  |  |         var otp = state.config._otp; // common.otp();
 | 
					
						
							|  |  |  |         var authReq = { | 
					
						
							| 
									
										
										
										
											2018-06-21 11:20:11 +00:00
										 |  |  |           subject: state.config.email | 
					
						
							|  |  |  |         , subject_scheme: 'mailto' | 
					
						
							|  |  |  |           // TODO create domains list earlier
 | 
					
						
							| 
									
										
										
										
											2018-06-29 04:15:23 -06:00
										 |  |  |         , scope: (state.config._servernames || Object.keys(state.config.servernames || {})) | 
					
						
							|  |  |  |             .concat(state.config._ports || Object.keys(state.config.ports || {})).join(',') | 
					
						
							| 
									
										
										
										
											2018-06-21 11:20:11 +00:00
										 |  |  |         , otp: otp | 
					
						
							|  |  |  |         , hostname: os.hostname() | 
					
						
							|  |  |  |           // Used for User-Agent
 | 
					
						
							|  |  |  |         , os_type: os.type() | 
					
						
							|  |  |  |         , os_platform: os.platform() | 
					
						
							|  |  |  |         , os_release: os.release() | 
					
						
							|  |  |  |         , os_arch: os.arch() | 
					
						
							|  |  |  |         }; | 
					
						
							| 
									
										
										
										
											2018-06-29 04:15:23 -06:00
										 |  |  |         var pairRequestUrl = url.resolve('https://' + dir.api_host.replace(/:hostname/g, state._relayHostname), dir.pair_request.pathname); | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |         var req = { | 
					
						
							|  |  |  |           url: pairRequestUrl | 
					
						
							|  |  |  |         , method: dir.pair_request.method | 
					
						
							|  |  |  |         , json: authReq | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |         var firstReq = true; | 
					
						
							|  |  |  |         var firstReady = true; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         function gotoNext(req) { | 
					
						
							|  |  |  |           //console.log('[debug] gotoNext called');
 | 
					
						
							|  |  |  |           urequest(req, function (err, resp, body) { | 
					
						
							|  |  |  |             if (err) { | 
					
						
							|  |  |  |               //console.log('[debug] gotoNext error');
 | 
					
						
							|  |  |  |               err._request = req; | 
					
						
							|  |  |  |               err._hint = '[telebitd.js] pair request'; | 
					
						
							|  |  |  |               handlers.error(err, function () {}); | 
					
						
							|  |  |  |               return; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             function checkLocation() { | 
					
						
							|  |  |  |               //console.log('[debug] checkLocation');
 | 
					
						
							|  |  |  |               // pending, try again
 | 
					
						
							|  |  |  |               if ('pending' === body.status && resp.headers.location) { | 
					
						
							|  |  |  |                 //console.log('[debug] pending');
 | 
					
						
							|  |  |  |                 setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); | 
					
						
							|  |  |  |                 return; | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               if ('ready' === body.status) { | 
					
						
							|  |  |  |                 //console.log('[debug] ready');
 | 
					
						
							|  |  |  |                 if (firstReady) { | 
					
						
							|  |  |  |                   //console.log('[debug] first ready');
 | 
					
						
							|  |  |  |                   firstReady = false; | 
					
						
							|  |  |  |                   state.token = body.access_token; | 
					
						
							|  |  |  |                   state.config.token = state.token; | 
					
						
							|  |  |  |                   handlers.offer(body.access_token, function () { | 
					
						
							|  |  |  |                     /*ignore*/ | 
					
						
							|  |  |  |                   }); | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 setTimeout(gotoNext, 2 * 1000, req); | 
					
						
							|  |  |  |                 return; | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               if ('complete' === body.status) { | 
					
						
							|  |  |  |                 //console.log('[debug] complete');
 | 
					
						
							|  |  |  |                 handlers.granted(null, function () { | 
					
						
							|  |  |  |                   handlers.end(null, function () {}); | 
					
						
							|  |  |  |                 }); | 
					
						
							|  |  |  |                 return; | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               //console.log('[debug] bad status');
 | 
					
						
							|  |  |  |               var err = new Error("Bad State:" + body.status); | 
					
						
							|  |  |  |               err._request = req; | 
					
						
							|  |  |  |               handlers.error(err, function () {}); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (firstReq) { | 
					
						
							|  |  |  |               //console.log('[debug] first req');
 | 
					
						
							|  |  |  |               handlers.requested(authReq, function () { | 
					
						
							|  |  |  |                 handlers.connect(body.access_token || body.jwt, function () { | 
					
						
							| 
									
										
										
										
											2018-06-22 23:55:18 -06:00
										 |  |  |                   var err; | 
					
						
							|  |  |  |                   if (!resp.headers.location) { | 
					
						
							|  |  |  |                     err = new Error("bad authentication request response"); | 
					
						
							|  |  |  |                     err._resp = resp.toJSON(); | 
					
						
							|  |  |  |                     handlers.error(err, function () {}); | 
					
						
							|  |  |  |                     return; | 
					
						
							|  |  |  |                   } | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |                   setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); | 
					
						
							|  |  |  |                 }); | 
					
						
							|  |  |  |               }); | 
					
						
							|  |  |  |               firstReq = false; | 
					
						
							|  |  |  |               return; | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |               //console.log('[debug] other req');
 | 
					
						
							|  |  |  |               checkLocation(); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         gotoNext(req); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-21 18:59:56 +00:00
										 |  |  |     if (dir && dir.api_host) { | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |       handlers.directory(dir, afterDir); | 
					
						
							|  |  |  |     } else { | 
					
						
							| 
									
										
										
										
											2018-06-21 11:20:11 +00:00
										 |  |  |       // backwards compat
 | 
					
						
							|  |  |  |       dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; | 
					
						
							| 
									
										
										
										
											2018-06-21 11:01:16 +00:00
										 |  |  |       afterDir(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-20 09:07:35 +00:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-25 23:37:51 -06:00
										 |  |  | common._init = function (rootpath, confpath) { | 
					
						
							|  |  |  |   try { | 
					
						
							|  |  |  |     mkdirp.sync(path.join(rootpath, 'var', 'log')); | 
					
						
							|  |  |  |     mkdirp.sync(path.join(rootpath, 'var', 'run')); | 
					
						
							|  |  |  |     mkdirp.sync(path.join(confpath)); | 
					
						
							|  |  |  |   } catch(e) { | 
					
						
							|  |  |  |     console.error(e); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }; |