MAJOR: Updates for Authenticated Web UI and CLI #30
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -525,6 +525,27 @@ controllers.newAccount = function (req, res) { | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| controllers.acmeAccounts = function (req, res) { | ||||
|   if (!req.jws || !req.jws.verified) { | ||||
|     res.statusCode = 400; | ||||
|     res.send({"error":{"message": "this type of requests must be encoded as a jws payload" | ||||
|       + " and signed by a known account holder"}}); | ||||
|     return; | ||||
|   } | ||||
|   var account; | ||||
|   var accountId = req.params[0]; | ||||
|   DB.accounts.some(function (acc) { | ||||
|     // TODO calculate thumbprint from jwk
 | ||||
|     // find a key with matching jwk
 | ||||
|     if (acc._id === accountId) { | ||||
|       account = acc; | ||||
|       return true; | ||||
|     } | ||||
|   }); | ||||
|   // TODO check that the JWS matches the accountI
 | ||||
|   console.warn("[warn] account ID still acts as secret, should use JWS kid for verification"); | ||||
|   res.send(account); | ||||
| }; | ||||
| 
 | ||||
| function jsonEggspress(req, res, next) { | ||||
|   /* | ||||
| @ -1064,6 +1085,10 @@ function handleApi() { | ||||
| 
 | ||||
|     next(); | ||||
|   } | ||||
|   // TODO convert /acme/accounts/:account_id into a regex
 | ||||
|   app.get(/^\/acme\/accounts\/([\w]+)/, controllers.acmeAccounts); | ||||
|   // POST-as-GET
 | ||||
|   app.post(/^\/acme\/accounts\/([\w]+)/, controllers.acmeAccounts); | ||||
|   app.use(/\b(relay)\b/, mustTrust, controllers.relay); | ||||
|   app.get(/\b(config)\b/, mustTrust, getConfigOnly); | ||||
|   app.use(/\b(init|config)\b/, mustTrust, initOrConfig); | ||||
|  | ||||
| @ -186,29 +186,31 @@ 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; | ||||
|     return requestAccountHelper().then(function (/*key*/) { | ||||
|       if (!appData.init.relay) { | ||||
|         appData.init.relay = DEFAULT_RELAY; | ||||
|       } | ||||
|       appData.init.relay = appData.init.relay.toLowerCase(); | ||||
|       telebitState = { relay: appData.init.relay }; | ||||
| 
 | ||||
|       telebitState.dir = dir; | ||||
|       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; | ||||
|         } | ||||
| 
 | ||||
|       // 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))); | ||||
|         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 () { | ||||
| @ -473,54 +475,84 @@ new Vue({ | ||||
| , methods: appMethods | ||||
| }); | ||||
| 
 | ||||
| function run(key) { | ||||
|   api._key = key; | ||||
|   // 😁 1. Get ACME directory
 | ||||
|   // 😁 2. Fetch ACME account
 | ||||
|   // 3. Test if account has access
 | ||||
|   // 4. Show command line auth instructions to auth
 | ||||
|   // 5. Sign requests / use JWT
 | ||||
|   // 6. Enforce token required for config, status, etc
 | ||||
|   // 7. Move admin interface to standard ports (admin.foo-bar-123.telebit.xyz)
 | ||||
|   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(); | ||||
| function requestAccountHelper() { | ||||
|   function reset() { | ||||
|     changeState('setup'); | ||||
|     setState(); | ||||
|   } | ||||
|   return new Promise(function (resolve) { | ||||
|     appData.init.email = localStorage.getItem('email'); | ||||
|     if (!appData.init.email) { | ||||
|       // don't resolve
 | ||||
|       reset(); | ||||
|       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'); | ||||
|       }); | ||||
|     } | ||||
|     return requestAccount(appData.init.email).then(function (key) { | ||||
|       if (!key) { throw new Error("[SANITY] Error: completed without key"); } | ||||
|       resolve(key); | ||||
|     }).catch(function (err) { | ||||
|       appData.init.email = ""; | ||||
|       localStorage.removeItem('email'); | ||||
|       console.error(err); | ||||
|       window.alert("something went wrong"); | ||||
|       // don't resolve
 | ||||
|       reset(); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
|     // TODO handle default state
 | ||||
|     changeState('status'); | ||||
|   }).catch(function (err) { | ||||
|     appData.views.flash.error = err.message || JSON.stringify(err, null, 2); | ||||
| function run() { | ||||
|   return requestAccountHelper().then(function (key) { | ||||
|     api._key = key; | ||||
|     // TODO create session instance of Telebit
 | ||||
|     Telebit._key = key; | ||||
|     // 😁 1. Get ACME directory
 | ||||
|     // 😁 2. Fetch ACME account
 | ||||
|     // 3. Test if account has access
 | ||||
|     // 4. Show command line auth instructions to auth
 | ||||
|     // 😁 5. Sign requests / use JWT
 | ||||
|     // 😁 6. Enforce token required for config, status, etc
 | ||||
|     // 7. Move admin interface to standard ports (admin.foo-bar-123.telebit.xyz)
 | ||||
|     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); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| @ -543,46 +575,34 @@ function getKey() { | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function getEmail() { | ||||
|   return Promise.resolve().then(function () { | ||||
|     var email = localStorage.getItem('email'); | ||||
|     if (email) { return email; } | ||||
|     while (!email) { | ||||
|       email = window.prompt("Email address (device owner)?"); | ||||
|     } | ||||
|     return email; | ||||
|   }); | ||||
| } | ||||
| function requestAccount() { | ||||
| function requestAccount(email) { | ||||
|   return getKey().then(function (jwk) { | ||||
|     return getEmail().then(function(email) { | ||||
|       // creates new or returns existing
 | ||||
|       var acme = ACME.create({}); | ||||
|       var url = window.location.protocol + '//' + window.location.host + '/acme/directory'; | ||||
|       return acme.init(url).then(function () { | ||||
|         return acme.accounts.create({ | ||||
|           agreeToTerms: function (tos) { return tos; } | ||||
|         , accountKeypair: { privateKeyJwk: jwk } | ||||
|         , email: email | ||||
|         }).then(function (account) { | ||||
|           console.log('account:'); | ||||
|           console.log(account); | ||||
|           if (account.id) { | ||||
|             localStorage.setItem('email', email); | ||||
|           } | ||||
|           return jwk; | ||||
|         }); | ||||
|     // creates new or returns existing
 | ||||
|     var acme = ACME.create({}); | ||||
|     var url = window.location.protocol + '//' + window.location.host + '/acme/directory'; | ||||
|     return acme.init(url).then(function () { | ||||
|       return acme.accounts.create({ | ||||
|         agreeToTerms: function (tos) { return tos; } | ||||
|       , accountKeypair: { privateKeyJwk: jwk } | ||||
|       , email: email | ||||
|       }).then(function (account) { | ||||
|         console.log('account:'); | ||||
|         console.log(account); | ||||
|         if (account.id) { | ||||
|           localStorage.setItem('email', email); | ||||
|         } | ||||
|         return jwk; | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| window.api = api; | ||||
| requestAccount().then(function (jwk) { | ||||
|   run(jwk); | ||||
|   setTimeout(function () { | ||||
|     document.body.hidden = false; | ||||
|   }, 50); | ||||
| }); | ||||
| run(); | ||||
| setTimeout(function () { | ||||
|   document.body.hidden = false; | ||||
| }, 50); | ||||
| 
 | ||||
| // Debug
 | ||||
| window.changeState = changeState; | ||||
| }()); | ||||
|  | ||||
| @ -34,11 +34,12 @@ module.exports = function eggspress() { | ||||
|       } | ||||
| 
 | ||||
|       var urlstr = (req.url.replace(/\/$/, '') + '/'); | ||||
|       if (!urlstr.match(todo[0])) { | ||||
|       var match = urlstr.match(todo[0]); | ||||
|       if (!match) { | ||||
|         //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) { | ||||
|       } else if ('string' === typeof todo[0] && 0 !== match.index) { | ||||
|         //console.log("[eggspress] string pattern is not the start", todo[0], req.url);
 | ||||
|         next(); | ||||
|         return; | ||||
| @ -58,6 +59,7 @@ module.exports = function eggspress() { | ||||
|       } | ||||
| 
 | ||||
|       var fns = todo[1].slice(0); | ||||
|       req.params = match.slice(1); | ||||
| 
 | ||||
|       function nextTodo(err) { | ||||
|         if (err) { fail(err); return; } | ||||
|  | ||||
| @ -93,26 +93,45 @@ module.exports.create = function (state) { | ||||
|     } | ||||
|     return reqOpts; | ||||
|   }; | ||||
|   RC.createErrorHandler = function (replay, opts, cb) { | ||||
|   RC.createRelauncher = 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); | ||||
|       /*global Promise*/ | ||||
|       var p = new Promise(function (resolve, reject) { | ||||
|         // 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) { | ||||
|           reject(err); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         // retried and failed again: quit
 | ||||
|         if (opts._taketwo) { | ||||
|           reject(err); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) { | ||||
|           if (err) { cb(err); return; } | ||||
|           if (err) { reject(err); return; } | ||||
|           opts._taketwo = true; | ||||
|           setTimeout(function () { | ||||
|             replay(opts, cb); | ||||
|             if (replay.length <= 1) { | ||||
|               replay(opts).then(resolve).catch(reject); | ||||
|               return; | ||||
|             } else { | ||||
|               replay(opts, function (err, res) { | ||||
|                 if (err) { reject(err); } | ||||
|                 else { resolve(res); } | ||||
|               }); | ||||
|               return; | ||||
|             } | ||||
|           }, 2500); | ||||
|         }); | ||||
|         return; | ||||
|       }); | ||||
|       if (cb) { | ||||
|         p.then(function () { cb(null); }).catch(function (err) { cb(err); }); | ||||
|       } | ||||
| 
 | ||||
|       cb(err); | ||||
|       return p; | ||||
|     }; | ||||
|   }; | ||||
|   RC.request = function request(opts, fn) { | ||||
| @ -141,7 +160,8 @@ module.exports.create = function (state) { | ||||
|       makeResponder(service, resp, fn); | ||||
|     }); | ||||
| 
 | ||||
|     req.on('error', RC.createErrorHandler(RC.request, opts, fn)); | ||||
|     var errHandler = RC.createRelauncher(RC.request, opts, fn); | ||||
|     req.on('error', errHandler); | ||||
| 
 | ||||
|     // Simple GET
 | ||||
|     if ('POST' !== method || !opts.data) { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user