MAJOR: Updates for Authenticated Web UI and CLI #30
| @ -80,6 +80,7 @@ | |||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|   <script src="/js/vue.js"></script> |   <script src="/js/vue.js"></script> | ||||||
|  |   <script src="/js/telebit.js"></script> | ||||||
|   <script src="/js/app.js"></script> |   <script src="/js/app.js"></script> | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ | |||||||
| console.log("hello"); | console.log("hello"); | ||||||
| 
 | 
 | ||||||
| var Vue = window.Vue; | var Vue = window.Vue; | ||||||
|  | var Telebit = window.TELEBIT; | ||||||
| var api = {}; | var api = {}; | ||||||
| 
 | 
 | ||||||
| api.config = function apiConfig() { | api.config = function apiConfig() { | ||||||
| @ -53,6 +54,13 @@ var appMethods = { | |||||||
|     if (DEFAULT_RELAY !== appData.init.relay) { |     if (DEFAULT_RELAY !== appData.init.relay) { | ||||||
|       window.alert("TODO: Custom Relay Not Implemented Yet"); |       window.alert("TODO: Custom Relay Not Implemented Yet"); | ||||||
|     } |     } | ||||||
|  |     Telebit.api.directory({ relay: appData.init.relay }, function (err, dir) { | ||||||
|  |       if (err) { | ||||||
|  |         window.alert("Error:" + (err.message || JSON.stringify(err, null, 2))); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       window.alert("Success:" + JSON.stringify(dir, null, 2)); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| , defaultRelay: function () { | , defaultRelay: function () { | ||||||
|     appData.init.relay = DEFAULT_RELAY; |     appData.init.relay = DEFAULT_RELAY; | ||||||
|  | |||||||
							
								
								
									
										247
									
								
								lib/admin/js/telebit.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								lib/admin/js/telebit.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,247 @@ | |||||||
|  | ;(function (exports) { | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var common = exports.TELEBIT = {}; | ||||||
|  | 
 | ||||||
|  | if ('undefined' !== typeof fetch) { | ||||||
|  |   common.requestAsync = function (opts) { | ||||||
|  |     if (opts.json && true !== opts.json) { | ||||||
|  |       opts.body = opts.json; | ||||||
|  |     } | ||||||
|  |     if (opts.json) { | ||||||
|  |       if (!opts.headers) { opts.headers = {}; } | ||||||
|  |       if (opts.body) { | ||||||
|  |         opts.headers['Content-Type'] = 'application/json'; | ||||||
|  |       } else { | ||||||
|  |         opts.headers.Accepts = 'application/json'; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return window.fetch(opts.url, opts).then(function (resp) { | ||||||
|  |       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.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 = new URL(hostname); | ||||||
|  |   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('jsonwebtoken'); | ||||||
|  |   var tokenData = { | ||||||
|  |     domains: Object.keys(state.config.servernames || {}).filter(function (name) { | ||||||
|  |       return /\./.test(name); | ||||||
|  |     }) | ||||||
|  |   , ports: Object.keys(state.config.ports || {}).filter(function (port) { | ||||||
|  |       port = parseInt(port, 10); | ||||||
|  |       return port > 0 && port <= 65535; | ||||||
|  |     }) | ||||||
|  |   , aud: state._relayUrl | ||||||
|  |   , iss: Math.round(Date.now() / 1000) | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return jwt.sign(tokenData, state.config.secret); | ||||||
|  | }; | ||||||
|  | common.api = {}; | ||||||
|  | common.api.directory = function (state, next) { | ||||||
|  |   console.log('state:'); | ||||||
|  |   console.log(state); | ||||||
|  |   state._relayUrl = common.parseUrl(state.relay); | ||||||
|  |   common.requestAsync({ url: state._relayUrl + common.apiDirectory, json: true }).then(function (resp) { | ||||||
|  |     var dir = resp.body; | ||||||
|  |     if (!dir) { dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; } | ||||||
|  |     state._apiDirectory = dir; | ||||||
|  |     next(null, dir); | ||||||
|  |   }).catch(function (err) { | ||||||
|  |     next(err); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 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, cb) { | ||||||
|  |   common.api.directory(state, function (err, dir) { | ||||||
|  |     cb(err, common.api._parseWss(state, dir)); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | common.api.token = function (state, handlers) { | ||||||
|  |   common.api.directory(state, function (err, dir) { | ||||||
|  |     // directory, requested, connect, tunnelUrl, offer, granted, end
 | ||||||
|  |     function afterDir() { | ||||||
|  |       if (common.debug) { console.log('[debug] after dir'); } | ||||||
|  |       state.wss = common.api._parseWss(state, dir); | ||||||
|  | 
 | ||||||
|  |       handlers.tunnelUrl(state.wss, function () { | ||||||
|  |         if (common.debug) { console.log('[debug] after tunnelUrl'); } | ||||||
|  |         if (state.config.secret /* && !state.config.token */) { | ||||||
|  |           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 }
 | ||||||
|  |           handlers.connect(state.token, function () { | ||||||
|  |             handlers.end(null, function () {}); | ||||||
|  |           }); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // backwards compat (TODO remove)
 | ||||||
|  |         if (err || !dir || !dir.pair_request) { | ||||||
|  |           if (common.debug) { console.log('[debug] no dir, connect'); } | ||||||
|  |           handlers.error(new Error("No token found or generated, and no pair_request api found.")); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // TODO sign token with own private key, including public key and thumbprint
 | ||||||
|  |         //      (much like ACME JOSE account)
 | ||||||
|  |         var otp = state.config._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)); | ||||||
|  |         var req = { | ||||||
|  |           url: pairRequestUrl | ||||||
|  |         , method: dir.pair_request.method | ||||||
|  |         , json: authReq | ||||||
|  |         }; | ||||||
|  |         var firstReq = true; | ||||||
|  |         var firstReady = true; | ||||||
|  | 
 | ||||||
|  |         function gotoNext(req) { | ||||||
|  |           if (common.debug) { console.log('[debug] gotoNext called'); } | ||||||
|  |           if (common.debug) { console.log(req); } | ||||||
|  |           common.requestAsync(req).then(function (resp) { | ||||||
|  |             var body = resp.body; | ||||||
|  | 
 | ||||||
|  |             function checkLocation() { | ||||||
|  |               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'); } | ||||||
|  |                 setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); | ||||||
|  |                 return; | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               if ('ready' === body.status) { | ||||||
|  |                 if (common.debug) { console.log('[debug] ready'); } | ||||||
|  |                 if (firstReady) { | ||||||
|  |                   if (common.debug) { console.log('[debug] first ready'); } | ||||||
|  |                   firstReady = false; | ||||||
|  |                   state.token = body.access_token; | ||||||
|  |                   state.config.token = state.token; | ||||||
|  |                   handlers.offer(body.access_token, function () { | ||||||
|  |                     /*ignore*/ | ||||||
|  |                   }); | ||||||
|  |                 } | ||||||
|  |                 setTimeout(gotoNext, 2 * 1000, req); | ||||||
|  |                 return; | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               if ('complete' === body.status) { | ||||||
|  |                 if (common.debug) { console.log('[debug] complete'); } | ||||||
|  |                 handlers.granted(null, function () { | ||||||
|  |                   handlers.end(null, function () {}); | ||||||
|  |                 }); | ||||||
|  |                 return; | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               if (common.debug) { console.log('[debug] bad status'); } | ||||||
|  |               var err = new Error("Bad State:" + body.status); | ||||||
|  |               err._request = req; | ||||||
|  |               handlers.error(err, function () {}); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (firstReq) { | ||||||
|  |               if (common.debug) { console.log('[debug] first req'); } | ||||||
|  |               handlers.requested(authReq, function () { | ||||||
|  |                 handlers.connect(body.access_token || body.jwt, function () { | ||||||
|  |                   var err; | ||||||
|  |                   if (!resp.headers.location) { | ||||||
|  |                     err = new Error("bad authentication request response"); | ||||||
|  |                     err._resp = resp.toJSON && resp.toJSON(); | ||||||
|  |                     handlers.error(err, function () {}); | ||||||
|  |                     return; | ||||||
|  |                   } | ||||||
|  |                   setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); | ||||||
|  |                 }); | ||||||
|  |               }); | ||||||
|  |               firstReq = false; | ||||||
|  |               return; | ||||||
|  |             } else { | ||||||
|  |               if (common.debug) { console.log('[debug] other req'); } | ||||||
|  |               checkLocation(); | ||||||
|  |             } | ||||||
|  |           }).catch(function (err) { | ||||||
|  |             if (common.debug) { console.log('[debug] gotoNext error'); } | ||||||
|  |             err._request = req; | ||||||
|  |             err._hint = '[telebitd.js] pair request'; | ||||||
|  |             handlers.error(err, function () {}); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         gotoNext(req); | ||||||
|  | 
 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (dir && dir.api_host) { | ||||||
|  |       handlers.directory(dir, afterDir); | ||||||
|  |     } else { | ||||||
|  |       // backwards compat
 | ||||||
|  |       dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; | ||||||
|  |       afterDir(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  | }; | ||||||
|  | }('undefined' !== typeof module ? module.exports : window)); | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user