Compare commits
	
		
			15 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 445e08c4d2 | ||
|  | 7bc2193d09 | ||
| f0049c7f06 | |||
| b5d57817cf | |||
| 1e3f7f671d | |||
| 00f3b3ab45 | |||
|  | 19a42a596c | ||
|  | de2290dd3e | ||
|  | 7db8a7a4ae | ||
| ceddf444b0 | |||
| 76ec7eb066 | |||
|  | 05dab9a52c | ||
| 20321b2fbe | |||
| 8bf4bfc7c0 | |||
| 4f0db8bc9c | 
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								README.md
									
									
									
									
									
								
							| @ -1,10 +1,7 @@ | |||||||
| # Telebit™ Remote | # Telebit™ Remote | a [Root](https://rootprojects.org) project | ||||||
| 
 | 
 | ||||||
| The T-Rex Long-Arm of the Internet | Because friends don't let friends localhost™ | ||||||
| 
 | 
 | ||||||
| <small>because friends don't let friends localhost</small> |  | ||||||
| 
 |  | ||||||
| | Sponsored by [ppl](https://ppl.family) |  | ||||||
| | **Telebit Remote** | | **Telebit Remote** | ||||||
| | [Telebit Relay](https://git.coolaj86.com/coolaj86/telebit-relay.js) | | [Telebit Relay](https://git.coolaj86.com/coolaj86/telebit-relay.js) | ||||||
| | [sclient](https://telebit.cloud/sclient) | | [sclient](https://telebit.cloud/sclient) | ||||||
| @ -123,8 +120,8 @@ Windows & Node.js | |||||||
| 1. Install [node.js](https://nodejs.org) | 1. Install [node.js](https://nodejs.org) | ||||||
| 2. Open _Node.js_ | 2. Open _Node.js_ | ||||||
| 2. Run the command `npm install -g telebit` | 2. Run the command `npm install -g telebit` | ||||||
| 2. Copy the example daemon conifg to your user folder `.config/telebit/telebitd.yml` (such as `/Users/John/.config/telebit/telebitd.yml`) | 2. Copy the example daemon config to your user folder `.config/telebit/telebitd.yml` (such as `/Users/John/.config/telebit/telebitd.yml`) | ||||||
| 2. Copy the example remote conifg to your user folder `.config/telebit/telebit.yml` (such as `/Users/John/.config/telebit/telebit.yml`) | 2. Copy the example remote config to your user folder `.config/telebit/telebit.yml` (such as `/Users/John/.config/telebit/telebit.yml`) | ||||||
| 2. Change the email address | 2. Change the email address | ||||||
| 2. Run `npx telebit init` and follow the instructions | 2. Run `npx telebit init` and follow the instructions | ||||||
| 2. Run `npx telebit list` | 2. Run `npx telebit list` | ||||||
| @ -526,7 +523,7 @@ rm -rf ~/.config/telebit ~/.local/share/telebit | |||||||
| Browser Library | Browser Library | ||||||
| ======= | ======= | ||||||
| 
 | 
 | ||||||
| This is implemented with websockets, so browser compatibility is a hopeful future outcome. Would love help. | This is implemented with websockets, so you should be able to | ||||||
| 
 | 
 | ||||||
| LICENSE | LICENSE | ||||||
| ======= | ======= | ||||||
|  | |||||||
| @ -8,13 +8,11 @@ var os = require('os'); | |||||||
| //var url = require('url');
 | //var url = require('url');
 | ||||||
| var fs = require('fs'); | var fs = require('fs'); | ||||||
| var path = require('path'); | var path = require('path'); | ||||||
|  | var http = require('http'); | ||||||
| //var https = require('https');
 | //var https = require('https');
 | ||||||
| var YAML = require('js-yaml'); | var YAML = require('js-yaml'); | ||||||
| var TOML = require('toml'); | var TOML = require('toml'); | ||||||
| var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8')); | var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8')); | ||||||
| var JWT = require('../lib/jwt.js'); |  | ||||||
| var keypairs = require('keypairs'); |  | ||||||
| 
 |  | ||||||
| /* | /* | ||||||
| if ('function' !== typeof TOML.stringify) { | if ('function' !== typeof TOML.stringify) { | ||||||
|   TOML.stringify = require('json2toml'); |   TOML.stringify = require('json2toml'); | ||||||
| @ -25,12 +23,8 @@ var camelCopy = recase.camelCopy.bind(recase); | |||||||
| //var snakeCopy = recase.snakeCopy.bind(recase);
 | //var snakeCopy = recase.snakeCopy.bind(recase);
 | ||||||
| 
 | 
 | ||||||
| var urequest = require('@coolaj86/urequest'); | var urequest = require('@coolaj86/urequest'); | ||||||
| var urequestAsync = require('util').promisify(urequest); |  | ||||||
| var common = require('../lib/cli-common.js'); | var common = require('../lib/cli-common.js'); | ||||||
| 
 | 
 | ||||||
| var defaultConfPath = path.join(os.homedir(), '.config/telebit'); |  | ||||||
| var defaultConfFile = path.join(defaultConfPath, 'telebit.yml'); |  | ||||||
| 
 |  | ||||||
| var argv = process.argv.slice(2); | var argv = process.argv.slice(2); | ||||||
| 
 | 
 | ||||||
| var argIndex = argv.indexOf('--config'); | var argIndex = argv.indexOf('--config'); | ||||||
| @ -45,30 +39,6 @@ if (-1 === argIndex) { | |||||||
| } | } | ||||||
| if (-1 !== argIndex) { | if (-1 !== argIndex) { | ||||||
|   confpath = argv.splice(argIndex, 2)[1]; |   confpath = argv.splice(argIndex, 2)[1]; | ||||||
|   state.configArg = confpath; |  | ||||||
|   // shortname
 |  | ||||||
|   if (state.configArg) { |  | ||||||
|     if (/^[\w:\.\-]+$/.test(state.configArg)) { |  | ||||||
|       state.configDir = defaultConfPath; |  | ||||||
|       state.configFile = path.join(defaultConfPath, confpath + '.yml'); |  | ||||||
|     } else if (/[\/\\]$/.test(state.configArg)) { |  | ||||||
|       state.configDir = state.configArg; |  | ||||||
|       state.configFile = path.join(state.configDir, 'telebit.yml'); |  | ||||||
|     } else if (/[\/\\][^\.\/\\]\.[^\.\/\\]$/.test(state.configArg)) { |  | ||||||
|       state.configDir = path.pathname(state.configArg); |  | ||||||
|       state.configFile = state.configArg; |  | ||||||
|     } else { |  | ||||||
|       console.error(); |  | ||||||
|       console.error("Not a valid config path, file, or shortname: '%s'", state.configArg); |  | ||||||
|       console.error(); |  | ||||||
|       console.error("Valid config options look like this:"); |  | ||||||
|       console.error(" Full path: ~/.config/telebit/telebit.yml (full path)"); |  | ||||||
|       console.error(" Directory: ~/.config/telebit/            (directory)"); |  | ||||||
|       console.error(" Shortname: lucky-duck                    (shortname)"); |  | ||||||
|       process.exit(37); |  | ||||||
|     } |  | ||||||
|     confpath = state.configFile; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| argIndex = argv.indexOf('--tty'); | argIndex = argv.indexOf('--tty'); | ||||||
| if (-1 !== argIndex) { | if (-1 !== argIndex) { | ||||||
| @ -87,9 +57,7 @@ function help() { | |||||||
| 
 | 
 | ||||||
| var verstr = [ pkg.name + ' remote v' + pkg.version ]; | var verstr = [ pkg.name + ' remote v' + pkg.version ]; | ||||||
| if (!confpath) { | if (!confpath) { | ||||||
|   state.configDir = defaultConfPath; |   confpath = path.join(os.homedir(), '.config/telebit/telebit.yml'); | ||||||
|   state.configFile = defaultConfFile; |  | ||||||
|   confpath = state.configFile; |  | ||||||
|   verstr.push('(--config \'' + confpath.replace(new RegExp('^' + os.homedir()), '~') + '\')'); |   verstr.push('(--config \'' + confpath.replace(new RegExp('^' + os.homedir()), '~') + '\')'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -155,10 +123,13 @@ function askForConfig(state, mainCb) { | |||||||
|             return; |             return; | ||||||
|           } |           } | ||||||
|           if (200 !== resp.statusCode || (Buffer.isBuffer(body) || 'object' !== typeof body) || !body.api_host) { |           if (200 !== resp.statusCode || (Buffer.isBuffer(body) || 'object' !== typeof body) || !body.api_host) { | ||||||
|             console.warn(TPLS.remote.setup.fail_relay_check |             console.warn("==================="); | ||||||
|               .replace(/{{\s*status_code\s*}}/, resp.statusCode) |             console.warn("      WARNING      "); | ||||||
|               .replace(/{{\s*url\s*}}/, urlstr) |             console.warn("==================="); | ||||||
|             ); |             console.warn(""); | ||||||
|  |             console.warn("[" + resp.statusCode + "] '" + urlstr + "'"); | ||||||
|  |             console.warn("This server does not describe a current telebit version (but it may still work)."); | ||||||
|  |             console.warn(""); | ||||||
|             console.warn(body); |             console.warn(body); | ||||||
|           } else if (body && body.pair_request) { |           } else if (body && body.pair_request) { | ||||||
|             state._can_pair = true; |             state._can_pair = true; | ||||||
| @ -187,7 +158,7 @@ function askForConfig(state, mainCb) { | |||||||
|     } |     } | ||||||
|   ]; |   ]; | ||||||
|   var standardSet = [ |   var standardSet = [ | ||||||
|     // There are questions that we need to ask in the CLI
 |     // There are questions that we need to aks in the CLI
 | ||||||
|     // if we can't guarantee that they are being asked in the web interface
 |     // if we can't guarantee that they are being asked in the web interface
 | ||||||
|     function askAgree(cb) { |     function askAgree(cb) { | ||||||
|       if (state.config.agreeTos) { cb(); return; } |       if (state.config.agreeTos) { cb(); return; } | ||||||
| @ -264,9 +235,10 @@ function askForConfig(state, mainCb) { | |||||||
|       //console.info("\tPrivate key (hex)");
 |       //console.info("\tPrivate key (hex)");
 | ||||||
|       console.info(""); |       console.info(""); | ||||||
|       rl.question('auth: ', function (resp) { |       rl.question('auth: ', function (resp) { | ||||||
|  |         var jwt = require('jsonwebtoken'); | ||||||
|         resp = (resp || '').trim(); |         resp = (resp || '').trim(); | ||||||
|         try { |         try { | ||||||
|           JWT.decode(resp); |           jwt.decode(resp); | ||||||
|           state.config.token = resp; |           state.config.token = resp; | ||||||
|         } catch(e) { |         } catch(e) { | ||||||
|           // is not jwt
 |           // is not jwt
 | ||||||
| @ -333,145 +305,389 @@ function askForConfig(state, mainCb) { | |||||||
|   next(); |   next(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var RC; | var utils = { | ||||||
|  |   request: function request(opts, fn) { | ||||||
|  |     if (!opts) { opts = {}; } | ||||||
|  |     var service = opts.service || 'config'; | ||||||
|  |     var req = http.request({ | ||||||
|  |       socketPath: state._ipc.path | ||||||
|  |     , method: opts.method || 'GET' | ||||||
|  |     , path: '/rpc/' + service | ||||||
|  |     }, function (resp) { | ||||||
|  |       var body = ''; | ||||||
| 
 | 
 | ||||||
| function parseConfig(err, text) { |       function finish() { | ||||||
|   function handleConfig(err, config) { |         if (200 !== resp.statusCode) { | ||||||
|     state.config = config; |           console.warn(resp.statusCode); | ||||||
|     var verstrd = [ pkg.name + ' daemon v' + state.config.version ]; |           console.warn(body || ('get' + service + ' failed')); | ||||||
|     if (state.config.version && state.config.version !== pkg.version) { |           //cb(new Error("not okay"), body);
 | ||||||
|       console.info(verstr.join(' '), verstrd.join(' ')); |           return; | ||||||
|     } else { |         } | ||||||
|       console.info(verstr.join(' ')); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     if (err) { |         if (!body) { fn(null, null); return; } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |           body = JSON.parse(body); | ||||||
|  |         } catch(e) { | ||||||
|  |           // ignore
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         fn(null, body); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (resp.headers['content-length']) { | ||||||
|  |         resp.on('data', function (chunk) { | ||||||
|  |           body += chunk.toString(); | ||||||
|  |         }); | ||||||
|  |         resp.on('end', function () { | ||||||
|  |           finish(); | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         finish(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     req.on('error', function (err) { | ||||||
|  |       // ENOENT - never started, cleanly exited last start, or creating socket at a different path
 | ||||||
|  |       // ECONNREFUSED - leftover socket just needs to be restarted
 | ||||||
|       if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { |       if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { | ||||||
|         console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to."); |         if (opts._taketwo) { | ||||||
|         console.error(err); |           console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to."); | ||||||
|       } else if ('ENOTSOCK' === err.code) { |           console.error(err); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         require('../usr/share/install-launcher.js').install({ env: process.env }, function (err) { | ||||||
|  |           if (err) { fn(err); return; } | ||||||
|  |           opts._taketwo = true; | ||||||
|  |           setTimeout(function () { | ||||||
|  |             utils.request(opts, fn); | ||||||
|  |           }, 2500); | ||||||
|  |         }); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       if ('ENOTSOCK' === err.code) { | ||||||
|         console.error(err); |         console.error(err); | ||||||
|         return; |         return; | ||||||
|       } else { |  | ||||||
|         console.error(err); |  | ||||||
|       } |       } | ||||||
|       process.exit(101); |       console.error(err); | ||||||
|       return; |       return; | ||||||
|     } |     }); | ||||||
|  |     req.end(); | ||||||
|  |   } | ||||||
|  | , putConfig: function putConfig(service, args, fn) { | ||||||
|  |     var req = http.request({ | ||||||
|  |       socketPath: state._ipc.path | ||||||
|  |     , method: 'POST' | ||||||
|  |     , path: '/rpc/' + service + '?_body=' + encodeURIComponent(JSON.stringify(args)) | ||||||
|  |     }, function (resp) { | ||||||
| 
 | 
 | ||||||
|     //
 |       function finish() { | ||||||
|     // check for init first, before anything else
 |         if ('function' === typeof fn) { | ||||||
|     // because it has arguments that may help in
 |           fn(null, resp); | ||||||
|     // the next steps
 |           return; | ||||||
|     //
 |  | ||||||
|     if (-1 !== argv.indexOf('init')) { |  | ||||||
|       parsers.init(argv, function (err) { |  | ||||||
|         if (err) { |  | ||||||
|           console.error("Error while initializing config [init]:"); |  | ||||||
|           throw err; |  | ||||||
|         } |  | ||||||
|         getToken(function (err) { |  | ||||||
|           if (err) { |  | ||||||
|             console.error("Error while getting token [init]:"); |  | ||||||
|             throw err; |  | ||||||
|           } |  | ||||||
|           parseCli(state); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!state.config.relay || !state.config.token) { |  | ||||||
|       if (!state.config.relay) { |  | ||||||
|         state.config.relay = 'telebit.cloud'; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       //console.log("question the user?", Date.now());
 |  | ||||||
|       askForConfig(state, function (err, state) { |  | ||||||
|         // no errors actually get passed, so this is just future-proofing
 |  | ||||||
|         if (err) { throw err; } |  | ||||||
| 
 |  | ||||||
|         if (!state.config.token && state._can_pair) { |  | ||||||
|           state.config._otp = common.otp(); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         //console.log("done questioning:", Date.now());
 |         console.info(""); | ||||||
|         if (!state.token && !state.config.token) { |         if (200 !== resp.statusCode) { | ||||||
|           if (err) { |           console.warn("'" + service + "' may have failed." | ||||||
|             console.error("Error while initializing config [init]:"); |            + " Consider peaking at the logs either with 'journalctl -xeu telebit' or /opt/telebit/var/log/error.log"); | ||||||
|             throw err; |           console.warn(resp.statusCode, body); | ||||||
|           } |           //cb(new Error("not okay"), body);
 | ||||||
|           getToken(function (err) { |           return; | ||||||
|             if (err) { |         } | ||||||
|               console.error("Error while getting token [init]:"); | 
 | ||||||
|               throw err; |         if (!body) { | ||||||
|             } |           console.info("👌"); | ||||||
|             parseCli(state); |           return; | ||||||
|           }); |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |           body = JSON.parse(body); | ||||||
|  |         } catch(e) { | ||||||
|  |           // ignore
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ("AWAIT_AUTH" === body.code) { | ||||||
|  |           console.info(body.message); | ||||||
|  |         } else if ("CONFIG" === body.code) { | ||||||
|  |           delete body.code; | ||||||
|  |           //console.info(TOML.stringify(body));
 | ||||||
|  |           console.info(YAML.safeDump(body)); | ||||||
|         } else { |         } else { | ||||||
|           parseCli(state); |           if ('http' === body.module) { | ||||||
|  |             // TODO we'll support slingshot-ing in the future
 | ||||||
|  |             if (String(body.local) === String(parseInt(body.local, 10))) { | ||||||
|  |               console.info('> Forwarding https://' + body.remote + ' => localhost:' + body.local); | ||||||
|  |             } else { | ||||||
|  |               console.info('> Serving ' + body.local + ' as https://' + body.remote); | ||||||
|  |             } | ||||||
|  |           } else if ('tcp' === body.module) { | ||||||
|  |               console.info('> Forwarding ' + state.config.relay + ':' + body.remote + ' => localhost:' + body.local); | ||||||
|  |           } else if ('ssh' === body.module) { | ||||||
|  |               //console.info('> Forwarding ' + state.config.relay + ' -p ' + JSON.stringify(body) + ' => localhost:' + body.local);
 | ||||||
|  |               console.info('> Forwarding ssh+https (openssl proxy) => localhost:' + body.local); | ||||||
|  |           } else { | ||||||
|  |             console.info(JSON.stringify(body, null, 2)); | ||||||
|  |           } | ||||||
|  |           console.info(); | ||||||
|         } |         } | ||||||
|       }); |       } | ||||||
|  | 
 | ||||||
|  |       var body = ''; | ||||||
|  |       if (resp.headers['content-length']) { | ||||||
|  |         resp.on('data', function (chunk) { | ||||||
|  |           body += chunk.toString(); | ||||||
|  |         }); | ||||||
|  |         resp.on('end', function () { | ||||||
|  |           finish(); | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         finish(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     req.on('error', function (err) { | ||||||
|  |       console.error('Put Config Error:'); | ||||||
|  |       console.error(err); | ||||||
|  |       return; | ||||||
|  |     }); | ||||||
|  |     req.end(); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Two styles:
 | ||||||
|  | //     http 3000
 | ||||||
|  | //     http modulename
 | ||||||
|  | function makeRpc(key) { | ||||||
|  |   if (key !== argv[0]) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   utils.putConfig(argv[0], argv.slice(1)); | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function packConfig(config) { | ||||||
|  |   return Object.keys(config).map(function (key) { | ||||||
|  |     var val = config[key]; | ||||||
|  |     if ('undefined' === val) { | ||||||
|  |       throw new Error("'undefined' used as a string value"); | ||||||
|  |     } | ||||||
|  |     if ('undefined' === typeof val) { | ||||||
|  |       //console.warn('[DEBUG]', key, 'is present but undefined');
 | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     if (val && 'object' === typeof val && !Array.isArray(val)) { | ||||||
|  |       val = JSON.stringify(val); | ||||||
|  |     } | ||||||
|  |     return key + ':' + val; // converts arrays to strings with ,
 | ||||||
|  |   }); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     //console.log("no questioning:");
 | function getToken(err, state) { | ||||||
|     parseCli(state); |   if (err) { | ||||||
|  |     console.error("Error while initializing config [init]:"); | ||||||
|  |     throw err; | ||||||
|  |   } | ||||||
|  |   state.relay = state.config.relay; | ||||||
|  | 
 | ||||||
|  |   // { _otp, config: {} }
 | ||||||
|  |   common.api.token(state, { | ||||||
|  |     error: function (err/*, next*/) { | ||||||
|  |       console.error("[Error] common.api.token:"); | ||||||
|  |       console.error(err); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |   , directory: function (dir, next) { | ||||||
|  |       //console.log('[directory] Telebit Relay Discovered:');
 | ||||||
|  |       //console.log(dir);
 | ||||||
|  |       state._apiDirectory = dir; | ||||||
|  |       next(); | ||||||
|  |     } | ||||||
|  |   , tunnelUrl: function (tunnelUrl, next) { | ||||||
|  |       //console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl);
 | ||||||
|  |       state.wss = tunnelUrl; | ||||||
|  |       next(); | ||||||
|  |     } | ||||||
|  |   , requested: function (authReq, next) { | ||||||
|  |       //console.log("[requested] Pairing Requested");
 | ||||||
|  |       state.config._otp = state.config._otp = authReq.otp; | ||||||
|  | 
 | ||||||
|  |       if (!state.config.token && state._can_pair) { | ||||||
|  |         console.info(""); | ||||||
|  |         console.info("=============================================="); | ||||||
|  |         console.info("                 Hey, Listen!                 "); | ||||||
|  |         console.info("=============================================="); | ||||||
|  |         console.info("                                              "); | ||||||
|  |         console.info("  GO CHECK YOUR EMAIL!                        "); | ||||||
|  |         console.info("                                              "); | ||||||
|  |         console.info("  DEVICE PAIR CODE:     0000                  ".replace(/0000/g, state.config._otp)); | ||||||
|  |         console.info("                                              "); | ||||||
|  |         console.info("=============================================="); | ||||||
|  |         console.info(""); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       next(); | ||||||
|  |     } | ||||||
|  |   , connect: function (pretoken, next) { | ||||||
|  |       //console.log("[connect] Enabling Pairing Locally...");
 | ||||||
|  |       state.config.pretoken = pretoken; | ||||||
|  |       state._connecting = true; | ||||||
|  | 
 | ||||||
|  |       // TODO use php-style object querification
 | ||||||
|  |       utils.putConfig('config', packConfig(state.config), function (err/*, body*/) { | ||||||
|  |         if (err) { | ||||||
|  |           state._error = err; | ||||||
|  |           console.error("Error while initializing config [connect]:"); | ||||||
|  |           console.error(err); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         console.info(TPLS.remote.waiting.replace(/{email}/, state.config.email)); | ||||||
|  |         next(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   , offer: function (token, next) { | ||||||
|  |       //console.log("[offer] Pairing Enabled by Relay");
 | ||||||
|  |       state.config.token = token; | ||||||
|  |       if (state._error) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       state._connecting = true; | ||||||
|  |       try { | ||||||
|  |         require('jsonwebtoken').decode(token); | ||||||
|  |         //console.log(require('jsonwebtoken').decode(token));
 | ||||||
|  |       } catch(e) { | ||||||
|  |         console.warn("[warning] could not decode token"); | ||||||
|  |       } | ||||||
|  |       utils.putConfig('config', packConfig(state.config), function (err/*, body*/) { | ||||||
|  |         if (err) { | ||||||
|  |           state._error = err; | ||||||
|  |           console.error("Error while initializing config [offer]:"); | ||||||
|  |           console.error(err); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         //console.log("Pairing Enabled Locally");
 | ||||||
|  |         next(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   , granted: function (_, next) { | ||||||
|  |       //console.log("[grant] Pairing complete!");
 | ||||||
|  |       next(); | ||||||
|  |     } | ||||||
|  |   , end: function () { | ||||||
|  |       utils.putConfig('enable', [], function (err) { | ||||||
|  |         if (err) { console.error(err); return; } | ||||||
|  |         console.info(TPLS.remote.success); | ||||||
|  | 
 | ||||||
|  |         // workaround for https://github.com/nodejs/node/issues/21319
 | ||||||
|  |         if (state._useTty) { | ||||||
|  |           setTimeout(function () { | ||||||
|  |             console.info(TPLS.remote.next_steps); | ||||||
|  |             process.exit(0); | ||||||
|  |           }, 0.5 * 1000); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         // end workaround
 | ||||||
|  | 
 | ||||||
|  |         parseCli(state); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function parseCli(/*state*/) { | ||||||
|  |   var special = [ | ||||||
|  |     'false', 'none', 'off', 'disable' | ||||||
|  |   , 'true', 'auto', 'on', 'enable' | ||||||
|  |   ]; | ||||||
|  |   if (-1 !== argv.indexOf('init')) { | ||||||
|  |     utils.putConfig('list', []/*, function (err) { | ||||||
|  |     }*/); | ||||||
|  |     return; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function parseCli(/*state*/) { |   if ([ 'ssh', 'http', 'tcp' ].some(function (key) { | ||||||
|     var special = [ |     if (key !== argv[0]) { | ||||||
|       'false', 'none', 'off', 'disable' |       return false; | ||||||
|     , 'true', 'auto', 'on', 'enable' |  | ||||||
|     ]; |  | ||||||
|     if (-1 !== argv.indexOf('init')) { |  | ||||||
|       RC.request({ service: 'list', method: 'POST', data: [] }, handleRemoteRequest('list')); |  | ||||||
|       return; |  | ||||||
|     } |     } | ||||||
| 
 |     if (argv[1]) { | ||||||
|     if ([ 'ssh', 'http', 'tcp' ].some(function (key) { |       if (String(argv[1]) === String(parseInt(argv[1], 10))) { | ||||||
|       if (key !== argv[0]) { |         // looks like a port
 | ||||||
|         return false; |         argv[1] = parseInt(argv[1], 10); | ||||||
|       } |       } else if (/\/|\\/.test(argv[1])) { | ||||||
|       if (argv[1]) { |         // looks like a path
 | ||||||
|         if (String(argv[1]) === String(parseInt(argv[1], 10))) { |         argv[1] = path.resolve(argv[1]); | ||||||
|           // looks like a port
 |         // TODO make a default assignment here
 | ||||||
|           argv[1] = parseInt(argv[1], 10); |       } else if (-1 === special.indexOf(argv[1])) { | ||||||
|         } else if (/\/|\\/.test(argv[1])) { |         console.error("Not sure what you meant by '" + argv[1] + "'."); | ||||||
|           // looks like a path
 |         console.error("Remember: paths should begin with ." + path.sep + ", like '." + path.sep + argv[1] + "'"); | ||||||
|           argv[1] = path.resolve(argv[1]); |  | ||||||
|           // TODO make a default assignment here
 |  | ||||||
|         } else if (-1 === special.indexOf(argv[1])) { |  | ||||||
|           console.error("Not sure what you meant by '" + argv[1] + "'."); |  | ||||||
|           console.error("Remember: paths should begin with ." + path.sep + ", like '." + path.sep + argv[1] + "'"); |  | ||||||
|           return true; |  | ||||||
|         } |  | ||||||
|         RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0])); |  | ||||||
|         return true; |         return true; | ||||||
|       } |       } | ||||||
|       help(); |       utils.putConfig(argv[0], argv.slice(1)); | ||||||
|       return true; |  | ||||||
|     })) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Two styles:
 |  | ||||||
|     //     http 3000
 |  | ||||||
|     //     http modulename
 |  | ||||||
|     function makeRpc(key) { |  | ||||||
|       if (key !== argv[0]) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|       RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0])); |  | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|     if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) { |     return true; | ||||||
|       return; |   })) { | ||||||
|     } |     return; | ||||||
| 
 |  | ||||||
|     help(); |  | ||||||
|     process.exit(11); |  | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   help(); | ||||||
|  |   process.exit(11); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function handleConfig(err, config) { | ||||||
|  |   //console.log('CONFIG');
 | ||||||
|  |   //console.log(config);
 | ||||||
|  |   state.config = config; | ||||||
|  |   var verstrd = [ pkg.name + ' daemon v' + state.config.version ]; | ||||||
|  |   if (state.config.version && state.config.version !== pkg.version) { | ||||||
|  |     console.info(verstr.join(' '), verstrd.join(' ')); | ||||||
|  |   } else { | ||||||
|  |     console.info(verstr.join(' ')); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (err) { console.error(err); process.exit(101); return; } | ||||||
|  | 
 | ||||||
|  |   //
 | ||||||
|  |   // check for init first, before anything else
 | ||||||
|  |   // because it has arguments that may help in
 | ||||||
|  |   // the next steps
 | ||||||
|  |   //
 | ||||||
|  |   if (-1 !== argv.indexOf('init')) { | ||||||
|  |     parsers.init(argv, getToken); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!state.config.relay || !state.config.token) { | ||||||
|  |     if (!state.config.relay) { | ||||||
|  |       state.config.relay = 'telebit.cloud'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     //console.log("question the user?", Date.now());
 | ||||||
|  |     askForConfig(state, function (err, state) { | ||||||
|  |       // no errors actually get passed, so this is just future-proofing
 | ||||||
|  |       if (err) { throw err; } | ||||||
|  | 
 | ||||||
|  |       if (!state.config.token && state._can_pair) { | ||||||
|  |         state.config._otp = common.otp(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       //console.log("done questioning:", Date.now());
 | ||||||
|  |       if (!state.token && !state.config.token) { | ||||||
|  |         getToken(err, state); | ||||||
|  |       } else { | ||||||
|  |         parseCli(state); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   //console.log("no questioning:");
 | ||||||
|  |   parseCli(state); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function parseConfig(err, text) { | ||||||
|   try { |   try { | ||||||
|     state._clientConfig = JSON.parse(text || '{}'); |     state._clientConfig = JSON.parse(text || '{}'); | ||||||
|   } catch(e1) { |   } catch(e1) { | ||||||
| @ -490,7 +706,13 @@ function parseConfig(err, text) { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   state._clientConfig = camelCopy(state._clientConfig || {}) || {}; |   state._clientConfig = camelCopy(state._clientConfig || {}) || {}; | ||||||
|   RC = require('../lib/rc/index.js').create(state); |   common._init( | ||||||
|  |     // make a default working dir and log dir
 | ||||||
|  |     state._clientConfig.root || path.join(os.homedir(), '.local/share/telebit') | ||||||
|  |   , (state._clientConfig.root && path.join(state._clientConfig.root, 'etc')) | ||||||
|  |       || path.resolve(common.DEFAULT_CONFIG_PATH, '..') | ||||||
|  |   ); | ||||||
|  |   state._ipc = common.pipename(state._clientConfig, true); | ||||||
| 
 | 
 | ||||||
|   if (!Object.keys(state._clientConfig).length) { |   if (!Object.keys(state._clientConfig).length) { | ||||||
|     console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')'); |     console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')'); | ||||||
| @ -505,229 +727,7 @@ function parseConfig(err, text) { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function handleRemoteRequest(service, fn) { |   utils.request({ service: 'config' }, handleConfig); | ||||||
|     return function (err, body) { |  | ||||||
|       if ('function' === typeof fn) { |  | ||||||
|         fn(err, body); // XXX was resp
 |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       console.info(""); |  | ||||||
|       if (err) { |  | ||||||
|         console.warn("'" + service + "' may have failed." |  | ||||||
|          + " Consider peaking at the logs either with 'journalctl -xeu telebit' or /opt/telebit/var/log/error.log"); |  | ||||||
|         console.warn(err.statusCode, err.message); |  | ||||||
|         //cb(new Error("not okay"), body);
 |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (!body) { |  | ||||||
|         console.info("👌"); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       try { |  | ||||||
|         body = JSON.parse(body); |  | ||||||
|       } catch(e) { |  | ||||||
|         // ignore
 |  | ||||||
| 
 |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if ("AWAIT_AUTH" === body.code) { |  | ||||||
|         console.info(body.message); |  | ||||||
|       } else if ("CONFIG" === body.code) { |  | ||||||
|         delete body.code; |  | ||||||
|         //console.info(TOML.stringify(body));
 |  | ||||||
|         console.info(YAML.safeDump(body)); |  | ||||||
|       } else { |  | ||||||
|         if ('http' === body.module) { |  | ||||||
|           // TODO we'll support slingshot-ing in the future
 |  | ||||||
|           if (body.local) { |  | ||||||
|             if (String(body.local) === String(parseInt(body.local, 10))) { |  | ||||||
|               console.info('> Forwarding https://' + body.remote + ' => localhost:' + body.local); |  | ||||||
|             } else { |  | ||||||
|               console.info('> Serving ' + body.local + ' as https://' + body.remote); |  | ||||||
|             } |  | ||||||
|           } else { |  | ||||||
|             console.info('> Rejecting End-to-End Encrypted HTTPS for now'); |  | ||||||
|           } |  | ||||||
|         } else if ('tcp' === body.module) { |  | ||||||
|           if (body.local) { |  | ||||||
|             console.info('> Forwarding ' + state.config.relay + ':' + body.remote + ' => localhost:' + body.local); |  | ||||||
|           } else { |  | ||||||
|             console.info('> Rejecting Legacy TCP'); |  | ||||||
|           } |  | ||||||
|         } else if ('ssh' === body.module) { |  | ||||||
|           //console.info('> Forwarding ' + state.config.relay + ' -p ' + JSON.stringify(body) + ' => localhost:' + body.local);
 |  | ||||||
|           if (body.local) { |  | ||||||
|             console.info('> Forwarding ssh+https (openssl proxy) => localhost:' + body.local); |  | ||||||
|           } else { |  | ||||||
|             console.info('> Rejecting SSH-over-HTTPS for now'); |  | ||||||
|           } |  | ||||||
|         } else if ('status' === body.module) { |  | ||||||
|           // TODO funny one this one
 |  | ||||||
|           if (body.port) { |  | ||||||
|             console.info('http://localhost:' + (body.port)); |  | ||||||
|           } |  | ||||||
|           console.info(JSON.stringify(body, null, 2)); |  | ||||||
|         } else { |  | ||||||
|           console.info(JSON.stringify(body, null, 2)); |  | ||||||
|         } |  | ||||||
|         console.info(); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function getToken(fn) { |  | ||||||
|     state.relay = state.config.relay; |  | ||||||
| 
 |  | ||||||
|     // { _otp, config: {} }
 |  | ||||||
|     common.api.token(state, { |  | ||||||
|       error: function (err/*, next*/) { |  | ||||||
|         console.error("[Error] common.api.token:"); |  | ||||||
|         console.error(err); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|     , directory: function (dir, next) { |  | ||||||
|         //console.log('[directory] Telebit Relay Discovered:');
 |  | ||||||
|         //console.log(dir);
 |  | ||||||
|         state._apiDirectory = dir; |  | ||||||
|         next(); |  | ||||||
|       } |  | ||||||
|     , tunnelUrl: function (tunnelUrl, next) { |  | ||||||
|         //console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl);
 |  | ||||||
|         state.wss = tunnelUrl; |  | ||||||
|         next(); |  | ||||||
|       } |  | ||||||
|     , requested: function (authReq, next) { |  | ||||||
|         //console.log("[requested] Pairing Requested");
 |  | ||||||
|         state.config._otp = state.config._otp = authReq.otp; |  | ||||||
| 
 |  | ||||||
|         if (!state.config.token && state._can_pair) { |  | ||||||
|           console.info(TPLS.remote.code.replace(/0000/g, state.config._otp)); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         next(); |  | ||||||
|       } |  | ||||||
|     , connect: function (pretoken, next) { |  | ||||||
|         //console.log("[connect] Enabling Pairing Locally...");
 |  | ||||||
|         state.config.pretoken = pretoken; |  | ||||||
|         state._connecting = true; |  | ||||||
| 
 |  | ||||||
|         // TODO use php-style object querification
 |  | ||||||
|         RC.request({ service: 'config', method: 'POST', data: state.config }, handleRemoteRequest('config', function (err/*, body*/) { |  | ||||||
|           if (err) { |  | ||||||
|             state._error = err; |  | ||||||
|             console.error("Error while initializing config [connect]:"); |  | ||||||
|             console.error(err); |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
|           console.info("waiting..."); |  | ||||||
|           next(); |  | ||||||
|         })); |  | ||||||
|       } |  | ||||||
|     , offer: function (token, next) { |  | ||||||
|         //console.log("[offer] Pairing Enabled by Relay");
 |  | ||||||
|         state.config.token = token; |  | ||||||
|         if (state._error) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         state._connecting = true; |  | ||||||
|         try { |  | ||||||
|           JWT.decode(token); |  | ||||||
|           //console.log(JWT.decode(token));
 |  | ||||||
|         } catch(e) { |  | ||||||
|           console.warn("[warning] could not decode token"); |  | ||||||
|         } |  | ||||||
|         RC.request({ service: 'config', method: 'POST', data: state.config }, handleRemoteRequest('config', function (err/*, body*/) { |  | ||||||
|           if (err) { |  | ||||||
|             state._error = err; |  | ||||||
|             console.error("Error while initializing config [offer]:"); |  | ||||||
|             console.error(err); |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
|           //console.log("Pairing Enabled Locally");
 |  | ||||||
|           next(); |  | ||||||
|         })); |  | ||||||
|       } |  | ||||||
|     , granted: function (_, next) { |  | ||||||
|         //console.log("[grant] Pairing complete!");
 |  | ||||||
|         next(); |  | ||||||
|       } |  | ||||||
|     , end: function () { |  | ||||||
|         RC.request({ service: 'enable', method: 'POST', data: [] }, handleRemoteRequest('enable', function (err) { |  | ||||||
|           if (err) { console.error(err); return; } |  | ||||||
|           console.info("Success"); |  | ||||||
| 
 |  | ||||||
|           // workaround for https://github.com/nodejs/node/issues/21319
 |  | ||||||
|           if (state._useTty) { |  | ||||||
|             setTimeout(function () { |  | ||||||
|               console.info("Some fun things to try first:\n"); |  | ||||||
|               console.info("    ~/telebit http ~/public"); |  | ||||||
|               console.info("    ~/telebit tcp 5050"); |  | ||||||
|               console.info("    ~/telebit ssh auto"); |  | ||||||
|               console.info(); |  | ||||||
|               console.info("Press any key to continue..."); |  | ||||||
|               console.info(); |  | ||||||
|               process.exit(0); |  | ||||||
|             }, 0.5 * 1000); |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
|           // end workaround
 |  | ||||||
| 
 |  | ||||||
|           //parseCli(state);
 |  | ||||||
|           fn(); |  | ||||||
|         })); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var bootState = {}; |  | ||||||
|   function bootstrap() { |  | ||||||
|     // Create / retrieve account (sign-in, more or less)
 |  | ||||||
|     // TODO hit directory resource /.well-known/openid-configuration -> acme_uri (?)
 |  | ||||||
|     // Occassionally rotate the key just for the sake of testing the key rotation
 |  | ||||||
|     return urequestAsync({ method: 'HEAD', url: RC.resolve('/acme/new-nonce') }).then(function (resp) { |  | ||||||
|       var nonce = resp.headers['replay-nonce']; |  | ||||||
|       var newAccountUrl = RC.resolve('/acme/new-acct'); |  | ||||||
|       return keypairs.signJws({ |  | ||||||
|         jwk: state.key |  | ||||||
|       , protected: { |  | ||||||
|           // alg will be filled out automatically
 |  | ||||||
|           jwk: state.pub |  | ||||||
|         , nonce: nonce |  | ||||||
|         , url: newAccountUrl |  | ||||||
|         } |  | ||||||
|       , payload: JSON.stringify({ |  | ||||||
|           // We can auto-agree here because the client is the user agent of the primary user
 |  | ||||||
|           termsOfServiceAgreed: true |  | ||||||
|         , contact: [] // I don't think we have email yet...
 |  | ||||||
|         //, externalAccountBinding: null
 |  | ||||||
|         }) |  | ||||||
|       }).then(function (jws) { |  | ||||||
|         return urequestAsync({ |  | ||||||
|           url: newAccountUrl |  | ||||||
|         , method: 'POST' |  | ||||||
|         , json: jws // TODO default to post when body is present
 |  | ||||||
|         , headers: { "Content-Type": 'application/jose+json' } |  | ||||||
|         }).then(function (resp) { |  | ||||||
|           console.log(newAccountUrl, 'resp.body:'); |  | ||||||
|           console.log(resp.body); |  | ||||||
|           if (!resp.body || 'valid' !== resp.body.status) { |  | ||||||
|             throw new Error("did not successfully create or restore account"); |  | ||||||
|           } |  | ||||||
|           return RC.requestAsync({ service: 'config', method: 'GET' }).catch(function (err) { |  | ||||||
|             console.error(err.stack); |  | ||||||
|             process.exit(27); |  | ||||||
|           }).then(handleConfig); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }).catch(RC.createErrorHandler(bootstrap, bootState, function (err) { |  | ||||||
|       console.error(err); |  | ||||||
|       process.exit(17); |  | ||||||
|     })); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   bootstrap(); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var parsers = { | var parsers = { | ||||||
| @ -807,41 +807,6 @@ var parsers = { | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| var keystore = require('../lib/keystore.js').create(state); | fs.readFile(confpath, 'utf8', parseConfig); | ||||||
| state.keystore = keystore; |  | ||||||
| state.keystoreSecure = !keystore.insecure; |  | ||||||
| keystore.all().then(function (list) { |  | ||||||
|   var keyext = '.key.jwk.json'; |  | ||||||
|   var key; |  | ||||||
|   // TODO create map by account and index into that map to get the master key
 |  | ||||||
|   // and sort keys in the process
 |  | ||||||
|   list.some(function (el) { |  | ||||||
|     if (keyext === el.account.slice(-keyext.length) |  | ||||||
|       && el.password.kty && el.password.kid) { |  | ||||||
|       key = el.password; |  | ||||||
|       return true; |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   if (key) { |  | ||||||
|     state.key = key; |  | ||||||
|     state.pub = keypairs.neuter({ jwk: key }); |  | ||||||
|     fs.readFile(confpath, 'utf8', parseConfig); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return keypairs.generate().then(function (pair) { |  | ||||||
|     var jwk = pair.private; |  | ||||||
|     return keypairs.thumbprint({ jwk: jwk }).then(function (kid) { |  | ||||||
|       jwk.kid = kid; |  | ||||||
|       return keystore.set(kid + keyext, jwk).then(function () { |  | ||||||
|         var size = (jwk.crv || Buffer.from(jwk.n, 'base64').byteLength * 8); |  | ||||||
|         console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid); |  | ||||||
|         state.key = jwk; |  | ||||||
|         fs.readFile(confpath, 'utf8', parseConfig); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| }()); | }()); | ||||||
|  | |||||||
| @ -21,7 +21,6 @@ if ('rsync' === process.argv[2]) { | |||||||
|   require('sclient/bin/sclient.js'); |   require('sclient/bin/sclient.js'); | ||||||
|   return; |   return; | ||||||
| } | } | ||||||
| // handle ssh client rather than ssh https tunnel
 |  | ||||||
| if ('ssh' === process.argv[2] && /[\w-]+\.[a-z]{2,}/i.test(process.argv[3])) { | if ('ssh' === process.argv[2] && /[\w-]+\.[a-z]{2,}/i.test(process.argv[3])) { | ||||||
|   process.argv.splice(1,1,'sclient'); |   process.argv.splice(1,1,'sclient'); | ||||||
|   process.argv.splice(2,1,'ssh'); |   process.argv.splice(2,1,'ssh'); | ||||||
|  | |||||||
							
								
								
									
										1181
									
								
								bin/telebitd.js
									
									
									
									
									
								
							
							
						
						
									
										1181
									
								
								bin/telebitd.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,60 +0,0 @@ | |||||||
| <!DOCTYPE html> |  | ||||||
| <html> |  | ||||||
| <head> |  | ||||||
|   <title>Telebit Documentation</title> |  | ||||||
| </head> |  | ||||||
| <body> |  | ||||||
|   <div class="v-app"> |  | ||||||
|     <h1>Telebit (Remote) Documentation</h1> |  | ||||||
| 
 |  | ||||||
|     <section> |  | ||||||
|       <h2>GET /api/config</h2> |  | ||||||
|       <pre><code>{{ config }}</code></pre> |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|     <section> |  | ||||||
|       <h2>GET /api/status</h2> |  | ||||||
|       <pre><code>{{ status }}</code></pre> |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|     <section> |  | ||||||
|       <h2>POST /api/init</h2> |  | ||||||
|       <form v-on:submit.stop.prevent="initialize"> |  | ||||||
| 
 |  | ||||||
|         <label for="-email">Email:</label> |  | ||||||
|         <input id="-email" v-model="init.email" type="text" placeholder="john@example.com"> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         <label for="-teletos"><input id="-teletos" v-model="init.teletos" type="checkbox"> |  | ||||||
|           Accept Telebit Terms of Service</label> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         <label for="-letos"><input id="-letos" v-model="init.letos" type="checkbox"> |  | ||||||
|           Accept Let's Encrypt Terms of Service</label> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|       </form> |  | ||||||
|       <pre><code>{{ init }}</code></pre> |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|     <section> |  | ||||||
|       <h2>POST /api/http</h2> |  | ||||||
|       <pre><code>{{ http }}</code></pre> |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|     <section> |  | ||||||
|       <h2>POST /api/tcp</h2> |  | ||||||
|       <pre><code>{{ tcp }}</code></pre> |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|     <section> |  | ||||||
|       <h2>POST /api/ssh</h2> |  | ||||||
|       <pre><code>{{ ssh }}</code></pre> |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|   </div> |  | ||||||
| 
 |  | ||||||
|   <script src="/js/vue.js"></script> |  | ||||||
|   <script src="/js/app.js"></script> |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
| @ -1,234 +0,0 @@ | |||||||
| <!DOCTYPE html> |  | ||||||
| <html> |  | ||||||
| <head> |  | ||||||
|   <title>Telebit Setup</title> |  | ||||||
| </head> |  | ||||||
| <body> |  | ||||||
|   <script>document.body.hidden = true;</script> |  | ||||||
| 
 |  | ||||||
|   <div class="v-app"> |  | ||||||
|     <h1>Telebit (Remote) Setup v{{ config.version }}</h1> |  | ||||||
| 
 |  | ||||||
|     <section v-if="views.flash.error"> |  | ||||||
|       {{ views.flash.error }} |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|     <section v-if="views.section.loading"> |  | ||||||
|       Loading... |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|     <section v-if="views.section.setup"> |  | ||||||
|       <h2>Create Account</h2> |  | ||||||
|       <form v-on:submit.stop.prevent="initialize"> |  | ||||||
| 
 |  | ||||||
|         <label for="-email">Email:</label> |  | ||||||
|         <input id="-email" v-model="init.email" type="text" placeholder="john@example.com" required> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         <label for="-teletos"><input id="-teletos" v-model="init.teletos" type="checkbox" required> |  | ||||||
|           Accept Telebit Terms of Service</label> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         <label for="-letos"><input id="-letos" v-model="init.letos" type="checkbox" required> |  | ||||||
|           Accept Let's Encrypt Terms of Service</label> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         <label for="-notifications">Notification Preferences</label> |  | ||||||
|         <select id="-notifications" v-model="init.notifications"> |  | ||||||
|           <option value="newsletter">Occassional Newsletter</option> |  | ||||||
|           <option value="important" default><strong>Important Messages Only</strong></option> |  | ||||||
|           <option value="required">Required Only</option> |  | ||||||
|         </select> |  | ||||||
| 
 |  | ||||||
|         <small> |  | ||||||
|         <p v-if="'newsletter' == init.notifications"> |  | ||||||
|         You'll receive a friendly note now and then in addition to the important updates. |  | ||||||
|         </p> |  | ||||||
|         <p v-if="'important' == init.notifications"> |  | ||||||
|         You'll only receive updates that we believe will be of the most value to you, and the required updates. |  | ||||||
|         </p> |  | ||||||
|         <p v-if="'required' == init.notifications"> |  | ||||||
|         You'll only receive security updates, transactional and account-related messages, and legal notices. |  | ||||||
|         </p> |  | ||||||
|         </small> |  | ||||||
| 
 |  | ||||||
|         <details><summary><small>Advanced</small></summary> |  | ||||||
| 
 |  | ||||||
|         <label for="-relay">Relay:</label> |  | ||||||
|           <input id="-relay" v-model="init.relay" type="text" placeholder="telebit.cloud"> |  | ||||||
|         <br> |  | ||||||
|         <button type="button" v-on:click="defaultRelay">Use Default</button> |  | ||||||
|         <button type="button" v-on:click="betaRelay">Use Beta</button> |  | ||||||
|         <br> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         <label for="-acme-server">ACME (Let's Encrypt) Server:</label> |  | ||||||
|           <input id="-acme-server" v-model="init.acmeServer" type="text" placeholder="https://acme-v02.api.letsencrypt.org/directory"> |  | ||||||
|         <br> |  | ||||||
|         <button type="button" v-on:click="productionAcme">Use Production</button> |  | ||||||
|         <button type="button" v-on:click="stagingAcme">Use Staging</button> |  | ||||||
|         <br> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         </details> |  | ||||||
| 
 |  | ||||||
|         <button type="submit">Accept & Continue</button> |  | ||||||
| 
 |  | ||||||
|       </form> |  | ||||||
|       <pre><code>{{ init }}</code></pre> |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|     <section v-if="views.section.advanced"> |  | ||||||
|       <h2>Advanced Setup for {{ init.relay }}</h2> |  | ||||||
|       <form v-on:submit.stop.prevent="advance"> |  | ||||||
| 
 |  | ||||||
|         <strong><label for="-secret">Relay Shared Secret:</label></strong> |  | ||||||
|         <input id="-secret" v-model="init.secret" type="text" placeholder="ex: xxxxxxxxxxxx"> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         <strong><label for="-domains">Domains:</label></strong> |  | ||||||
|         <br> |  | ||||||
|         <small>(comma separated list of domains to use for http, tls, https, etc)</small> |  | ||||||
|         <br> |  | ||||||
|         <input id="-domains" v-model="init.domains" type="text" placeholder="ex: whatever.com, example.com"> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         <strong><label for="-ports">TCP Ports:</label></strong> |  | ||||||
|         <br> |  | ||||||
|         <small>(comman separated list of ports, excluding 80 and 443, typically port over 1024)</small> |  | ||||||
|         <br> |  | ||||||
|         <input id="-ports" v-model="init.ports" type="text" placeholder="ex: 5050, 3000, 8080"> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         <label for="-telemetry"><input id="-telemetry" v-model="init.telemetry" type="checkbox"> |  | ||||||
|           Contribute to Telebit by sharing telemetry</label> |  | ||||||
|         <br> |  | ||||||
| 
 |  | ||||||
|         <button type="submit">Finish</button> |  | ||||||
| 
 |  | ||||||
|       </form> |  | ||||||
|       <pre><code>{{ init }}</code></pre> |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|     <section v-if="views.section.otp"> |  | ||||||
|       <pre><code><h2>{{ init.otp }}</h2></code></pre> |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|     <section v-if="views.section.status"> |  | ||||||
|       http://localhost:{{ status.port }} |  | ||||||
|       <br> |  | ||||||
|       <br> |  | ||||||
| 
 |  | ||||||
|       <section v-if="views.section.status_chooser"> |  | ||||||
|         <button v-on:click.prevent.stop="changeState('status/share')">Share Files & Folders</button> |  | ||||||
|         <button v-on:click.prevent.stop="changeState('status/host')">Host a Website or Webapp</button> |  | ||||||
|         <button v-on:click.prevent.stop="changeState('status/access')">Remote Access via SSH</button> |  | ||||||
|       </section> |  | ||||||
| 
 |  | ||||||
|       <section v-if="views.section.status_access"> |  | ||||||
|         SSH: |  | ||||||
|         <span v-if="status.ssh">{{ status.ssh }} |  | ||||||
|           <button v-on:click="ssh(-1)">Disable SSH</button></span> |  | ||||||
|         <span v-if="!status.ssh"><input type="text" v-model="state.ssh" placeholder="22"> |  | ||||||
|           <button v-on:click="ssh(state.ssh)">Enable SSH</button></span> |  | ||||||
|         <br> |  | ||||||
|         <br> |  | ||||||
|         <div v-if="state.ssh_active">SSH is currently running</div> |  | ||||||
|         <div v-if="!state.ssh_active">SSH is not currently running</div> |  | ||||||
|         <br> |  | ||||||
|         <div v-if="state.ssh_insecure">Password Authentication is NOT disabled. |  | ||||||
|           Please consider updating your <code>sshd_config</code> and restarting ssh. |  | ||||||
|           <pre><code>{{ status }}</code></pre> |  | ||||||
|         </div> |  | ||||||
|         <div v-if="!state.ssh_insecure">Key-Only Authentication is enabled :)</div> |  | ||||||
|         <br> |  | ||||||
|         <div class="alert alert-info"> |  | ||||||
|           <strong>Important:</strong> Accessing this device with other SSH clients: |  | ||||||
|           <br> |  | ||||||
|           In order to use your other ssh clients with telebit you will need to put them into |  | ||||||
|           <strong>ssh+https mode</strong>. |  | ||||||
| 
 |  | ||||||
|           We recommend downloading <code><a href="https://telebit.cloud/sclient/" target="_blank">sclient</a></code> |  | ||||||
|           to do so, because it makes it as simple as adding <code>-o ProxyCommand="sclient %h"</code> to your |  | ||||||
|           ssh command to enable ssh+https: |  | ||||||
|           <pre><code>ssh -o ProxyCommand="sclient %h" {{ newHttp.name }}</code></pre> |  | ||||||
|           <br> |  | ||||||
|           However, most clients can also use <code>openssl s_client</code>, which does the same thing, but is |  | ||||||
|           more difficult to remember: |  | ||||||
|           <pre><code>proxy_cmd='openssl s_client -connect %h:443 -servername %h -quiet' |  | ||||||
| ssh -o ProxyCommand="$proxy_cmd" hot-skunk-45.telebit.io</code></pre> |  | ||||||
|         </div> |  | ||||||
|       </section> |  | ||||||
| 
 |  | ||||||
|       <section v-if="views.section.status_share"> |  | ||||||
|         Path Hosting: |  | ||||||
|         <ul> |  | ||||||
|           <li v-for="domain in status.pathHosting"> |  | ||||||
|             <form v-on:submit.prevent.stop="changePathHost(domain, domain.path)"> |  | ||||||
|             {{ domain.name }} |  | ||||||
|             <input type="text" v-model="domain.path" v-bind:placeholder="domain.handler"> |  | ||||||
|             <button type="submit" |  | ||||||
|               v-if="domain.handler == domain.path">Save</button> |  | ||||||
|             <button type="button" v-on:click="deletePathHost(domain)">X</button> |  | ||||||
|             </form> |  | ||||||
|           </li> |  | ||||||
|         </ul> |  | ||||||
|         <form v-on:submit.prevent.stop="createShare(newHttp.sub, newHttp.name, newHttp.handler)"> |  | ||||||
|           <input v-model="newHttp.sub" type="text" placeholder="subdomain (ex: pub)"> |  | ||||||
|           <select v-model="newHttp.name"> |  | ||||||
|             <option v-for="w in status.wildDomains" v-bind:value="w.name">{{ w.name }}</option> |  | ||||||
|           </select> |  | ||||||
|           <input v-model="newHttp.handler" type="text" placeholder="path (ex: ~/Public)" required> |  | ||||||
|           <button>Add</button> |  | ||||||
|         </form> |  | ||||||
|         <br> |  | ||||||
|       </section> |  | ||||||
| 
 |  | ||||||
|       <section v-if="views.section.status_host"> |  | ||||||
|         Port Forwarding: |  | ||||||
|         <ul> |  | ||||||
|           <li v-for="domain in status.portForwards"> |  | ||||||
|             <form v-on:submit.prevent.stop="changePortForward(domain, domain._port)"> |  | ||||||
|             {{ domain.name }} |  | ||||||
|             <input type="text" v-model="domain._port" v-bind:placeholder="domain.handler"> |  | ||||||
|             <button type="submit" |  | ||||||
|               v-if="domain.handler == domain._port">Save</button> |  | ||||||
|             <button type="button" v-on:click="deletePortForward(domain)">X</button> |  | ||||||
|             </form> |  | ||||||
|           </li> |  | ||||||
|         </ul> |  | ||||||
|         <form v-on:submit="createHost(newHttp.sub, newHttp.name, newHttp.handler)"> |  | ||||||
|           <input v-model="newHttp.sub" type="text" placeholder="subdomain (ex: api)"> |  | ||||||
|           <select v-model="newHttp.name"> |  | ||||||
|             <option v-for="w in status.wildDomains" v-bind:value="w.name">{{ w.name }}</option> |  | ||||||
|           </select> |  | ||||||
|           <input v-model="newHttp.handler" type="number" placeholder="port (ex: 3000)" required> |  | ||||||
|           <button>Add</button> |  | ||||||
|         </form> |  | ||||||
|       </section> |  | ||||||
| 
 |  | ||||||
|       <br> |  | ||||||
|       Uptime: {{ statusUptime }} |  | ||||||
|       <br> |  | ||||||
|       Runtime: {{ statusRuntime }} |  | ||||||
|       <br> |  | ||||||
|       Reconnects: {{ status.reconnects }} |  | ||||||
| 
 |  | ||||||
|       <details><summary><small>Advanced</small></summary> |  | ||||||
|       <button v-if="!status.enabled" v-on:click="enable">Enable Traffic</button> |  | ||||||
|       <button v-if="status.enabled" v-on:click="disable">Disable Traffic</button> |  | ||||||
|       <br> |  | ||||||
|       <br> |  | ||||||
| 
 |  | ||||||
|       <pre><code>{{ status }}</code></pre> |  | ||||||
|       </details> |  | ||||||
|     </section> |  | ||||||
| 
 |  | ||||||
|   </div> |  | ||||||
| 
 |  | ||||||
|   <script src="/js/vue.js"></script> |  | ||||||
|   <script src="/js/telebit.js"></script> |  | ||||||
|   <script src="/js/telebit-token.js"></script> |  | ||||||
|   <script src="/js/app.js"></script> |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
| @ -1,513 +0,0 @@ | |||||||
| ;(function () { |  | ||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var Vue = window.Vue; |  | ||||||
| var Telebit = window.TELEBIT; |  | ||||||
| var api = {}; |  | ||||||
| 
 |  | ||||||
| /* |  | ||||||
| function safeFetch(url, opts) { |  | ||||||
|   var controller = new AbortController(); |  | ||||||
|   var tok = setTimeout(function () { |  | ||||||
|     controller.abort(); |  | ||||||
|   }, 4000); |  | ||||||
|   if (!opts) { |  | ||||||
|     opts = {}; |  | ||||||
|   } |  | ||||||
|   opts.signal = controller.signal; |  | ||||||
|   return window.fetch(url, opts).finally(function () { |  | ||||||
|     clearTimeout(tok); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| api.config = function apiConfig() { |  | ||||||
|   return Telebit.reqLocalAsync({ |  | ||||||
|     url: "/api/config" |  | ||||||
|   , method: "GET" |  | ||||||
|   }).then(function (resp) { |  | ||||||
|     var json = resp.body; |  | ||||||
|     appData.config = json; |  | ||||||
|     return json; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| api.status = function apiStatus() { |  | ||||||
|   return Telebit.reqLocalAsync({ url: "/api/status", method: "GET" }).then(function (resp) { |  | ||||||
|     var json = resp.body; |  | ||||||
|     return json; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| api.http = function apiHttp(o) { |  | ||||||
|   var opts = { |  | ||||||
|     url: "/api/http" |  | ||||||
|   , method: "POST" |  | ||||||
|   , headers: { 'Content-Type': 'application/json' } |  | ||||||
|   , json: { name: o.name, handler: o.handler, indexes: o.indexes } |  | ||||||
|   }; |  | ||||||
|   return Telebit.reqLocalAsync(opts).then(function (resp) { |  | ||||||
|     var json = resp.body; |  | ||||||
|     appData.initResult = json; |  | ||||||
|     return json; |  | ||||||
|   }).catch(function (err) { |  | ||||||
|     window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2))); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| api.ssh = function apiSsh(port) { |  | ||||||
|   var opts = { |  | ||||||
|     url: "/api/ssh" |  | ||||||
|   , method: "POST" |  | ||||||
|   , headers: { 'Content-Type': 'application/json' } |  | ||||||
|   , json: { port: port } |  | ||||||
|   }; |  | ||||||
|   return Telebit.reqLocalAsync(opts).then(function (resp) { |  | ||||||
|     var json = resp.body; |  | ||||||
|     appData.initResult = json; |  | ||||||
|     return json; |  | ||||||
|   }).catch(function (err) { |  | ||||||
|     window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2))); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| api.enable = function apiEnable() { |  | ||||||
|   var opts = { |  | ||||||
|     url: "/api/enable" |  | ||||||
|   , method: "POST" |  | ||||||
|   //, headers: { 'Content-Type': 'application/json' }
 |  | ||||||
|   }; |  | ||||||
|   return Telebit.reqLocalAsync(opts).then(function (resp) { |  | ||||||
|     var json = resp.body; |  | ||||||
|     console.log('enable', json); |  | ||||||
|     return json; |  | ||||||
|   }).catch(function (err) { |  | ||||||
|     window.alert("Error: [enable] " + (err.message || JSON.stringify(err, null, 2))); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| api.disable = function apiDisable() { |  | ||||||
|   var opts = { |  | ||||||
|     url: "/api/disable" |  | ||||||
|   , method: "POST" |  | ||||||
|   //, headers: { 'Content-Type': 'application/json' }
 |  | ||||||
|   }; |  | ||||||
|   return Telebit.reqLocalAsync(opts).then(function (resp) { |  | ||||||
|     var json = resp.body; |  | ||||||
|     console.log('disable', json); |  | ||||||
|     return json; |  | ||||||
|   }).catch(function (err) { |  | ||||||
|     window.alert("Error: [disable] " + (err.message || JSON.stringify(err, null, 2))); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function showOtp(otp, pollUrl) { |  | ||||||
|   localStorage.setItem('poll_url', pollUrl); |  | ||||||
|   telebitState.pollUrl = pollUrl; |  | ||||||
|   appData.init.otp = otp; |  | ||||||
|   changeState('otp'); |  | ||||||
| } |  | ||||||
| function doConfigure() { |  | ||||||
|   if (telebitState.dir.pair_request) { |  | ||||||
|     telebitState._can_pair = true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   //
 |  | ||||||
|   // Read config from form
 |  | ||||||
|   //
 |  | ||||||
| 
 |  | ||||||
|   // Create Empty Config, If Necessary
 |  | ||||||
|   if (!telebitState.config) { telebitState.config = {}; } |  | ||||||
|   if (!telebitState.config.greenlock) { telebitState.config.greenlock = {}; } |  | ||||||
| 
 |  | ||||||
|   // Populate Config
 |  | ||||||
|   if (appData.init.teletos && appData.init.letos) { telebitState.config.agreeTos = true; } |  | ||||||
|   if (appData.init.relay) { telebitState.config.relay = appData.init.relay; } |  | ||||||
|   if (appData.init.email) { telebitState.config.email = appData.init.email; } |  | ||||||
|   if ('undefined' !== typeof appData.init.letos) { telebitState.config.greenlock.agree = appData.init.letos; } |  | ||||||
|   if ('newsletter' === appData.init.notifications) { |  | ||||||
|     telebitState.config.newsletter = true; telebitState.config.communityMember = true; |  | ||||||
|   } |  | ||||||
|   if ('important' === appData.init.notifications) { telebitState.config.communityMember = true; } |  | ||||||
|   if (appData.init.acmeVersion) { telebitState.config.greenlock.version = appData.init.acmeVersion; } |  | ||||||
|   if (appData.init.acmeServer) { telebitState.config.greenlock.server = appData.init.acmeServer; } |  | ||||||
| 
 |  | ||||||
|   // Temporary State
 |  | ||||||
|   telebitState._otp = Telebit.otp(); |  | ||||||
|   appData.init.otp = telebitState._otp; |  | ||||||
| 
 |  | ||||||
|   return Telebit.authorize(telebitState, showOtp).then(function () { |  | ||||||
|     return changeState('status'); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // TODO test for internet connectivity (and telebit connectivity)
 |  | ||||||
| var DEFAULT_RELAY = 'telebit.cloud'; |  | ||||||
| var BETA_RELAY = 'telebit.ppl.family'; |  | ||||||
| var TELEBIT_RELAYS = [ |  | ||||||
|   DEFAULT_RELAY |  | ||||||
| , BETA_RELAY |  | ||||||
| ]; |  | ||||||
| var PRODUCTION_ACME = 'https://acme-v02.api.letsencrypt.org/directory'; |  | ||||||
| var STAGING_ACME = 'https://acme-staging-v02.api.letsencrypt.org/directory'; |  | ||||||
| var appData = { |  | ||||||
|   config: {} |  | ||||||
| , status: {} |  | ||||||
| , init: { |  | ||||||
|     teletos: true |  | ||||||
|   , letos: true |  | ||||||
|   , notifications: "important" |  | ||||||
|   , relay: DEFAULT_RELAY |  | ||||||
|   , telemetry: true |  | ||||||
|   , acmeServer: PRODUCTION_ACME |  | ||||||
|   } |  | ||||||
| , state: {} |  | ||||||
| , views: { |  | ||||||
|     flash: { |  | ||||||
|       error: "" |  | ||||||
|     } |  | ||||||
|   , section: { |  | ||||||
|       loading: true |  | ||||||
|     , setup: false |  | ||||||
|     , advanced: false |  | ||||||
|     , otp: false |  | ||||||
|     , status: false |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| , newHttp: {} |  | ||||||
| }; |  | ||||||
| var telebitState = {}; |  | ||||||
| var appMethods = { |  | ||||||
|   initialize: function () { |  | ||||||
|     console.log("call initialize"); |  | ||||||
|     if (!appData.init.relay) { |  | ||||||
|       appData.init.relay = DEFAULT_RELAY; |  | ||||||
|     } |  | ||||||
|     appData.init.relay = appData.init.relay.toLowerCase(); |  | ||||||
|     telebitState = { relay: appData.init.relay }; |  | ||||||
| 
 |  | ||||||
|     return Telebit.api.directory(telebitState).then(function (dir) { |  | ||||||
|       if (!dir.api_host) { |  | ||||||
|         window.alert("Error: '" + telebitState.relay + "' does not appear to be a valid telebit service"); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       telebitState.dir = dir; |  | ||||||
| 
 |  | ||||||
|       // If it's one of the well-known relays
 |  | ||||||
|       if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) { |  | ||||||
|         return doConfigure(); |  | ||||||
|       } else { |  | ||||||
|         changeState('advanced'); |  | ||||||
|       } |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       console.error(err); |  | ||||||
|       window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2))); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| , advance: function () { |  | ||||||
|     return doConfigure(); |  | ||||||
|   } |  | ||||||
| , productionAcme: function () { |  | ||||||
|     console.log("prod acme:"); |  | ||||||
|     appData.init.acmeServer = PRODUCTION_ACME; |  | ||||||
|     console.log(appData.init.acmeServer); |  | ||||||
|   } |  | ||||||
| , stagingAcme: function () { |  | ||||||
|     console.log("staging acme:"); |  | ||||||
|     appData.init.acmeServer = STAGING_ACME; |  | ||||||
|     console.log(appData.init.acmeServer); |  | ||||||
|   } |  | ||||||
| , defaultRelay: function () { |  | ||||||
|     appData.init.relay = DEFAULT_RELAY; |  | ||||||
|   } |  | ||||||
| , betaRelay: function () { |  | ||||||
|     appData.init.relay = BETA_RELAY; |  | ||||||
|   } |  | ||||||
| , enable: function () { |  | ||||||
|     api.enable(); |  | ||||||
|   } |  | ||||||
| , disable: function () { |  | ||||||
|     api.disable(); |  | ||||||
|   } |  | ||||||
| , ssh: function (port) { |  | ||||||
|     // -1 to disable
 |  | ||||||
|     // 0 is auto (22)
 |  | ||||||
|     // 1-65536
 |  | ||||||
|     api.ssh(port || 22); |  | ||||||
|   } |  | ||||||
| , createShare: function (sub, domain, handler) { |  | ||||||
|     if (sub) { |  | ||||||
|       domain = sub + '.' + domain; |  | ||||||
|     } |  | ||||||
|     api.http({ name: domain, handler: handler, indexes: true }); |  | ||||||
|     appData.newHttp = {}; |  | ||||||
|   } |  | ||||||
| , createHost: function (sub, domain, handler) { |  | ||||||
|     if (sub) { |  | ||||||
|       domain = sub + '.' + domain; |  | ||||||
|     } |  | ||||||
|     api.http({ name: domain, handler: handler, 'x-forwarded-for': name }); |  | ||||||
|     appData.newHttp = {}; |  | ||||||
|   } |  | ||||||
| , changePortForward: function (domain, port) { |  | ||||||
|     api.http({ name: domain.name, handler: port }); |  | ||||||
|   } |  | ||||||
| , deletePortForward: function (domain) { |  | ||||||
|     api.http({ name: domain.name, handler: 'none' }); |  | ||||||
|   } |  | ||||||
| , changePathHost: function (domain, path) { |  | ||||||
|     api.http({ name: domain.name, handler: path }); |  | ||||||
|   } |  | ||||||
| , deletePathHost: function (domain) { |  | ||||||
|     api.http({ name: domain.name, handler: 'none' }); |  | ||||||
|   } |  | ||||||
| , changeState: changeState |  | ||||||
| }; |  | ||||||
| var appStates = { |  | ||||||
|   setup: function () { |  | ||||||
|     appData.views.section = { setup: true }; |  | ||||||
|   } |  | ||||||
| , advanced: function () { |  | ||||||
|     appData.views.section = { advanced: true }; |  | ||||||
|   } |  | ||||||
| , otp: function () { |  | ||||||
|     appData.views.section = { otp: true }; |  | ||||||
|   } |  | ||||||
| , status: function () { |  | ||||||
|     function exitState() { |  | ||||||
|       clearInterval(tok); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var tok = setInterval(updateStatus, 2000); |  | ||||||
| 
 |  | ||||||
|     return updateStatus().then(function () { |  | ||||||
|       appData.views.section = { status: true, status_chooser: true }; |  | ||||||
|       return exitState; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| appStates.status.share = function () { |  | ||||||
|   function exitState() { |  | ||||||
|     clearInterval(tok); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var tok = setInterval(updateStatus, 2000); |  | ||||||
| 
 |  | ||||||
|   appData.views.section = { status: true, status_share: true }; |  | ||||||
|   return updateStatus().then(function () { |  | ||||||
|     return exitState; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| appStates.status.host = function () { |  | ||||||
|   function exitState() { |  | ||||||
|     clearInterval(tok); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var tok = setInterval(updateStatus, 2000); |  | ||||||
| 
 |  | ||||||
|   appData.views.section = { status: true, status_host: true }; |  | ||||||
|   return updateStatus().then(function () { |  | ||||||
|     return exitState; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| appStates.status.access = function () { |  | ||||||
|   function exitState() { |  | ||||||
|     clearInterval(tok); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var tok = setInterval(updateStatus, 2000); |  | ||||||
| 
 |  | ||||||
|   appData.views.section = { status: true, status_access: true }; |  | ||||||
|   return updateStatus().then(function () { |  | ||||||
|     return exitState; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function updateStatus() { |  | ||||||
|   return api.status().then(function (status) { |  | ||||||
|     if (status.error) { |  | ||||||
|       appData.views.flash.error = status.error.message || JSON.stringify(status.error, null, 2); |  | ||||||
|     } |  | ||||||
|     var wilddomains = []; |  | ||||||
|     var rootdomains = []; |  | ||||||
|     var subdomains = []; |  | ||||||
|     var directories = []; |  | ||||||
|     var portforwards = []; |  | ||||||
|     var free = []; |  | ||||||
|     appData.status = status; |  | ||||||
|     if ('maybe' === status.ssh_requests_password) { |  | ||||||
|       appData.status.ssh_active = false; |  | ||||||
|     } else { |  | ||||||
|       appData.status.ssh_active = true; |  | ||||||
|       if ('yes' === status.ssh_requests_password) { |  | ||||||
|         appData.status.ssh_insecure = true; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     if ('yes' === status.ssh_password_authentication) { |  | ||||||
|       appData.status.ssh_insecure = true; |  | ||||||
|     } |  | ||||||
|     if ('yes' === status.ssh_permit_root_login) { |  | ||||||
|       appData.status.ssh_insecure = true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // only update what's changed
 |  | ||||||
|     if (appData.state.ssh !== appData.status.ssh) { |  | ||||||
|       appData.state.ssh = appData.status.ssh; |  | ||||||
|     } |  | ||||||
|     if (appData.state.ssh_insecure !== appData.status.ssh_insecure) { |  | ||||||
|       appData.state.ssh_insecure = appData.status.ssh_insecure; |  | ||||||
|     } |  | ||||||
|     if (appData.state.ssh_active !== appData.status.ssh_active) { |  | ||||||
|       appData.state.ssh_active = appData.status.ssh_active; |  | ||||||
|     } |  | ||||||
|     Object.keys(appData.status.servernames).forEach(function (k) { |  | ||||||
|       var s = appData.status.servernames[k]; |  | ||||||
|       s.name = k; |  | ||||||
|       if (s.wildcard) { wilddomains.push(s); } |  | ||||||
|       if (!s.sub && !s.wildcard) { rootdomains.push(s); } |  | ||||||
|       if (s.sub) { subdomains.push(s); } |  | ||||||
|       if (s.handler) { |  | ||||||
|         if (s.handler.toString() === parseInt(s.handler, 10).toString()) { |  | ||||||
|           s._port = s.handler; |  | ||||||
|           portforwards.push(s); |  | ||||||
|         } else { |  | ||||||
|           s.path = s.handler; |  | ||||||
|           directories.push(s); |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         free.push(s); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     appData.status.portForwards = portforwards; |  | ||||||
|     appData.status.pathHosting = directories; |  | ||||||
|     appData.status.wildDomains = wilddomains; |  | ||||||
|     appData.newHttp.name = (appData.status.wildDomains[0] || {}).name; |  | ||||||
|     appData.state.ssh = (appData.status.ssh > 0) && appData.status.ssh || undefined; |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function changeState(newstate) { |  | ||||||
|   var newhash = '#/' + newstate + '/'; |  | ||||||
|   if (location.hash === newhash) { |  | ||||||
|     if (!telebitState.firstState) { |  | ||||||
|       telebitState.firstState = true; |  | ||||||
|       setState(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   location.hash = newhash; |  | ||||||
| } |  | ||||||
| /*globals Promise*/ |  | ||||||
| window.addEventListener('hashchange', setState, false); |  | ||||||
| function setState(/*ev*/) { |  | ||||||
|   //ev.oldURL
 |  | ||||||
|   //ev.newURL
 |  | ||||||
|   if (appData.exit) { |  | ||||||
|     console.log('previous state exiting'); |  | ||||||
|     appData.exit.then(function (exit) { |  | ||||||
|       if ('function' === typeof exit) { |  | ||||||
|         exit(); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   var parts = location.hash.substr(1).replace(/^\//, '').replace(/\/$/, '').split('/').filter(Boolean); |  | ||||||
|   var fn = appStates; |  | ||||||
|   parts.forEach(function (s) { |  | ||||||
|     console.log("state:", s); |  | ||||||
|     fn = fn[s]; |  | ||||||
|   }); |  | ||||||
|   appData.exit = Promise.resolve(fn()); |  | ||||||
|   //appMethods.states[newstate]();
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function msToHumanReadable(ms) { |  | ||||||
|   var uptime = ms; |  | ||||||
|   var uptimed = uptime / 1000; |  | ||||||
|   var minute = 60; |  | ||||||
|   var hour = 60 * minute; |  | ||||||
|   var day = 24 * hour; |  | ||||||
|   var days = 0; |  | ||||||
|   var times = []; |  | ||||||
|   while (uptimed > day) { |  | ||||||
|     uptimed -= day; |  | ||||||
|     days += 1; |  | ||||||
|   } |  | ||||||
|   times.push(days + " days "); |  | ||||||
|   var hours = 0; |  | ||||||
|   while (uptimed > hour) { |  | ||||||
|     uptimed -= hour; |  | ||||||
|     hours += 1; |  | ||||||
|   } |  | ||||||
|   times.push(hours.toString().padStart(2, "0") + " h "); |  | ||||||
|   var minutes = 0; |  | ||||||
|   while (uptimed > minute) { |  | ||||||
|     uptimed -= minute; |  | ||||||
|     minutes += 1; |  | ||||||
|   } |  | ||||||
|   times.push(minutes.toString().padStart(2, "0") + " m "); |  | ||||||
|   var seconds = Math.round(uptimed); |  | ||||||
|   times.push(seconds.toString().padStart(2, "0") + " s "); |  | ||||||
|   return times.join(''); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| new Vue({ |  | ||||||
|   el: ".v-app" |  | ||||||
| , data: appData |  | ||||||
| , computed: { |  | ||||||
|     statusProctime: function () { |  | ||||||
|       return msToHumanReadable(this.status.proctime); |  | ||||||
|     } |  | ||||||
|   , statusRuntime: function () { |  | ||||||
|       return msToHumanReadable(this.status.runtime); |  | ||||||
|     } |  | ||||||
|   , statusUptime: function () { |  | ||||||
|       return msToHumanReadable(this.status.uptime); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| , methods: appMethods |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| api.config().then(function (config) { |  | ||||||
|   telebitState.config = config; |  | ||||||
|   if (config.greenlock) { |  | ||||||
|     appData.init.acmeServer = config.greenlock.server; |  | ||||||
|   } |  | ||||||
|   if (config.relay) { |  | ||||||
|     appData.init.relay = config.relay; |  | ||||||
|   } |  | ||||||
|   if (config.email) { |  | ||||||
|     appData.init.email = config.email; |  | ||||||
|   } |  | ||||||
|   if (config.agreeTos) { |  | ||||||
|     appData.init.letos = config.agreeTos; |  | ||||||
|     appData.init.teletos = config.agreeTos; |  | ||||||
|   } |  | ||||||
|   if (config._otp) { |  | ||||||
|     appData.init.otp = config._otp; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   telebitState.pollUrl = config._pollUrl || localStorage.getItem('poll_url'); |  | ||||||
| 
 |  | ||||||
|   if ((!config.token && !config._otp) || !config.relay || !config.email || !config.agreeTos) { |  | ||||||
|     changeState('setup'); |  | ||||||
|     setState(); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   if (!config.token && config._otp) { |  | ||||||
|     changeState('otp'); |  | ||||||
|     setState(); |  | ||||||
|     // this will skip ahead as necessary
 |  | ||||||
|     return Telebit.authorize(telebitState, showOtp).then(function () { |  | ||||||
|       return changeState('status'); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // TODO handle default state
 |  | ||||||
|   changeState('status'); |  | ||||||
| }).catch(function (err) { |  | ||||||
|   appData.views.flash.error = err.message || JSON.stringify(err, null, 2); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| window.api = api; |  | ||||||
| 
 |  | ||||||
| setTimeout(function () { |  | ||||||
|   document.body.hidden = false; |  | ||||||
| }, 50); |  | ||||||
| 
 |  | ||||||
| }()); |  | ||||||
| @ -1,116 +0,0 @@ | |||||||
| ;(function (exports) { |  | ||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| /* global Promise */ |  | ||||||
| var PromiseA; |  | ||||||
| if ('undefined' !== typeof Promise) { |  | ||||||
|   PromiseA = Promise; |  | ||||||
| } else { |  | ||||||
|   throw new Error("no Promise implementation defined"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var common = exports.TELEBIT || require('./lib/common.js'); |  | ||||||
| 
 |  | ||||||
| common.authorize = common.getToken = function getToken(state, showOtp) { |  | ||||||
|   state.relay = state.config.relay; |  | ||||||
| 
 |  | ||||||
|   // { _otp, config: {} }
 |  | ||||||
|   return common.api.token(state, { |  | ||||||
|     error: function (err) { console.error("[Error] common.api.token handlers.error: \n", err); return PromiseA.reject(err); } |  | ||||||
|   , directory: function (dir) { |  | ||||||
|       /*console.log('[directory] Telebit Relay Discovered:', dir);*/ |  | ||||||
|       state._apiDirectory = dir; |  | ||||||
|       return PromiseA.resolve(); |  | ||||||
|     } |  | ||||||
|   , tunnelUrl: function (tunnelUrl) { |  | ||||||
|       //console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl);
 |  | ||||||
|       state.wss = tunnelUrl; |  | ||||||
|       return PromiseA.resolve(); |  | ||||||
|     } |  | ||||||
|   , requested: function (authReq, pollUrl) { |  | ||||||
|       console.log("[requested] Pairing Requested"); |  | ||||||
|       state._otp = state._otp = authReq.otp; |  | ||||||
| 
 |  | ||||||
|       if (!state.config.token && state._can_pair) { |  | ||||||
|         console.info("0000".replace(/0000/g, state._otp)); |  | ||||||
|         showOtp(authReq.otp, pollUrl); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return PromiseA.resolve(); |  | ||||||
|     } |  | ||||||
|   , connect: function (pretoken) { |  | ||||||
|       console.log("[connect] Enabling Pairing Locally..."); |  | ||||||
|       state.config.pretoken = pretoken; |  | ||||||
|       state._connecting = true; |  | ||||||
| 
 |  | ||||||
|       // This will only be saved to the session
 |  | ||||||
|       state.config._otp = state._otp; |  | ||||||
|       return common.reqLocalAsync({ url: '/api/config', method: 'POST', body: state.config, json: true }).then(function () { |  | ||||||
|         console.info("waiting..."); |  | ||||||
|         return PromiseA.resolve(); |  | ||||||
|       }).catch(function (err) { |  | ||||||
|         state._error = err; |  | ||||||
|         console.error("Error while initializing config [connect]:"); |  | ||||||
|         console.error(err); |  | ||||||
|         return PromiseA.reject(err); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , offer: function (token) { |  | ||||||
|       //console.log("[offer] Pairing Enabled by Relay");
 |  | ||||||
|       state.token = token; |  | ||||||
|       state.config.token = token; |  | ||||||
|       if (state._error) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       state._connecting = true; |  | ||||||
|       try { |  | ||||||
|         //require('jsonwebtoken').decode(token);
 |  | ||||||
|         token = token.split('.'); |  | ||||||
|         token[0] = token[0].replace(/_/g, '/').replace(/-/g, '+'); |  | ||||||
|         while (token[0].length % 4) { token[0] += '='; } |  | ||||||
|         btoa(token[0]); |  | ||||||
|         token[1] = token[1].replace(/_/g, '/').replace(/-/g, '+'); |  | ||||||
|         while (token[1].length % 4) { token[1] += '='; } |  | ||||||
|         btoa(token[1]); |  | ||||||
|         //console.log(require('jsonwebtoken').decode(token));
 |  | ||||||
|       } catch(e) { |  | ||||||
|         console.warn("[warning] could not decode token"); |  | ||||||
|       } |  | ||||||
|       return common.reqLocalAsync({ url: '/api/config', method: 'POST', body: state.config, json: true }).then(function () { |  | ||||||
|         //console.log("Pairing Enabled Locally");
 |  | ||||||
|         return PromiseA.resolve(); |  | ||||||
|       }).catch(function (err) { |  | ||||||
|         state._error = err; |  | ||||||
|         console.error("Error while initializing config [offer]:"); |  | ||||||
|         console.error(err); |  | ||||||
|         return PromiseA.reject(err); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , granted: function (/*_*/) { /*console.log("[grant] Pairing complete!");*/ return PromiseA.resolve(); } |  | ||||||
|   , end: function () { |  | ||||||
|       return common.reqLocalAsync({ url: '/api/enable', method: 'POST', body: [], json: true }).then(function () { |  | ||||||
|         console.info("Success"); |  | ||||||
| 
 |  | ||||||
|         // workaround for https://github.com/nodejs/node/issues/21319
 |  | ||||||
|         if (state._useTty) { |  | ||||||
|           setTimeout(function () { |  | ||||||
|             console.info("Some fun things to try first:\n"); |  | ||||||
|             console.info("    ~/telebit http ~/public"); |  | ||||||
|             console.info("    ~/telebit tcp 5050"); |  | ||||||
|             console.info("    ~/telebit ssh auto"); |  | ||||||
|             console.info(); |  | ||||||
|             console.info("Press any key to continue..."); |  | ||||||
|             console.info(); |  | ||||||
|             process.exit(0); |  | ||||||
|           }, 0.5 * 1000); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         // end workaround
 |  | ||||||
| 
 |  | ||||||
|         //parseCli(state);
 |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| }('undefined' === typeof module ? window : module.exports)); |  | ||||||
| @ -1,338 +0,0 @@ | |||||||
| ;(function (exports) { |  | ||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var common = exports.TELEBIT = {}; |  | ||||||
| common.debug = true; |  | ||||||
| 
 |  | ||||||
| /* global Promise */ |  | ||||||
| var PromiseA; |  | ||||||
| if ('undefined' !== typeof Promise) { |  | ||||||
|   PromiseA = Promise; |  | ||||||
| } else { |  | ||||||
|   throw new Error("no Promise implementation defined"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /*globals AbortController*/ |  | ||||||
| if ('undefined' !== typeof fetch) { |  | ||||||
|   common.requestAsync = function (opts) { |  | ||||||
|     // funnel requests through the local server
 |  | ||||||
|     // (avoid CORS, for now)
 |  | ||||||
|     var relayOpts = { |  | ||||||
|       url: '/api/relay' |  | ||||||
|     , method: 'POST' |  | ||||||
|     , headers: { |  | ||||||
|         'Content-Type': 'application/json' |  | ||||||
|       , 'Accepts': 'application/json' |  | ||||||
|       } |  | ||||||
|     , body: JSON.stringify(opts) |  | ||||||
|     }; |  | ||||||
|     var controller = new AbortController(); |  | ||||||
|     var tok = setTimeout(function () { |  | ||||||
|       controller.abort(); |  | ||||||
|     }, 4000); |  | ||||||
|     if (!relayOpts) { |  | ||||||
|       relayOpts = {}; |  | ||||||
|     } |  | ||||||
|     relayOpts.signal = controller.signal; |  | ||||||
|     return window.fetch(relayOpts.url, relayOpts).then(function (resp) { |  | ||||||
|       clearTimeout(tok); |  | ||||||
|       return resp.json().then(function (json) { |  | ||||||
|         if (json.error) { |  | ||||||
|           return PromiseA.reject(new Error(json.error && json.error.message || JSON.stringify(json.error))); |  | ||||||
|         } |  | ||||||
|         return json; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
|   common.reqLocalAsync = function (opts) { |  | ||||||
|     if (!opts) { opts = {}; } |  | ||||||
|     if (opts.json && true !== opts.json) { |  | ||||||
|       opts.body = opts.json; |  | ||||||
|       opts.json = true; |  | ||||||
|     } |  | ||||||
|     if (opts.json) { |  | ||||||
|       if (!opts.headers) { opts.headers = {}; } |  | ||||||
|       if (opts.body) { |  | ||||||
|         opts.headers['Content-Type'] = 'application/json'; |  | ||||||
|         if ('string' !== typeof opts.body) { |  | ||||||
|           opts.body = JSON.stringify(opts.body); |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         opts.headers.Accepts = 'application/json'; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     var controller = new AbortController(); |  | ||||||
|     var tok = setTimeout(function () { |  | ||||||
|       controller.abort(); |  | ||||||
|     }, 4000); |  | ||||||
|     opts.signal = controller.signal; |  | ||||||
|     return window.fetch(opts.url, opts).then(function (resp) { |  | ||||||
|       clearTimeout(tok); |  | ||||||
|       return resp.json().then(function (json) { |  | ||||||
|         var headers = {}; |  | ||||||
|         resp.headers.forEach(function (k, v) { |  | ||||||
|           headers[k] = v; |  | ||||||
|         }); |  | ||||||
|         return { statusCode: resp.status, headers: headers, body: json }; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| } else { |  | ||||||
|   common.requestAsync = require('util').promisify(require('@coolaj86/urequest')); |  | ||||||
|   common.reqLocalAsync = require('util').promisify(require('@coolaj86/urequest')); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| common.parseUrl = function (hostname) { |  | ||||||
|   // add scheme, if missing
 |  | ||||||
|   if (!/:\/\//.test(hostname)) { |  | ||||||
|     hostname = 'https://' + hostname; |  | ||||||
|   } |  | ||||||
|   var location = new URL(hostname); |  | ||||||
|   hostname = location.hostname + (location.port ? ':' + location.port : ''); |  | ||||||
|   hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname; |  | ||||||
|   return hostname; |  | ||||||
| }; |  | ||||||
| common.parseHostname = function (hostname) { |  | ||||||
|   var location = {}; |  | ||||||
|   try { |  | ||||||
|     location = new URL(hostname); |  | ||||||
|   } catch(e) { |  | ||||||
|     // ignore
 |  | ||||||
|   } |  | ||||||
|   if (!location.protocol || /\./.test(location.protocol)) { |  | ||||||
|     hostname = 'https://' + hostname; |  | ||||||
|     location = new URL(hostname); |  | ||||||
|   } |  | ||||||
|   //hostname = location.hostname + (location.port ? ':' + location.port : '');
 |  | ||||||
|   //hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
 |  | ||||||
|   return location.hostname; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| common.apiDirectory = '_apis/telebit.cloud/index.json'; |  | ||||||
| 
 |  | ||||||
| common.otp = function getOtp() { |  | ||||||
|   return Math.round(Math.random() * 9999).toString().padStart(4, '0'); |  | ||||||
| }; |  | ||||||
| common.signToken = function (state) { |  | ||||||
|   var JWT = require('./jwt.js'); |  | ||||||
|   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; |  | ||||||
|     }) |  | ||||||
|   , aud: state._relayUrl |  | ||||||
|   , iss: Math.round(Date.now() / 1000) |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   return JWT.sign(tokenData, state.config.secret); |  | ||||||
| }; |  | ||||||
| common.promiseTimeout = function (ms) { |  | ||||||
|   var tok; |  | ||||||
|   var p = new PromiseA(function (resolve) { |  | ||||||
|     tok = setTimeout(function () { |  | ||||||
|       resolve(); |  | ||||||
|     }, ms); |  | ||||||
|   }); |  | ||||||
|   p.cancel = function () { |  | ||||||
|     clearTimeout(tok); |  | ||||||
|   }; |  | ||||||
|   return p; |  | ||||||
| }; |  | ||||||
| common.api = {}; |  | ||||||
| common.api.directory = function (state) { |  | ||||||
|   console.log('[DEBUG] state:'); |  | ||||||
|   console.log(state); |  | ||||||
|   state._relayUrl = common.parseUrl(state.relay); |  | ||||||
|   if (!state._relays) { state._relays = {}; } |  | ||||||
|   if (state._relays[state._relayUrl]) { |  | ||||||
|     return PromiseA.resolve(state._relays[state._relayUrl]); |  | ||||||
|   } |  | ||||||
|   return common.requestAsync({ url: state._relayUrl + common.apiDirectory, json: true }).then(function (resp) { |  | ||||||
|     var dir = resp.body; |  | ||||||
|     state._relays[state._relayUrl] = dir; |  | ||||||
|     return dir; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| common.api._parseWss = function (state, dir) { |  | ||||||
|   if (!dir || !dir.api_host) { |  | ||||||
|     dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; |  | ||||||
|   } |  | ||||||
|   state._relayHostname = common.parseHostname(state.relay); |  | ||||||
|   return dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state._relayHostname) + dir.tunnel.pathname; |  | ||||||
| }; |  | ||||||
| common.api.wss = function (state) { |  | ||||||
|   return common.api.directory(state).then(function (dir) { |  | ||||||
|     return common.api._parseWss(state, dir); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| common.api.token = function (state, handlers) { |  | ||||||
| 
 |  | ||||||
|   var firstReady = true; |  | ||||||
|   function pollStatus(req) { |  | ||||||
|     if (common.debug) { console.log('[debug] pollStatus called'); } |  | ||||||
|     if (common.debug) { console.log(req); } |  | ||||||
|     return common.requestAsync(req).then(function checkLocation(resp) { |  | ||||||
|       var body = resp.body; |  | ||||||
|       if (common.debug) { console.log('[debug] checkLocation'); } |  | ||||||
|       if (common.debug) { console.log(body); } |  | ||||||
|       // pending, try again
 |  | ||||||
|       if ('pending' === body.status && resp.headers.location) { |  | ||||||
|         if (common.debug) { console.log('[debug] pending'); } |  | ||||||
|         return common.promiseTimeout(2 * 1000).then(function () { |  | ||||||
|           return pollStatus({ url: resp.headers.location, json: true }); |  | ||||||
|         }); |  | ||||||
|       } else if ('ready' === body.status) { |  | ||||||
|         if (common.debug) { console.log('[debug] ready'); } |  | ||||||
|         if (firstReady) { |  | ||||||
|           if (common.debug) { console.log('[debug] first ready'); } |  | ||||||
|           firstReady = false; |  | ||||||
|           // falls through on purpose
 |  | ||||||
|           PromiseA.resolve(handlers.offer(body.access_token)).then(function () { |  | ||||||
|             /*ignore*/ |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|         return common.promiseTimeout(2 * 1000).then(function () { |  | ||||||
|           return pollStatus(req); |  | ||||||
|         }); |  | ||||||
|       } else if ('complete' === body.status) { |  | ||||||
|         if (common.debug) { console.log('[debug] complete'); } |  | ||||||
|         return PromiseA.resolve(handlers.granted(null)).then(function () { |  | ||||||
|           return PromiseA.resolve(handlers.end(null)).then(function () {}); |  | ||||||
|         }); |  | ||||||
|       } else { |  | ||||||
|         if (common.debug) { console.log('[debug] bad status'); } |  | ||||||
|         var err = new Error("Bad State:" + body.status); |  | ||||||
|         err._request = req; |  | ||||||
|         return PromiseA.reject(err); |  | ||||||
|       } |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       if (common.debug) { console.log('[debug] pollStatus error'); } |  | ||||||
|       err._request = req; |  | ||||||
|       err._hint = '[telebitd.js] pair request'; |  | ||||||
|       return PromiseA.resolve(handlers.error(err)).then(function () {}); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // directory, requested, connect, tunnelUrl, offer, granted, end
 |  | ||||||
|   function requestAuth(dir) { |  | ||||||
|     if (common.debug) { console.log('[debug] after dir'); } |  | ||||||
|     state.wss = common.api._parseWss(state, dir); |  | ||||||
| 
 |  | ||||||
|     return PromiseA.resolve(handlers.tunnelUrl(state.wss)).then(function () { |  | ||||||
|       if (common.debug) { console.log('[debug] after tunnelUrl'); } |  | ||||||
|       if (state.config.secret /* && !state.config.token */) { |  | ||||||
|         // TODO make token here in the browser
 |  | ||||||
|         //state.config._token = common.signToken(state);
 |  | ||||||
|       } |  | ||||||
|       state.token = state.token || state.config.token || state.config._token; |  | ||||||
|       if (state.token) { |  | ||||||
|         if (common.debug) { console.log('[debug] token via token or secret'); } |  | ||||||
|         // { token, pretoken }
 |  | ||||||
|         return PromiseA.resolve(handlers.connect(state.token)).then(function () { |  | ||||||
|           return PromiseA.resolve(handlers.end(null)); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (!dir.pair_request) { |  | ||||||
|         if (common.debug) { console.log('[debug] no dir, connect'); } |  | ||||||
|         return PromiseA.resolve(handlers.error(new Error("No token found or generated, and no pair_request api found."))); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // TODO sign token with own private key, including public key and thumbprint
 |  | ||||||
|       //      (much like ACME JOSE account)
 |  | ||||||
|       // TODO handle agree
 |  | ||||||
|       var otp = state._otp; // common.otp();
 |  | ||||||
|       var authReq = { |  | ||||||
|         subject: state.config.email |  | ||||||
|       , subject_scheme: 'mailto' |  | ||||||
|         // TODO create domains list earlier
 |  | ||||||
|       , scope: (state.config._servernames || Object.keys(state.config.servernames || {})) |  | ||||||
|           .concat(state.config._ports || Object.keys(state.config.ports || {})).join(',') |  | ||||||
|       , otp: otp |  | ||||||
|       // TODO make call to daemon for this info beforehand
 |  | ||||||
|       /* |  | ||||||
|       , hostname: os.hostname() |  | ||||||
|         // Used for User-Agent
 |  | ||||||
|       , os_type: os.type() |  | ||||||
|       , os_platform: os.platform() |  | ||||||
|       , os_release: os.release() |  | ||||||
|       , os_arch: os.arch() |  | ||||||
|       */ |  | ||||||
|       }; |  | ||||||
|       var pairRequestUrl = new URL(dir.pair_request.pathname, 'https://' + dir.api_host.replace(/:hostname/g, state._relayHostname)); |  | ||||||
|       console.log('pairRequestUrl:', pairRequestUrl); |  | ||||||
|       //console.log('pairRequestUrl:', JSON.stringify(pairRequestUrl.toJSON()));
 |  | ||||||
|       var req = { |  | ||||||
|         // WHATWG URL defines .toJSON() but, of course, it's not implemented
 |  | ||||||
|         // because... why would we implement JavaScript objects in the DOM
 |  | ||||||
|         // when we can have perfectly incompatible non-JS objects?
 |  | ||||||
|         url: { |  | ||||||
|           host: pairRequestUrl.host |  | ||||||
|         , hostname: pairRequestUrl.hostname |  | ||||||
|         , href: pairRequestUrl.href |  | ||||||
|         , pathname: pairRequestUrl.pathname |  | ||||||
|           // because why wouldn't node require 'path' on a json object and accept 'pathname' on a URL object...
 |  | ||||||
|           // https://twitter.com/coolaj86/status/1053947919890403328
 |  | ||||||
|         , path: pairRequestUrl.pathname |  | ||||||
|         , port: pairRequestUrl.port || null |  | ||||||
|         , protocol: pairRequestUrl.protocol |  | ||||||
|         , search: pairRequestUrl.search || null |  | ||||||
|         } |  | ||||||
|       , method: dir.pair_request.method |  | ||||||
|       , json: authReq |  | ||||||
|       }; |  | ||||||
| 
 |  | ||||||
|       return common.requestAsync(req).then(function doFirst(resp) { |  | ||||||
|         var body = resp.body; |  | ||||||
|         if (common.debug) { console.log('[debug] first req'); } |  | ||||||
|         if (!body.access_token && !body.jwt) { |  | ||||||
|           return PromiseA.reject(new Error("something wrong with pre-authorization request")); |  | ||||||
|         } |  | ||||||
|         return PromiseA.resolve(handlers.requested(authReq, resp.headers.location)).then(function () { |  | ||||||
|           return PromiseA.resolve(handlers.connect(body.access_token || body.jwt)).then(function () { |  | ||||||
|             var err; |  | ||||||
|             if (!resp.headers.location) { |  | ||||||
|               err = new Error("bad authentication request response"); |  | ||||||
|               err._resp = resp.toJSON && resp.toJSON(); |  | ||||||
|               return PromiseA.resolve(handlers.error(err)).then(function () {}); |  | ||||||
|             } |  | ||||||
|             return common.promiseTimeout(2 * 1000).then(function () { |  | ||||||
|               return pollStatus({ url: resp.headers.location, json: true }); |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       }).catch(function (err) { |  | ||||||
|         if (common.debug) { console.log('[debug] gotoFirst error'); } |  | ||||||
|         err._request = req; |  | ||||||
|         err._hint = '[telebitd.js] pair request'; |  | ||||||
|         return PromiseA.resolve(handlers.error(err)).then(function () {}); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (state.pollUrl) { |  | ||||||
|     return pollStatus({ url: state.pollUrl, json: true }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // backwards compat (TODO verify we can remove this)
 |  | ||||||
|   var failoverDir = '{ "api_host": ":hostname", "tunnel": { "method": "wss", "pathname": "" } }'; |  | ||||||
|   return common.api.directory(state).then(function (dir) { |  | ||||||
|     console.log('[debug] [directory]', dir); |  | ||||||
|     if (!dir.api_host) { dir = JSON.parse(failoverDir); } |  | ||||||
|     return dir; |  | ||||||
|   }).catch(function (err) { |  | ||||||
|     console.warn('[warn] [directory] fetch fail, using failover'); |  | ||||||
|     console.warn(err); |  | ||||||
|     return JSON.parse(failoverDir); |  | ||||||
|   }).then(function (dir) { |  | ||||||
|     return PromiseA.resolve(handlers.directory(dir)).then(function () { |  | ||||||
|       console.log('[debug] [directory]', dir); |  | ||||||
|       return requestAuth(dir); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| }('undefined' !== typeof module ? module.exports : window)); |  | ||||||
							
								
								
									
										10947
									
								
								lib/admin/js/vue.js
									
									
									
									
									
								
							
							
						
						
									
										10947
									
								
								lib/admin/js/vue.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -118,7 +118,7 @@ common.otp = function getOtp() { | |||||||
|   return Math.round(Math.random() * 9999).toString().padStart(4, '0'); |   return Math.round(Math.random() * 9999).toString().padStart(4, '0'); | ||||||
| }; | }; | ||||||
| common.signToken = function (state) { | common.signToken = function (state) { | ||||||
|   var JWT = require('./jwt.js'); |   var jwt = require('jsonwebtoken'); | ||||||
|   var tokenData = { |   var tokenData = { | ||||||
|     domains: Object.keys(state.config.servernames || {}).filter(function (name) { |     domains: Object.keys(state.config.servernames || {}).filter(function (name) { | ||||||
|       return /\./.test(name); |       return /\./.test(name); | ||||||
| @ -131,7 +131,7 @@ common.signToken = function (state) { | |||||||
|   , iss: Math.round(Date.now() / 1000) |   , iss: Math.round(Date.now() / 1000) | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   return JWT.sign(tokenData, state.config.secret); |   return jwt.sign(tokenData, state.config.secret); | ||||||
| }; | }; | ||||||
| common.api = {}; | common.api = {}; | ||||||
| common.api.directory = function (state, next) { | common.api.directory = function (state, next) { | ||||||
|  | |||||||
							
								
								
									
										102
									
								
								lib/eggspress.js
									
									
									
									
									
								
							
							
						
						
									
										102
									
								
								lib/eggspress.js
									
									
									
									
									
								
							| @ -1,102 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| function eggSend(obj) { |  | ||||||
|   /*jslint validthis: true*/ |  | ||||||
|   var me = this; |  | ||||||
|   if (!me.getHeader('content-type')) { |  | ||||||
|     me.setHeader('Content-Type', 'application/json'); |  | ||||||
|   } |  | ||||||
|   me.end(JSON.stringify(obj)); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports = function eggspress() { |  | ||||||
|   //var patternsMap = {};
 |  | ||||||
|   var allPatterns = []; |  | ||||||
|   var app = function (req, res) { |  | ||||||
|     var patterns = allPatterns.slice(0).reverse(); |  | ||||||
|     function next(err) { |  | ||||||
|       if (err) { |  | ||||||
|         req.end(err.message); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       var todo = patterns.pop(); |  | ||||||
|       if (!todo) { |  | ||||||
|         console.log('[eggspress] Did not match any patterns', req.url); |  | ||||||
|         require('finalhandler')(req, res)(); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // '', GET, POST, DELETE
 |  | ||||||
|       if (todo[2] && req.method.toLowerCase() !== todo[2]) { |  | ||||||
|         //console.log("[eggspress] HTTP method doesn't match", req.url);
 |  | ||||||
|         next(); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       var urlstr = (req.url.replace(/\/$/, '') + '/'); |  | ||||||
|       if (!urlstr.match(todo[0])) { |  | ||||||
|         //console.log("[eggspress] pattern doesn't match", todo[0], req.url);
 |  | ||||||
|         next(); |  | ||||||
|         return; |  | ||||||
|       } else if ('string' === typeof todo[0] && 0 !== urlstr.match(todo[0]).index) { |  | ||||||
|         //console.log("[eggspress] string pattern is not the start", todo[0], req.url);
 |  | ||||||
|         next(); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       function fail(e) { |  | ||||||
|         console.error("[eggspress] error", todo[2], todo[0], req.url); |  | ||||||
|         console.error(e); |  | ||||||
|         // TODO make a nice error message
 |  | ||||||
|         res.end(e.message); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       try { |  | ||||||
|         console.log("[eggspress] matched pattern", todo[0], req.url); |  | ||||||
|         var p = todo[1](req, res, next); |  | ||||||
|         if (p && p.catch) { |  | ||||||
|           p.catch(fail); |  | ||||||
|         } |  | ||||||
|       } catch(e) { |  | ||||||
|         fail(e); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.send = eggSend; |  | ||||||
| 
 |  | ||||||
|     next(); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   app.use = function (pattern, fn) { |  | ||||||
|     return app._use('', pattern, fn); |  | ||||||
|   }; |  | ||||||
|   [ 'HEAD', 'GET', 'POST', 'DELETE' ].forEach(function (method) { |  | ||||||
|     app[method.toLowerCase()] = function (pattern, fn) { |  | ||||||
|       return app._use(method, pattern, fn); |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   app.post = function (pattern, fn) { |  | ||||||
|     return app._use('POST', pattern, fn); |  | ||||||
|   }; |  | ||||||
|   app._use = function (method, pattern, fn) { |  | ||||||
|     // always end in a slash, for now
 |  | ||||||
|     if ('string' === typeof pattern) { |  | ||||||
|       pattern = pattern.replace(/\/$/, '')  + '/'; |  | ||||||
|     } |  | ||||||
|     /* |  | ||||||
|     if (!patternsMap[pattern]) { |  | ||||||
|       patternsMap[pattern] = []; |  | ||||||
|     } |  | ||||||
|     patternsMap[pattern].push(fn); |  | ||||||
|     patterns = Object.keys(patternsMap).sort(function (a, b) { |  | ||||||
|       return b.length - a.length; |  | ||||||
|     }); |  | ||||||
|     */ |  | ||||||
|     allPatterns.push([pattern, fn, method.toLowerCase()]); |  | ||||||
|     return app; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   return app; |  | ||||||
| }; |  | ||||||
| @ -6,7 +6,7 @@ Telebit Remote is the T-Rex long-arm of the Internet. UNSTOPPABLE! | |||||||
| 
 | 
 | ||||||
| Using reliable HTTPS tunneling to establishing peer-to-peer connections, | Using reliable HTTPS tunneling to establishing peer-to-peer connections, | ||||||
| Telebit is empowering the next generation of tinkerers. Access your devices. | Telebit is empowering the next generation of tinkerers. Access your devices. | ||||||
| Share your stuff. Be UNSTOPPABLE! (Join us at https://ppl.family) | Share your stuff. Be UNSTOPPABLE! (Join us at https://rootprojects.org) | ||||||
| 
 | 
 | ||||||
| Usage: | Usage: | ||||||
| 
 | 
 | ||||||
| @ -135,7 +135,7 @@ usage: telebit http <path/port/none> [subdomain] | |||||||
| 
 | 
 | ||||||
| Use cases: | Use cases: | ||||||
| 
 | 
 | ||||||
|         - Lazy man's AirDrop (works or lazy women too!) |         - Lazy man's AirDrop (works for lazy women too!) | ||||||
|         - Testing dev sites on a phone |         - Testing dev sites on a phone | ||||||
|         - Sharing indie music and movies with friends" |         - Sharing indie music and movies with friends" | ||||||
| 
 | 
 | ||||||
| @ -145,7 +145,7 @@ usage: telebit ssh <auto|port|none> | |||||||
| 
 | 
 | ||||||
| All https traffic will be inspected to see if it looks like ssh Once enabled all traffic that looks | All https traffic will be inspected to see if it looks like ssh Once enabled all traffic that looks | ||||||
| 
 | 
 | ||||||
|         ssh auto                        Make ssh Just Works™ (on port 22) |         ssh auto                        Make ssh Just Work™ (on port 22) | ||||||
| 
 | 
 | ||||||
|         ssh <port>                      forward ssh traffic to non-standard port |         ssh <port>                      forward ssh traffic to non-standard port | ||||||
|         ex: telebit ssh 22              ex: explicitly forward ssh-looking packets to localhost:22 |         ex: telebit ssh 22              ex: explicitly forward ssh-looking packets to localhost:22 | ||||||
| @ -464,6 +464,19 @@ code = " | |||||||
| ============================================== | ============================================== | ||||||
| " | " | ||||||
| 
 | 
 | ||||||
|  | waiting = "waiting for you to check your email..." | ||||||
|  | 
 | ||||||
|  | success = "Success" | ||||||
|  | 
 | ||||||
|  | next_steps = "Some fun things to try first: | ||||||
|  | 
 | ||||||
|  |     ~/telebit http ~/Public | ||||||
|  |     ~/telebit tcp 5050 | ||||||
|  |     ~/telebit ssh auto | ||||||
|  | 
 | ||||||
|  | Press any key to continue... | ||||||
|  | " | ||||||
|  | 
 | ||||||
| [remote.setup] | [remote.setup] | ||||||
| 
 | 
 | ||||||
| email = "Welcome! | email = "Welcome! | ||||||
| @ -476,13 +489,5 @@ By using Telebit you agree to: | |||||||
| Enter your email to agree and login/create your account: | Enter your email to agree and login/create your account: | ||||||
| " | " | ||||||
| 
 | 
 | ||||||
| fail_relay_check = "=================== |  | ||||||
|       WARNING |  | ||||||
| =================== |  | ||||||
| 
 |  | ||||||
| [{{status_code}}] '{{url}}' |  | ||||||
| This server does not describe a current telebit version (but it may still work). |  | ||||||
| " |  | ||||||
| 
 |  | ||||||
| [daemon] | [daemon] | ||||||
| version = "telebit daemon v{version}" | version = "telebit daemon v{version}" | ||||||
|  | |||||||
| @ -51,18 +51,24 @@ | |||||||
| 
 | 
 | ||||||
|     <div> |     <div> | ||||||
|       <h2>You've claimed <span class="js-servername">{{servername}}</span></h2> |       <h2>You've claimed <span class="js-servername">{{servername}}</span></h2> | ||||||
|       <p>Here's some ways you can use it:</p> |       <p>Here are some ways you can use Telebit via Terminal or other Command Line Interface:</p> | ||||||
|       <div class="code-block"> |       <div class="code-block"> | ||||||
|         <pre><code>telebit http ~/Public            # serve a public folder | 	      <br /> | ||||||
| telebit http 3000                # forward all https traffic to localhost:3000 |         <pre><code>~/telebit ssh auto            # allows you to connect to your computer with <br />                                ssh-over-https from a different computer</span></code></pre> | ||||||
| telebit http none                # remove all https handlers</code></pre> |         <pre><code>~/telebit http ~/Public            # serve a public folder | ||||||
|  | ~/telebit http 3000                # forward all https traffic to localhost:3000 | ||||||
|  | ~/telebit http none                # remove all https handlers</code></pre> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <p>You can <em>always</em> tunnel <strong>SSH over HTTPS</strong>, |     <p>And remember you can <em>always</em> tunnel <strong>SSH over HTTPS</strong>, | ||||||
|       even while you're using it for something else:</p> |       even while you're using it for something else:</p> | ||||||
|       <div class="code-block"><pre><code>telebit ssh auto</code></pre> |   <p> </p> | ||||||
|  |    | ||||||
|  |     <details> | ||||||
|  |     <p><summary><strong>Here are some examples for those of you that want to access files and folders remotely.  </strong></summary></p> | ||||||
|  |     <p><strong>This function allows you to connect one computer to another computer you also have SSH on.</strong></p> | ||||||
|  |       <div class="code-block"><pre><code>~/telebit ssh <span class="js-servername">{{servername}}</span></code></pre> | ||||||
|       	<br> |       	<br> | ||||||
| 				<pre><code>telebit ssh <span class="js-servername">{{servername}}</span></code></pre> |  | ||||||
|     		- or - |     		- or - | ||||||
|     		<pre><code>ssh -o ProxyCommand='<a href="https://telebit.cloud/sclient">sclient</a> %h' <span class="js-servername">{{servername}}</span></code></pre> |     		<pre><code>ssh -o ProxyCommand='<a href="https://telebit.cloud/sclient">sclient</a> %h' <span class="js-servername">{{servername}}</span></code></pre> | ||||||
|     		- or - |     		- or - | ||||||
| @ -70,8 +76,7 @@ telebit http none                # remove all https handlers</code></pre> | |||||||
| ssh -o ProxyCommand="$proxy_cmd" <span class="js-servername">{{servername}}</span></code></pre> | ssh -o ProxyCommand="$proxy_cmd" <span class="js-servername">{{servername}}</span></code></pre> | ||||||
| 			</div> | 			</div> | ||||||
|     <pre><code>ssh -o ProxyCommand='openssl s_client -connect %h:443 -servername %h -quiet' <span class="js-servername">{{servername}}</span></code></pre> |     <pre><code>ssh -o ProxyCommand='openssl s_client -connect %h:443 -servername %h -quiet' <span class="js-servername">{{servername}}</span></code></pre> | ||||||
| 
 |     </details> | ||||||
| 
 |  | ||||||
|     <!--div class="js-port" hidden> |     <!--div class="js-port" hidden> | ||||||
|       <h2>You've claimed port <span class="js-serviceport">{{serviceport}}</span></h2> |       <h2>You've claimed port <span class="js-serviceport">{{serviceport}}</span></h2> | ||||||
|       <p>Here's some ways you can use it:</p> |       <p>Here's some ways you can use it:</p> | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
| 
 | 
 | ||||||
| document.body.hidden = false; | document.body.hidden = false; | ||||||
| 
 | 
 | ||||||
| var hash = window.location.hash.substr(1); | var hash = window.location.hash.replace(/^[\/#?]+/, ''); | ||||||
| var query = window.location.search; | var query = window.location.search; | ||||||
| 
 | 
 | ||||||
| function parseQuery(search) { | function parseQuery(search) { | ||||||
|  | |||||||
| @ -1,19 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var crypto = require('crypto'); |  | ||||||
| var FAT = require('jsonwebtoken'); |  | ||||||
| var JWT = require('./jwt.js'); |  | ||||||
| 
 |  | ||||||
| var key = "justanothersecretsecret"; |  | ||||||
| var keyid = crypto.createHash('sha256').update(key).digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); |  | ||||||
| 
 |  | ||||||
| var tok1 = FAT.sign({ foo: "hello" }, key, { keyid: keyid }); |  | ||||||
| var tok2 = JWT.sign({ foo: "hello" }, key); |  | ||||||
| 
 |  | ||||||
| if (tok1 !== tok2) { |  | ||||||
|   console.error(JWT.decode(tok1)); |  | ||||||
|   console.error(JWT.decode(tok2)); |  | ||||||
|   throw new Error("our jwt doesn't match auth0/jsonwebtoken"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| console.info('Pass'); |  | ||||||
							
								
								
									
										43
									
								
								lib/jwt.js
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								lib/jwt.js
									
									
									
									
									
								
							| @ -1,43 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var crypto = require('crypto'); |  | ||||||
| var JWT = module.exports; |  | ||||||
| 
 |  | ||||||
| JWT.decode = function (jwt) { |  | ||||||
|   var parts; |  | ||||||
| 
 |  | ||||||
|   try { |  | ||||||
|     parts = jwt.split('.'); |  | ||||||
|     return { |  | ||||||
|       header: JSON.parse(Buffer.from(parts[0], 'base64')) |  | ||||||
|     , payload: JSON.parse(Buffer.from(parts[1], 'base64')) |  | ||||||
|     , signature: parts[2] //Buffer.from(parts[2], 'base64')
 |  | ||||||
|     }; |  | ||||||
|   } catch(e) { |  | ||||||
|     throw new Error("JWT Parse Error: could not split, base64 decode, and JSON.parse token " + jwt); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| JWT.verify = function (jwt) { |  | ||||||
|   var decoded = JWT.decode(jwt); |  | ||||||
|   throw new Error("not implemented yet"); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function base64ToUrlSafe(str) { |  | ||||||
|   return str |  | ||||||
|     .replace(/\+/g, '-') |  | ||||||
|     .replace(/\//g, '_') |  | ||||||
|     .replace(/=/g, '') |  | ||||||
|   ; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| JWT.sign = function (claims, key) { |  | ||||||
|   if (!claims.iat && false !== claims.iat) { |  | ||||||
|     claims.iat = Math.round(Date.now()/1000); |  | ||||||
|   } |  | ||||||
|   var thumb = base64ToUrlSafe(crypto.createHash('sha256').update(key).digest('base64')); |  | ||||||
|   var protect = base64ToUrlSafe(Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid: thumb })).toString('base64')); |  | ||||||
|   var payload = base64ToUrlSafe(Buffer.from(JSON.stringify(claims)).toString('base64')); |  | ||||||
|   var signature = base64ToUrlSafe(crypto.createHmac('sha256', key).update(protect + '.' + payload).digest('base64')); |  | ||||||
|   return protect + '.' + payload + '.' + signature; |  | ||||||
| }; |  | ||||||
| @ -1,37 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| /*global Promise*/ |  | ||||||
| 
 |  | ||||||
| var fs = require('fs').promises; |  | ||||||
| var path = require('path'); |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (opts) { |  | ||||||
|   var keyext = '.key'; |  | ||||||
|   return { |  | ||||||
|     getPassword: function (service, name) { |  | ||||||
|       var f = path.join(opts.configDir, name + keyext); |  | ||||||
|       return fs.readFile(f, 'utf8').catch(function (err) { |  | ||||||
|         if ('ENOEXIST' === err.code) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , setPassword: function (service, name, key) { |  | ||||||
|       var f = path.join(opts.configDir, name + keyext); |  | ||||||
|       return fs.writeFile(f, key, 'utf8'); |  | ||||||
|     } |  | ||||||
|   , deletePassword: function (service, name) { |  | ||||||
|       var f = path.join(opts.configDir, name + keyext); |  | ||||||
|       return fs.unlink(f); |  | ||||||
|     } |  | ||||||
|   , findCredentials: function (/*service*/) { |  | ||||||
|       return fs.readDir(opts.configDir).then(function (nodes) { |  | ||||||
|         return Promise.all(nodes.filter(function (node) { |  | ||||||
|           return keyext === node.slice(-4); |  | ||||||
|         }).map(function (node) { |  | ||||||
|           return fs.readFile(path.join(opts.configDir, node + keyext)); |  | ||||||
|         })); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , insecure: true |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @ -1,22 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var keystore = require('./keystore.js').create({ |  | ||||||
|   configDir: require('path').join(require('os').homedir(), '.local/telebit/') |  | ||||||
| }); |  | ||||||
| var name = "testy-mctestface-1"; |  | ||||||
| return keystore.get(name).then(function (jwk) { |  | ||||||
|   console.log("get1", typeof jwk, jwk); |  | ||||||
|   if (!jwk || !jwk.kty) { |  | ||||||
|     return require('keypairs').generate().then(function (jwk) { |  | ||||||
|       var json = JSON.stringify(jwk.private); |  | ||||||
|       return keystore.set(name, json).then(function () { |  | ||||||
|         return keystore.get(name).then(function (val2) { |  | ||||||
|           console.log("get2", val2); |  | ||||||
|         }); |  | ||||||
|       }).catch(function (err) { |  | ||||||
|         console.log('badness', err); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   return jwk; |  | ||||||
| }); |  | ||||||
| @ -1,49 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (opts) { |  | ||||||
|   var service = opts.name || "Telebit"; |  | ||||||
|   var keytar; |  | ||||||
|   try { |  | ||||||
|     keytar = require('keytar'); |  | ||||||
|     // TODO test that long "passwords" (JWTs and JWKs) can be stored in all OSes
 |  | ||||||
|   } catch(e) { |  | ||||||
|     console.warn("Could not load native key management. Keys will be stored in plain text."); |  | ||||||
|     keytar = require('./keystore-fallback.js').create(opts); |  | ||||||
|     keytar.insecure = true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     get: function (name) { |  | ||||||
|       return keytar.getPassword(service, name).then(maybeParse); |  | ||||||
|     } |  | ||||||
|   , set: function (name, value) { |  | ||||||
|       return keytar.setPassword(service, name, maybeStringify(value)); |  | ||||||
|     } |  | ||||||
|   , delete: function (name) { |  | ||||||
|       return keytar.deletePassword(service, name); |  | ||||||
|     } |  | ||||||
|   , all: function () { |  | ||||||
|       return keytar.findCredentials(service).then(function (list) { |  | ||||||
|         return list.map(function (el) { |  | ||||||
|           el.password = maybeParse(el.password); |  | ||||||
|           return el; |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , insecure: keytar.insecure |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function maybeParse(str) { |  | ||||||
|   if (str && '{' === str[0]) { |  | ||||||
|     return JSON.parse(str); |  | ||||||
|   } |  | ||||||
|   return str; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function maybeStringify(obj) { |  | ||||||
|   if ('string' !== typeof obj && 'object' === typeof obj) { |  | ||||||
|     return JSON.stringify(obj); |  | ||||||
|   } |  | ||||||
|   return obj; |  | ||||||
| } |  | ||||||
							
								
								
									
										175
									
								
								lib/rc/index.js
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								lib/rc/index.js
									
									
									
									
									
								
							| @ -1,175 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var os = require('os'); |  | ||||||
| var path = require('path'); |  | ||||||
| var http = require('http'); |  | ||||||
| var keypairs = require('keypairs'); |  | ||||||
| 
 |  | ||||||
| var common = require('../cli-common.js'); |  | ||||||
| 
 |  | ||||||
| /* |  | ||||||
| function packConfig(config) { |  | ||||||
|   return Object.keys(config).map(function (key) { |  | ||||||
|     var val = config[key]; |  | ||||||
|     if ('undefined' === val) { |  | ||||||
|       throw new Error("'undefined' used as a string value"); |  | ||||||
|     } |  | ||||||
|     if ('undefined' === typeof val) { |  | ||||||
|       //console.warn('[DEBUG]', key, 'is present but undefined');
 |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     if (val && 'object' === typeof val && !Array.isArray(val)) { |  | ||||||
|       val = JSON.stringify(val); |  | ||||||
|     } |  | ||||||
|     return key + ':' + val; // converts arrays to strings with ,
 |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| module.exports.create = function (state) { |  | ||||||
|   common._init( |  | ||||||
|     // make a default working dir and log dir
 |  | ||||||
|     state._clientConfig.root || path.join(os.homedir(), '.local/share/telebit') |  | ||||||
|   , (state._clientConfig.root && path.join(state._clientConfig.root, 'etc')) |  | ||||||
|       || path.resolve(common.DEFAULT_CONFIG_PATH, '..') |  | ||||||
|   ); |  | ||||||
|   state._ipc = common.pipename(state._clientConfig, true); |  | ||||||
| 
 |  | ||||||
|   function makeResponder(service, resp, fn) { |  | ||||||
|     var body = ''; |  | ||||||
| 
 |  | ||||||
|     function finish() { |  | ||||||
|       var err; |  | ||||||
| 
 |  | ||||||
|       if (200 !== resp.statusCode) { |  | ||||||
|         err = new Error(body || ('get ' + service + ' failed')); |  | ||||||
|         err.statusCode = resp.statusCode; |  | ||||||
|         err.code = "E_REQUEST"; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (body) { |  | ||||||
|         try { |  | ||||||
|           body = JSON.parse(body); |  | ||||||
|         } catch(e) { |  | ||||||
|           console.error('Error:', err); |  | ||||||
|           // ignore
 |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       fn(err, body); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!resp.headers['content-length'] && !resp.headers['content-type']) { |  | ||||||
|       finish(); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // TODO use readable
 |  | ||||||
|     resp.on('data', function (chunk) { |  | ||||||
|       body += chunk.toString(); |  | ||||||
|     }); |  | ||||||
|     resp.on('end', finish); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var RC = {}; |  | ||||||
|   RC.resolve = function (pathstr) { |  | ||||||
|     // TODO use real hostname and return reqOpts rather than string?
 |  | ||||||
|     return 'http://localhost:' + (RC.port({}).port||'1').toString() + '/' + pathstr.replace(/^\//, ''); |  | ||||||
|   }; |  | ||||||
|   RC.port = function (reqOpts) { |  | ||||||
|     var fs = require('fs'); |  | ||||||
|     var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port'); |  | ||||||
|     if (fs.existsSync(portFile)) { |  | ||||||
|       reqOpts.host = 'localhost'; |  | ||||||
|       reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10); |  | ||||||
|       if (!state.ipc) { |  | ||||||
|         state.ipc = {}; |  | ||||||
|       } |  | ||||||
|       state.ipc.type = 'port'; |  | ||||||
|       state.ipc.path = path.dirname(state._ipc.path); |  | ||||||
|       state.ipc.port = reqOpts.port; |  | ||||||
|     } else { |  | ||||||
|       reqOpts.socketPath = state._ipc.path; |  | ||||||
|     } |  | ||||||
|     return reqOpts; |  | ||||||
|   }; |  | ||||||
|   RC.createErrorHandler = function (replay, opts, cb) { |  | ||||||
|     return function (err) { |  | ||||||
|       // ENOENT - never started, cleanly exited last start, or creating socket at a different path
 |  | ||||||
|       // ECONNREFUSED - leftover socket just needs to be restarted
 |  | ||||||
|       if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { |  | ||||||
|         if (opts._taketwo) { |  | ||||||
|           cb(err); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) { |  | ||||||
|           if (err) { cb(err); return; } |  | ||||||
|           opts._taketwo = true; |  | ||||||
|           setTimeout(function () { |  | ||||||
|             replay(opts, cb); |  | ||||||
|           }, 2500); |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       cb(err); |  | ||||||
|     }; |  | ||||||
|   }; |  | ||||||
|   RC.request = function request(opts, fn) { |  | ||||||
|     if (!opts) { opts = {}; } |  | ||||||
|     var service = opts.service || 'config'; |  | ||||||
|     /* |  | ||||||
|     var args = opts.data; |  | ||||||
|     if (args && 'control' === service) { |  | ||||||
|       args = packConfig(args); |  | ||||||
|     } |  | ||||||
|     var json = JSON.stringify(opts.data); |  | ||||||
|     */ |  | ||||||
|     var url = '/rpc/' + service; |  | ||||||
|     /* |  | ||||||
|     if (json) { |  | ||||||
|       url += ('?_body=' + encodeURIComponent(json)); |  | ||||||
|     } |  | ||||||
|     */ |  | ||||||
|     var method = opts.method || (opts.data && 'POST') || 'GET'; |  | ||||||
|     var reqOpts = { |  | ||||||
|       method: method |  | ||||||
|     , path: url |  | ||||||
|     }; |  | ||||||
|     reqOpts = RC.port(reqOpts); |  | ||||||
|     var req = http.request(reqOpts, function (resp) { |  | ||||||
|       makeResponder(service, resp, fn); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     req.on('error', RC.createErrorHandler(RC.request, opts, fn)); |  | ||||||
| 
 |  | ||||||
|     // Simple GET
 |  | ||||||
|     if ('POST' !== method || !opts.data) { |  | ||||||
|       return keypairs.signJwt({ |  | ||||||
|         jwk: state.key |  | ||||||
|       , claims: { iss: false, exp: Math.round(Date.now()/1000) + (15 * 60) } |  | ||||||
|       //TODO , exp: '15m'
 |  | ||||||
|       }).then(function (jwt) { |  | ||||||
|         req.setHeader("authorization", 'bearer ' + jwt); |  | ||||||
|         req.end(); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return keypairs.signJws({ |  | ||||||
|       jwk: state.key |  | ||||||
|     , protected: { |  | ||||||
|         // alg will be filled out automatically
 |  | ||||||
|         jwk: state.pub |  | ||||||
|       , nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server
 |  | ||||||
|         // TODO make localhost exceptional
 |  | ||||||
|       , url: RC.resolve(reqOpts.path) |  | ||||||
|       } |  | ||||||
|     , payload: JSON.stringify(opts.data) |  | ||||||
|     }).then(function (jws) { |  | ||||||
|       req.setHeader("Content-Type", 'application/jose+json'); |  | ||||||
|       req.write(JSON.stringify(jws)); |  | ||||||
|       req.end(); |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
|   return RC; |  | ||||||
| }; |  | ||||||
| @ -28,7 +28,6 @@ function TelebitRemote(state) { | |||||||
|   EventEmitter.call(this); |   EventEmitter.call(this); | ||||||
|   var me = this; |   var me = this; | ||||||
|   var priv = {}; |   var priv = {}; | ||||||
|   var path = require('path'); |  | ||||||
| 
 | 
 | ||||||
|   //var defaultHttpTimeout = (2 * 60);
 |   //var defaultHttpTimeout = (2 * 60);
 | ||||||
|   //var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000;
 |   //var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000;
 | ||||||
| @ -40,9 +39,8 @@ function TelebitRemote(state) { | |||||||
|   priv.tokens = []; |   priv.tokens = []; | ||||||
|   var auth; |   var auth; | ||||||
|   if(!state.sortingHat) { |   if(!state.sortingHat) { | ||||||
|     state.sortingHat = path.join(__dirname, '../sorting-hat.js'); |     state.sortingHat = "./sorting-hat.js"; | ||||||
|   } |   } | ||||||
|   state._connectionHandler = require(state.sortingHat); |  | ||||||
|   if (state.token) { |   if (state.token) { | ||||||
|     if ('undefined' === state.token) { |     if ('undefined' === state.token) { | ||||||
|       throw new Error("passed string 'undefined' as token"); |       throw new Error("passed string 'undefined' as token"); | ||||||
| @ -351,7 +349,7 @@ function TelebitRemote(state) { | |||||||
| 
 | 
 | ||||||
|       // TODO use readable streams instead
 |       // TODO use readable streams instead
 | ||||||
|       wstunneler._socket.pause(); |       wstunneler._socket.pause(); | ||||||
|       state._connectionHandler.assign(state, tun, function (err, conn) { |       require(state.sortingHat).assign(state, tun, function (err, conn) { | ||||||
|         if (err) { |         if (err) { | ||||||
|           err.message = err.message.replace(/:tun_id/, tun._id); |           err.message = err.message.replace(/:tun_id/, tun._id); | ||||||
|           packerHandlers._onConnectError(cid, tun, err); |           packerHandlers._onConnectError(cid, tun, err); | ||||||
| @ -474,12 +472,12 @@ function TelebitRemote(state) { | |||||||
|     priv.timeoutId = null; |     priv.timeoutId = null; | ||||||
|     var machine = Packer.create(packerHandlers); |     var machine = Packer.create(packerHandlers); | ||||||
| 
 | 
 | ||||||
|     console.info("[telebit:lib/daemon.js] [connect] '" + (state.wss || state.relay) + "'"); |     console.info("[telebit:lib/remote.js] [connect] '" + (state.wss || state.relay) + "'"); | ||||||
|     var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth;
 |     var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth;
 | ||||||
|     wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure }); |     wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure }); | ||||||
|     // XXXXXX
 |     // XXXXXX
 | ||||||
|     wstunneler.on('open', function () { |     wstunneler.on('open', function () { | ||||||
|       console.info("[telebit:lib/daemon.js] [open] connected to '" + (state.wss || state.relay) + "'"); |       console.info("[telebit:lib/remote.js] [open] connected to '" + (state.wss || state.relay) + "'"); | ||||||
|       me.emit('connect'); |       me.emit('connect'); | ||||||
|       priv.refreshTimeout(); |       priv.refreshTimeout(); | ||||||
|       priv.timeoutId = setTimeout(priv.checkTimeout, activityTimeout); |       priv.timeoutId = setTimeout(priv.checkTimeout, activityTimeout); | ||||||
							
								
								
									
										76
									
								
								lib/ssh.js
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								lib/ssh.js
									
									
									
									
									
								
							| @ -1,76 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| /*global Promise*/ |  | ||||||
| var PromiseA = Promise; |  | ||||||
| var crypto = require('crypto'); |  | ||||||
| var util = require('util'); |  | ||||||
| var readFile = util.promisify(require('fs').readFile); |  | ||||||
| var exec = require('child_process').exec; |  | ||||||
| 
 |  | ||||||
| function sshAllowsPassword(user) { |  | ||||||
|   // SSH on Windows is a thing now (beta 2015, standard 2018)
 |  | ||||||
|   // https://stackoverflow.com/questions/313111/is-there-a-dev-null-on-windows
 |  | ||||||
|   var nullfile = '/dev/null'; |  | ||||||
|   if (/^win/i.test(process.platform)) { |  | ||||||
|     nullfile = 'NUL'; |  | ||||||
|   } |  | ||||||
|   var args = [ |  | ||||||
|     'ssh', '-v', '-n' |  | ||||||
|   , '-o', 'Batchmode=yes' |  | ||||||
|   , '-o', 'StrictHostKeyChecking=no' |  | ||||||
|   , '-o', 'UserKnownHostsFile=' + nullfile |  | ||||||
|   , user + '@localhost' |  | ||||||
|   , '| true' |  | ||||||
|   ]; |  | ||||||
|   return new PromiseA(function (resolve) { |  | ||||||
|     // not using promisify because all 3 arguments convey information
 |  | ||||||
|     exec(args.join(' '), function (err, stdout, stderr) { |  | ||||||
|       stdout = (stdout||'').toString('utf8'); |  | ||||||
|       stderr = (stderr||'').toString('utf8'); |  | ||||||
|       if (/\bpassword\b/.test(stdout) || /\bpassword\b/.test(stderr)) { |  | ||||||
|         resolve('yes'); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       if (/\bAuthentications\b/.test(stdout) || /\bAuthentications\b/.test(stderr)) { |  | ||||||
|         resolve('no'); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       resolve('maybe'); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports.checkSecurity = function () { |  | ||||||
|   var conf = {}; |  | ||||||
|   var noRootPasswordRe = /(?:^|[\r\n]+)\s*PermitRootLogin\s+(prohibit-password|without-password|no)\s*/i; |  | ||||||
|   var noPasswordRe = /(?:^|[\r\n]+)\s*PasswordAuthentication\s+(no)\s*/i; |  | ||||||
|   var sshdConf = '/etc/ssh/sshd_config'; |  | ||||||
|   if (/^win/i.test(process.platform)) { |  | ||||||
|     // TODO use %PROGRAMDATA%\ssh\sshd_config
 |  | ||||||
|     sshdConf = 'C:\\ProgramData\\ssh\\sshd_config'; |  | ||||||
|   } |  | ||||||
|   return readFile(sshdConf, null).then(function (sshd) { |  | ||||||
|     sshd = sshd.toString('utf8'); |  | ||||||
|     var match; |  | ||||||
|     match = sshd.match(noRootPasswordRe); |  | ||||||
|     conf.permit_root_login = match ? match[1] : 'yes'; |  | ||||||
|     match = sshd.match(noPasswordRe); |  | ||||||
|     conf.password_authentication = match ? match[1] : 'yes'; |  | ||||||
|   }).catch(function () { |  | ||||||
|     // ignore error as that might not be the correct sshd_config location
 |  | ||||||
|   }).then(function () { |  | ||||||
|     var doesntExist = crypto.randomBytes(16).toString('hex'); |  | ||||||
|     return sshAllowsPassword(doesntExist).then(function (maybe) { |  | ||||||
|       conf.requests_password = maybe; |  | ||||||
|     }); |  | ||||||
|   }).then(function () { |  | ||||||
|     return conf; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| if (require.main === module) { |  | ||||||
|   module.exports.checkSecurity().then(function (conf) { |  | ||||||
|     console.log(conf); |  | ||||||
|     return conf; |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
							
								
								
									
										1124
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1124
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							| @ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
|   "name": "telebit", |   "name": "telebit", | ||||||
|   "version": "0.21.0-wip.1", |   "version": "0.20.251025", | ||||||
|   "description": "Break out of localhost. Connect to any device from anywhere over any tcp port or securely in a browser. A secure tunnel. A poor man's reverse VPN.", |   "description": "Break out of localhost. Connect to any device from anywhere over any tcp port or securely in a browser. A secure tunnel. A poor man's reverse VPN.", | ||||||
|   "main": "lib/daemon/index.js", |   "main": "lib/remote.js", | ||||||
|   "files": [ |   "files": [ | ||||||
|     "bin", |     "bin", | ||||||
|     "lib", |     "lib", | ||||||
| @ -55,10 +55,9 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@coolaj86/urequest": "^1.3.5", |     "@coolaj86/urequest": "^1.3.5", | ||||||
|     "finalhandler": "^1.1.1", |     "finalhandler": "^1.1.1", | ||||||
|     "greenlock": "^2.6.7", |     "greenlock": "^2.3.1", | ||||||
|     "js-yaml": "^3.11.0", |     "js-yaml": "^3.11.0", | ||||||
|     "keyfetch": "^1.1.8", |     "jsonwebtoken": "^7.1.9", | ||||||
|     "keypairs": "^1.2.14", |  | ||||||
|     "mkdirp": "^0.5.1", |     "mkdirp": "^0.5.1", | ||||||
|     "proxy-packer": "^2.0.2", |     "proxy-packer": "^2.0.2", | ||||||
|     "ps-list": "^5.0.0", |     "ps-list": "^5.0.0", | ||||||
| @ -73,9 +72,6 @@ | |||||||
|     "toml": "^0.4.1", |     "toml": "^0.4.1", | ||||||
|     "ws": "^6.0.0" |     "ws": "^6.0.0" | ||||||
|   }, |   }, | ||||||
|   "optionalDependencies": { |  | ||||||
|     "keytar": "^4.4.1" |  | ||||||
|   }, |  | ||||||
|   "trulyOptionalDependencies": { |   "trulyOptionalDependencies": { | ||||||
|     "bluebird": "^3.5.1" |     "bluebird": "^3.5.1" | ||||||
|   }, |   }, | ||||||
|  | |||||||
| @ -34,9 +34,9 @@ ExecReload=/bin/kill -USR1 $MAINPID | |||||||
| # Use private /tmp and /var/tmp, which are discarded after this stops. | # Use private /tmp and /var/tmp, which are discarded after this stops. | ||||||
| PrivateTmp=true | PrivateTmp=true | ||||||
| # Use a minimal /dev | # Use a minimal /dev | ||||||
| ;PrivateDevices=true | PrivateDevices=true | ||||||
| # Hide /home, /root, and /run/user. Nobody will steal your SSH-keys. | # Hide /home, /root, and /run/user. Nobody will steal your SSH-keys. | ||||||
| ProtectHome=true | ;ProtectHome=true | ||||||
| # Make /usr, /boot, /etc and possibly some more folders read-only. | # Make /usr, /boot, /etc and possibly some more folders read-only. | ||||||
| ProtectSystem=full | ProtectSystem=full | ||||||
| # ... except for a few because we want a place for config, logs, etc | # ... except for a few because we want a place for config, logs, etc | ||||||
| @ -61,4 +61,5 @@ NoNewPrivileges=true | |||||||
| ; NoNewPrivileges=true | ; NoNewPrivileges=true | ||||||
| 
 | 
 | ||||||
| [Install] | [Install] | ||||||
| WantedBy=multi-user.target | # For userspace service | ||||||
|  | WantedBy=default.target | ||||||
|  | |||||||
| @ -62,6 +62,6 @@ NoNewPrivileges=true | |||||||
| 
 | 
 | ||||||
| [Install] | [Install] | ||||||
| # For system-level service | # For system-level service | ||||||
| ;WantedBy=multi-user.target | WantedBy=multi-user.target | ||||||
| # For userspace service | # For userspace service | ||||||
| WantedBy=default.target | ;WantedBy=default.target | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ Launcher._killAll = function (fn) { | |||||||
|   var psList = require('ps-list'); |   var psList = require('ps-list'); | ||||||
|   psList().then(function (procs) { |   psList().then(function (procs) { | ||||||
|     procs.forEach(function (proc) { |     procs.forEach(function (proc) { | ||||||
|       if ('node' === proc.name && /\btelebit(d| daemon)\b/i.test(proc.cmd)) { |       if ('node' === proc.name && /\btelebitd\b/i.test(proc.cmd)) { | ||||||
|         console.log(proc); |         console.log(proc); | ||||||
|         process.kill(proc.pid); |         process.kill(proc.pid); | ||||||
|         return true; |         return true; | ||||||
| @ -45,7 +45,37 @@ Launcher._detect = function (things, fn) { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   require('./which.js').launcher(things._execOpts, fn); |   // could have used "command-exists" but I'm trying to stay low-dependency
 | ||||||
|  |   // os.platform(), os.type()
 | ||||||
|  |   if (!/^win/i.test(os.platform())) { | ||||||
|  |     if (/^darwin/i.test(os.platform())) { | ||||||
|  |       exec('command -v launchctl', things._execOpts, function (err, stdout, stderr) { | ||||||
|  |         err = Launcher._getError(err, stderr); | ||||||
|  |         fn(err, 'launchctl'); | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       exec('command -v systemctl', things._execOpts, function (err, stdout, stderr) { | ||||||
|  |         err = Launcher._getError(err, stderr); | ||||||
|  |         fn(err, 'systemctl'); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     // https://stackoverflow.com/questions/17908789/how-to-add-an-item-to-registry-to-run-at-startup-without-uac
 | ||||||
|  |     // wininit? regedit? SCM?
 | ||||||
|  |     // REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /V "My App" /t REG_SZ /F /D "C:\MyAppPath\MyApp.exe"
 | ||||||
|  |     // https://www.microsoft.com/developerblog/2015/11/09/reading-and-writing-to-the-windows-registry-in-process-from-node-js/
 | ||||||
|  |     // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-add
 | ||||||
|  |     // https://social.msdn.microsoft.com/Forums/en-US/5b318f44-281e-4098-8dee-3ba8435fa391/add-registry-key-for-autostart-of-app-in-ice?forum=quebectools
 | ||||||
|  |     // utils.elevate
 | ||||||
|  |     // https://github.com/CatalystCode/windows-registry-node
 | ||||||
|  |     exec('where reg.exe', things._execOpts, function (err, stdout, stderr) { | ||||||
|  |       //console.log((stdout||'').trim());
 | ||||||
|  |       if (stderr) { | ||||||
|  |         console.error(stderr); | ||||||
|  |       } | ||||||
|  |       fn(err, 'reg.exe'); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
| }; | }; | ||||||
| Launcher.install = function (things, fn) { | Launcher.install = function (things, fn) { | ||||||
|   if (!fn) { fn = function (err) { if (err) { console.error(err); } }; } |   if (!fn) { fn = function (err) { if (err) { console.error(err); } }; } | ||||||
|  | |||||||
| @ -133,8 +133,11 @@ my_tmp="$(mktemp -d -t telebit.XXXXXXXX)" | |||||||
| #TELEBIT_TMP="$my_tmp/telebit" | #TELEBIT_TMP="$my_tmp/telebit" | ||||||
| 
 | 
 | ||||||
| echo "Installing $my_name to '$TELEBIT_REAL_PATH'" | echo "Installing $my_name to '$TELEBIT_REAL_PATH'" | ||||||
| # v10.2+ has much needed networking fixes, but breaks ursa. v9.x has severe networking bugs. v8.x has working ursa, but requires tls workarounds" | # v10.2+ has much needed networking fixes, but breaks ursa. | ||||||
| NODEJS_VER="${NODEJS_VER:-v10.6}" | # v9.x has severe networking bugs. | ||||||
|  | # v8.x has working ursa, but requires tls workarounds" | ||||||
|  | # v10.13 seems to work for me locally (new greenlock) | ||||||
|  | NODEJS_VER="${NODEJS_VER:-v10.13}" | ||||||
| export NODEJS_VER | export NODEJS_VER | ||||||
| export NODE_PATH="$TELEBIT_TMP/lib/node_modules" | export NODE_PATH="$TELEBIT_TMP/lib/node_modules" | ||||||
| export NPM_CONFIG_PREFIX="$TELEBIT_TMP" | export NPM_CONFIG_PREFIX="$TELEBIT_TMP" | ||||||
| @ -493,23 +496,30 @@ elif [ "systemd" == "$my_system_launcher" ]; then | |||||||
|     else |     else | ||||||
|       echo -n "." |       echo -n "." | ||||||
|     fi |     fi | ||||||
|     systemctl --user daemon-reload |     set +e | ||||||
|     # enable also puts success output to stderr... why? |     if systemctl --user daemon-reload; then | ||||||
|     systemctl --user enable $my_app >/dev/null 2>/dev/null |       # enable also puts success output to stderr... why? | ||||||
|     #echo "    > systemctl --user enable systemd-tmpfiles-setup.service systemd-tmpfiles-clean.timer" |       systemctl --user enable $my_app >/dev/null 2>/dev/null | ||||||
|     #systemctl --user enable systemd-tmpfiles-setup.service systemd-tmpfiles-clean.timer |       #echo "    > systemctl --user enable systemd-tmpfiles-setup.service systemd-tmpfiles-clean.timer" | ||||||
|     if [ -n "${TELEBIT_DEBUG}" ]; then |       #systemctl --user enable systemd-tmpfiles-setup.service systemd-tmpfiles-clean.timer | ||||||
|       echo "    > systemctl --user start $my_app" |       if [ -n "${TELEBIT_DEBUG}" ]; then | ||||||
|     fi |         echo "    > systemctl --user start $my_app" | ||||||
|     systemctl --user stop $my_app >/dev/null 2>/dev/null |       fi | ||||||
|     systemctl --user start $my_app >/dev/null |       systemctl --user stop $my_app >/dev/null 2>/dev/null | ||||||
|     sleep 2; # give it time to start |       systemctl --user start $my_app >/dev/null | ||||||
|     _is_running=$(systemctl --user status --no-pager $my_app 2>/dev/null | grep "active.*running") | 
 | ||||||
|     if [ -z "$_is_running" ]; then |       sleep 2; # give it time to start | ||||||
|       echo "Something went wrong:" |       _is_running=$(systemctl --user status --no-pager $my_app 2>/dev/null | grep "active.*running") | ||||||
|       systemctl --user status --no-pager $my_app |       if [ -z "$_is_running" ]; then | ||||||
|       exit 1 |         echo "Something went wrong:" | ||||||
|  |         systemctl --user status --no-pager $my_app | ||||||
|  |       fi | ||||||
|  |     else | ||||||
|  |       echo "libpam-systemd is missing, which is required on Linux to register Telebit with the user launcher." | ||||||
|  |       echo "sudo apt-get install -y libpam-systemd" | ||||||
|  |       sudo apt-get install -y libpam-systemd | ||||||
|     fi |     fi | ||||||
|  |     set -e | ||||||
|     echo -n "." |     echo -n "." | ||||||
|   else |   else | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,63 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var os = require('os'); |  | ||||||
| var exec = require('child_process').exec; |  | ||||||
| 
 |  | ||||||
| var which = module.exports; |  | ||||||
| 
 |  | ||||||
| which._getError = function getError(err, stderr) { |  | ||||||
|   if (err) { return err; } |  | ||||||
|   if (stderr) { |  | ||||||
|     err = new Error(stderr); |  | ||||||
|     err.code = 'EWHICH'; |  | ||||||
|     return err; |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| module.exports.which = function (cmd, execOpts, fn) { |  | ||||||
|   return module.exports._which({ |  | ||||||
|     mac: cmd |  | ||||||
|   , linux: cmd |  | ||||||
|   , win: cmd |  | ||||||
|   }, execOpts, fn); |  | ||||||
| }; |  | ||||||
| module.exports.launcher = function (execOpts, fn) { |  | ||||||
|   return module.exports._which({ |  | ||||||
|     mac: 'launchctl' |  | ||||||
|   , linux: 'systemctl' |  | ||||||
|   , win: 'reg.exe' |  | ||||||
|   }, execOpts, fn); |  | ||||||
| }; |  | ||||||
| module.exports._which = function (progs, execOpts, fn) { |  | ||||||
|   // could have used "command-exists" but I'm trying to stay low-dependency
 |  | ||||||
|   // os.platform(), os.type()
 |  | ||||||
|   if (!/^win/i.test(os.platform())) { |  | ||||||
|     if (/^darwin/i.test(os.platform())) { |  | ||||||
|       exec('command -v ' + progs.mac, execOpts, function (err, stdout, stderr) { |  | ||||||
|         err = which._getError(err, stderr); |  | ||||||
|         fn(err, progs.mac); |  | ||||||
|       }); |  | ||||||
|     } else { |  | ||||||
|       exec('command -v ' + progs.linux, execOpts, function (err, stdout, stderr) { |  | ||||||
|         err = which._getError(err, stderr); |  | ||||||
|         fn(err, progs.linux); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } else { |  | ||||||
|     // https://stackoverflow.com/questions/17908789/how-to-add-an-item-to-registry-to-run-at-startup-without-uac
 |  | ||||||
|     // wininit? regedit? SCM?
 |  | ||||||
|     // REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /V "My App" /t REG_SZ /F /D "C:\MyAppPath\MyApp.exe"
 |  | ||||||
|     // https://www.microsoft.com/developerblog/2015/11/09/reading-and-writing-to-the-windows-registry-in-process-from-node-js/
 |  | ||||||
|     // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-add
 |  | ||||||
|     // https://social.msdn.microsoft.com/Forums/en-US/5b318f44-281e-4098-8dee-3ba8435fa391/add-registry-key-for-autostart-of-app-in-ice?forum=quebectools
 |  | ||||||
|     // utils.elevate
 |  | ||||||
|     // https://github.com/CatalystCode/windows-registry-node
 |  | ||||||
|     exec('where ' + progs.win, execOpts, function (err, stdout, stderr) { |  | ||||||
|       //console.log((stdout||'').trim());
 |  | ||||||
|       if (stderr) { |  | ||||||
|         console.error(stderr); |  | ||||||
|       } |  | ||||||
|       fn(err, progs.win); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user