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) { | function jsonEggspress(req, res, next) { | ||||||
|   /* |   /* | ||||||
| @ -1064,6 +1085,10 @@ function handleApi() { | |||||||
| 
 | 
 | ||||||
|     next(); |     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.use(/\b(relay)\b/, mustTrust, controllers.relay); | ||||||
|   app.get(/\b(config)\b/, mustTrust, getConfigOnly); |   app.get(/\b(config)\b/, mustTrust, getConfigOnly); | ||||||
|   app.use(/\b(init|config)\b/, mustTrust, initOrConfig); |   app.use(/\b(init|config)\b/, mustTrust, initOrConfig); | ||||||
|  | |||||||
| @ -186,29 +186,31 @@ var telebitState = {}; | |||||||
| var appMethods = { | var appMethods = { | ||||||
|   initialize: function () { |   initialize: function () { | ||||||
|     console.log("call initialize"); |     console.log("call initialize"); | ||||||
|     if (!appData.init.relay) { |     return requestAccountHelper().then(function (/*key*/) { | ||||||
|       appData.init.relay = DEFAULT_RELAY; |       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; |  | ||||||
|       } |       } | ||||||
|  |       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
 |         telebitState.dir = dir; | ||||||
|       if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) { | 
 | ||||||
|         return doConfigure(); |         // If it's one of the well-known relays
 | ||||||
|       } else { |         if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) { | ||||||
|         changeState('advanced'); |           return doConfigure(); | ||||||
|       } |         } else { | ||||||
|     }).catch(function (err) { |           changeState('advanced'); | ||||||
|       console.error(err); |         } | ||||||
|       window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2))); |       }).catch(function (err) { | ||||||
|  |         console.error(err); | ||||||
|  |         window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2))); | ||||||
|  |       }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| , advance: function () { | , advance: function () { | ||||||
| @ -473,54 +475,84 @@ new Vue({ | |||||||
| , methods: appMethods | , methods: appMethods | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| function run(key) { | function requestAccountHelper() { | ||||||
|   api._key = key; |   function reset() { | ||||||
|   // 😁 1. Get ACME directory
 |     changeState('setup'); | ||||||
|   // 😁 2. Fetch ACME account
 |     setState(); | ||||||
|   // 3. Test if account has access
 |   } | ||||||
|   // 4. Show command line auth instructions to auth
 |   return new Promise(function (resolve) { | ||||||
|   // 5. Sign requests / use JWT
 |     appData.init.email = localStorage.getItem('email'); | ||||||
|   // 6. Enforce token required for config, status, etc
 |     if (!appData.init.email) { | ||||||
|   // 7. Move admin interface to standard ports (admin.foo-bar-123.telebit.xyz)
 |       // don't resolve
 | ||||||
|   api.config().then(function (config) { |       reset(); | ||||||
|     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; |       return; | ||||||
|     } |     } | ||||||
|     if (!config.token && config._otp) { |     return requestAccount(appData.init.email).then(function (key) { | ||||||
|       changeState('otp'); |       if (!key) { throw new Error("[SANITY] Error: completed without key"); } | ||||||
|       setState(); |       resolve(key); | ||||||
|       // this will skip ahead as necessary
 |     }).catch(function (err) { | ||||||
|       return Telebit.authorize(telebitState, showOtp).then(function () { |       appData.init.email = ""; | ||||||
|         return changeState('status'); |       localStorage.removeItem('email'); | ||||||
|       }); |       console.error(err); | ||||||
|     } |       window.alert("something went wrong"); | ||||||
|  |       // don't resolve
 | ||||||
|  |       reset(); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     // TODO handle default state
 | function run() { | ||||||
|     changeState('status'); |   return requestAccountHelper().then(function (key) { | ||||||
|   }).catch(function (err) { |     api._key = key; | ||||||
|     appData.views.flash.error = err.message || JSON.stringify(err, null, 2); |     // 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() { | function requestAccount(email) { | ||||||
|   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() { |  | ||||||
|   return getKey().then(function (jwk) { |   return getKey().then(function (jwk) { | ||||||
|     return getEmail().then(function(email) { |     // creates new or returns existing
 | ||||||
|       // creates new or returns existing
 |     var acme = ACME.create({}); | ||||||
|       var acme = ACME.create({}); |     var url = window.location.protocol + '//' + window.location.host + '/acme/directory'; | ||||||
|       var url = window.location.protocol + '//' + window.location.host + '/acme/directory'; |     return acme.init(url).then(function () { | ||||||
|       return acme.init(url).then(function () { |       return acme.accounts.create({ | ||||||
|         return acme.accounts.create({ |         agreeToTerms: function (tos) { return tos; } | ||||||
|           agreeToTerms: function (tos) { return tos; } |       , accountKeypair: { privateKeyJwk: jwk } | ||||||
|         , accountKeypair: { privateKeyJwk: jwk } |       , email: email | ||||||
|         , email: email |       }).then(function (account) { | ||||||
|         }).then(function (account) { |         console.log('account:'); | ||||||
|           console.log('account:'); |         console.log(account); | ||||||
|           console.log(account); |         if (account.id) { | ||||||
|           if (account.id) { |           localStorage.setItem('email', email); | ||||||
|             localStorage.setItem('email', email); |         } | ||||||
|           } |         return jwk; | ||||||
|           return jwk; |  | ||||||
|         }); |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| window.api = api; | window.api = api; | ||||||
| requestAccount().then(function (jwk) { | run(); | ||||||
|   run(jwk); | setTimeout(function () { | ||||||
|   setTimeout(function () { |   document.body.hidden = false; | ||||||
|     document.body.hidden = false; | }, 50); | ||||||
|   }, 50); |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
|  | // Debug
 | ||||||
|  | window.changeState = changeState; | ||||||
| }()); | }()); | ||||||
|  | |||||||
| @ -34,11 +34,12 @@ module.exports = function eggspress() { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       var urlstr = (req.url.replace(/\/$/, '') + '/'); |       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);
 |         //console.log("[eggspress] pattern doesn't match", todo[0], req.url);
 | ||||||
|         next(); |         next(); | ||||||
|         return; |         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);
 |         //console.log("[eggspress] string pattern is not the start", todo[0], req.url);
 | ||||||
|         next(); |         next(); | ||||||
|         return; |         return; | ||||||
| @ -58,6 +59,7 @@ module.exports = function eggspress() { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       var fns = todo[1].slice(0); |       var fns = todo[1].slice(0); | ||||||
|  |       req.params = match.slice(1); | ||||||
| 
 | 
 | ||||||
|       function nextTodo(err) { |       function nextTodo(err) { | ||||||
|         if (err) { fail(err); return; } |         if (err) { fail(err); return; } | ||||||
|  | |||||||
| @ -93,26 +93,45 @@ module.exports.create = function (state) { | |||||||
|     } |     } | ||||||
|     return reqOpts; |     return reqOpts; | ||||||
|   }; |   }; | ||||||
|   RC.createErrorHandler = function (replay, opts, cb) { |   RC.createRelauncher = function (replay, opts, cb) { | ||||||
|     return function (err) { |     return function (err) { | ||||||
|       // ENOENT - never started, cleanly exited last start, or creating socket at a different path
 |       /*global Promise*/ | ||||||
|       // ECONNREFUSED - leftover socket just needs to be restarted
 |       var p = new Promise(function (resolve, reject) { | ||||||
|       if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { |         // ENOENT - never started, cleanly exited last start, or creating socket at a different path
 | ||||||
|         if (opts._taketwo) { |         // ECONNREFUSED - leftover socket just needs to be restarted
 | ||||||
|           cb(err); |         if ('ENOENT' !== err.code && 'ECONNREFUSED' !== err.code) { | ||||||
|  |           reject(err); | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         // retried and failed again: quit
 | ||||||
|  |         if (opts._taketwo) { | ||||||
|  |           reject(err); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) { |         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; |           opts._taketwo = true; | ||||||
|           setTimeout(function () { |           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); |           }, 2500); | ||||||
|         }); |         }); | ||||||
|         return; |         return; | ||||||
|  |       }); | ||||||
|  |       if (cb) { | ||||||
|  |         p.then(function () { cb(null); }).catch(function (err) { cb(err); }); | ||||||
|       } |       } | ||||||
| 
 |       return p; | ||||||
|       cb(err); |  | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|   RC.request = function request(opts, fn) { |   RC.request = function request(opts, fn) { | ||||||
| @ -141,7 +160,8 @@ module.exports.create = function (state) { | |||||||
|       makeResponder(service, resp, fn); |       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
 |     // Simple GET
 | ||||||
|     if ('POST' !== method || !opts.data) { |     if ('POST' !== method || !opts.data) { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user