MAJOR: Updates for Authenticated Web UI and CLI #30
| @ -1,12 +1,14 @@ | ||||
| #!/usr/bin/env node
 | ||||
| (function () { | ||||
| 'use strict'; | ||||
| /*global Promise*/ | ||||
| 
 | ||||
| var pkg = require('../package.json'); | ||||
| var os = require('os'); | ||||
| 
 | ||||
| //var url = require('url');
 | ||||
| var fs = require('fs'); | ||||
| var util = require('util'); | ||||
| var path = require('path'); | ||||
| //var https = require('https');
 | ||||
| var YAML = require('js-yaml'); | ||||
| @ -104,7 +106,9 @@ if (!confpath || /^--/.test(confpath)) { | ||||
|   process.exit(1); | ||||
| } | ||||
| 
 | ||||
| function askForConfig(state, mainCb) { | ||||
| var Console = {}; | ||||
| Console.setup = function (state) { | ||||
|   if (Console.rl) { return; } | ||||
|   var fs = require('fs'); | ||||
|   var ttyname = '/dev/tty'; | ||||
|   var stdin = useTty ? fs.createReadStream(ttyname, { | ||||
| @ -119,6 +123,34 @@ function askForConfig(state, mainCb) { | ||||
|   , terminal: !/^win/i.test(os.platform()) && !useTty | ||||
|   }); | ||||
|   state._useTty = useTty; | ||||
|   Console.rl = rl; | ||||
| }; | ||||
| Console.teardown = function () { | ||||
|   // https://github.com/nodejs/node/issues/21319
 | ||||
|   if (useTty) { try { Console.stdin.push(null); } catch(e) { /*ignore*/ } } | ||||
|   Console.rl.close(); | ||||
|   if (useTty) { try { Console.stdin.close(); } catch(e) { /*ignore*/ } } | ||||
|   Console.rl = null; | ||||
| }; | ||||
| 
 | ||||
| function askEmail(cb) { | ||||
|   Console.setup(); | ||||
|   if (state.config.email) { cb(); return; } | ||||
|   console.info(TPLS.remote.setup.email); | ||||
|   // TODO attempt to read email from npmrc or the like?
 | ||||
|   Console.rl.question('email: ', function (email) { | ||||
|     // TODO validate email domain
 | ||||
|     email = /@/.test(email) && email.trim(); | ||||
|     if (!email) { askEmail(cb); return; } | ||||
|     state.config.email = email.trim(); | ||||
|     state.config.agreeTos = true; | ||||
|     console.info(""); | ||||
|     setTimeout(cb, 250); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function askForConfig(state, mainCb) { | ||||
|   Console.setup(state); | ||||
| 
 | ||||
|   // NOTE: Use of setTimeout
 | ||||
|   // We're using setTimeout just to make the user experience a little
 | ||||
| @ -128,19 +160,7 @@ function askForConfig(state, mainCb) { | ||||
|   // <= 100ms is shorter than normal human reaction time (ability to place events chronologically, which happened first)
 | ||||
|   // ~ 150-250ms is the sweet spot for most humans (long enough to notice change and not be jarred, but stay on task)
 | ||||
|   var firstSet = [ | ||||
|     function askEmail(cb) { | ||||
|       if (state.config.email) { cb(); return; } | ||||
|       console.info(TPLS.remote.setup.email); | ||||
|       // TODO attempt to read email from npmrc or the like?
 | ||||
|       rl.question('email: ', function (email) { | ||||
|         email = /@/.test(email) && email.trim(); | ||||
|         if (!email) { askEmail(cb); return; } | ||||
|         state.config.email = email.trim(); | ||||
|         state.config.agreeTos = true; | ||||
|         console.info(""); | ||||
|         setTimeout(cb, 250); | ||||
|       }); | ||||
|     } | ||||
|     askEmail | ||||
|   , function askRelay(cb) { | ||||
|       function checkRelay(relay) { | ||||
|         // TODO parse and check https://{{relay}}/.well-known/telebit.cloud/directives.json
 | ||||
| @ -173,7 +193,7 @@ function askForConfig(state, mainCb) { | ||||
|       console.info(""); | ||||
|       console.info("What relay will you be using? (press enter for default)"); | ||||
|       console.info(""); | ||||
|       rl.question('relay [default: telebit.cloud]: ', checkRelay); | ||||
|       Console.rl.question('relay [default: telebit.cloud]: ', checkRelay); | ||||
|     } | ||||
|   , function checkRelay(cb) { | ||||
|       nextSet = []; | ||||
| @ -201,7 +221,7 @@ function askForConfig(state, mainCb) { | ||||
|       console.info(""); | ||||
|       console.info("Type 'y' or 'yes' to accept these Terms of Service."); | ||||
|       console.info(""); | ||||
|       rl.question('agree to all? [y/N]: ', function (resp) { | ||||
|       Console.rl.question('agree to all? [y/N]: ', function (resp) { | ||||
|         resp = resp.trim(); | ||||
|         if (!/^y(es)?$/i.test(resp) && 'true' !== resp) { | ||||
|           throw new Error("You didn't accept the Terms of Service... not sure what to do..."); | ||||
| @ -219,7 +239,7 @@ function askForConfig(state, mainCb) { | ||||
|       console.info(""); | ||||
|       console.info("What updates would you like to receive? (" + options.join(',') + ")"); | ||||
|       console.info(""); | ||||
|       rl.question('messages (default: important): ', function (updates) { | ||||
|       Console.rl.question('messages (default: important): ', function (updates) { | ||||
|         state._updates = (updates || '').trim().toLowerCase(); | ||||
|         if (!state._updates) { state._updates = 'important'; } | ||||
|         if (-1 === options.indexOf(state._updates)) { askUpdates(cb); return; } | ||||
| @ -240,7 +260,7 @@ function askForConfig(state, mainCb) { | ||||
|       console.info(""); | ||||
|       console.info("Contribute project telemetry data? (press enter for default [yes])"); | ||||
|       console.info(""); | ||||
|       rl.question('telemetry [Y/n]: ', function (telemetry) { | ||||
|       Console.rl.question('telemetry [Y/n]: ', function (telemetry) { | ||||
|         if (!telemetry || /^y(es)?$/i.test(telemetry)) { | ||||
|           state.config.telemetry = true; | ||||
|         } | ||||
| @ -263,7 +283,7 @@ function askForConfig(state, mainCb) { | ||||
|       console.info("\tShared Secret (HMAC hex)"); | ||||
|       //console.info("\tPrivate key (hex)");
 | ||||
|       console.info(""); | ||||
|       rl.question('auth: ', function (resp) { | ||||
|       Console.rl.question('auth: ', function (resp) { | ||||
|         resp = (resp || '').trim(); | ||||
|         try { | ||||
|           JWT.decode(resp); | ||||
| @ -291,7 +311,7 @@ function askForConfig(state, mainCb) { | ||||
|       console.info("What servername(s) will you be relaying here?"); | ||||
|       console.info("(use a comma-separated list such as example.com,example.net)"); | ||||
|       console.info(""); | ||||
|       rl.question('domain(s): ', function (resp) { | ||||
|       Console.rl.question('domain(s): ', function (resp) { | ||||
|         resp = (resp || '').trim().split(/,/g); | ||||
|         if (!resp.length) { askServernames(); return; } | ||||
|         // TODO validate the domains
 | ||||
| @ -306,7 +326,7 @@ function askForConfig(state, mainCb) { | ||||
|       console.info("What tcp port(s) will you be relaying here?"); | ||||
|       console.info("(use a comma-separated list such as 2222,5050)"); | ||||
|       console.info(""); | ||||
|       rl.question('port(s) [default:none]: ', function (resp) { | ||||
|       Console.rl.question('port(s) [default:none]: ', function (resp) { | ||||
|         resp = (resp || '').trim().split(/,/g); | ||||
|         if (!resp.length) { askPorts(); return; } | ||||
|         // TODO validate the domains
 | ||||
| @ -320,10 +340,7 @@ function askForConfig(state, mainCb) { | ||||
|   function next() { | ||||
|     var q = nextSet.shift(); | ||||
|     if (!q) { | ||||
|       // https://github.com/nodejs/node/issues/21319
 | ||||
|       if (useTty) { try { stdin.push(null); } catch(e) { /*ignore*/ } } | ||||
|       rl.close(); | ||||
|       if (useTty) { try { stdin.close(); } catch(e) { /*ignore*/ } } | ||||
|       Console.teardown(); | ||||
|       mainCb(null, state); | ||||
|       return; | ||||
|     } | ||||
| @ -335,8 +352,76 @@ function askForConfig(state, mainCb) { | ||||
| 
 | ||||
| var RC; | ||||
| 
 | ||||
| function parseConfig(err, text) { | ||||
|   function handleConfig(config) { | ||||
| function bootstrap(opts) { | ||||
|   state.key = opts.key; | ||||
|   // 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') | ||||
|   , headers: { "User-Agent": 'Telebit/' + pkg.version } | ||||
|   }).then(function (resp) { | ||||
|     var nonce = resp.headers['replay-nonce']; | ||||
|     var newAccountUrl = RC.resolve('/acme/new-acct'); | ||||
|     var contact = []; | ||||
|     if (opts.email) { | ||||
|       contact.push("mailto:" + opts.email); | ||||
|     } | ||||
|     return keypairs.signJws({ | ||||
|       jwk: state.key | ||||
|     , protected: { | ||||
|         // alg will be filled out automatically
 | ||||
|         jwk: state.pub | ||||
|       , kid: false | ||||
|       , 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: contact // I don't think we have email yet...
 | ||||
|       , onlyReturnExisting: opts.onlyReturnExisting || !opts.email | ||||
|       //, 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' | ||||
|         , "User-Agent": 'Telebit/' + pkg.version | ||||
|         } | ||||
|       }).then(function (resp) { | ||||
|         //nonce = resp.headers['replay-nonce'];
 | ||||
|         if (!resp.body || 'valid' !== resp.body.status) { | ||||
|           console.error('request jws:', jws); | ||||
|           console.error('response:'); | ||||
|           console.error(resp.headers); | ||||
|           console.error(resp.body); | ||||
|           throw new Error("did not successfully create or restore account"); | ||||
|         } | ||||
|         return resp; | ||||
|       }); | ||||
|     }); | ||||
|   }).catch(RC.createRelauncher(bootstrap._replay(opts), bootstrap._bootstate)).catch(function (err) { | ||||
|     console.error(err); | ||||
|     process.exit(17); | ||||
|   }); | ||||
| } | ||||
| bootstrap._bootstate = {}; | ||||
| bootstrap._replay = function (_opts) { | ||||
|   return function (opts) { | ||||
|     // supply opts to match reverse signature (.length checking)
 | ||||
|     opts = _opts; | ||||
|     return bootstrap(opts); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| function handleConfig(config) { | ||||
|   var _config = state.config || {}; | ||||
| 
 | ||||
|   state.config = config; | ||||
|   var verstrd = [ pkg.name + ' daemon v' + state.config.version ]; | ||||
|   if (state.config.version && state.config.version !== pkg.version) { | ||||
| @ -345,6 +430,10 @@ function parseConfig(err, text) { | ||||
|     console.info(verstr.join(' ')); | ||||
|   } | ||||
| 
 | ||||
|   if (!state.config.email && _config) { | ||||
|     state.config.email = _config.email; | ||||
|   } | ||||
| 
 | ||||
|   //
 | ||||
|   // check for init first, before anything else
 | ||||
|   // because it has arguments that may help in
 | ||||
| @ -408,9 +497,9 @@ function parseConfig(err, text) { | ||||
| 
 | ||||
|   //console.log("no questioning:");
 | ||||
|   parseCli(state); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|   function parseCli(/*state*/) { | ||||
| function parseCli(/*state*/) { | ||||
|   var special = [ | ||||
|     'false', 'none', 'off', 'disable' | ||||
|   , 'true', 'auto', 'on', 'enable' | ||||
| @ -462,42 +551,9 @@ function parseConfig(err, text) { | ||||
| 
 | ||||
|   help(); | ||||
|   process.exit(11); | ||||
|   } | ||||
|   try { | ||||
|     state._clientConfig = JSON.parse(text || '{}'); | ||||
|   } catch(e1) { | ||||
|     try { | ||||
|       state._clientConfig = YAML.safeLoad(text || '{}'); | ||||
|     } catch(e2) { | ||||
|       try { | ||||
|         state._clientConfig = TOML.parse(text || ''); | ||||
|       } catch(e3) { | ||||
|         console.error(e1.message); | ||||
|         console.error(e2.message); | ||||
|         process.exit(1); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|   state._clientConfig = camelCopy(state._clientConfig || {}) || {}; | ||||
|   RC = require('../lib/rc/index.js').create(state); | ||||
|   RC.requestAsync = require('util').promisify(RC.request); | ||||
| 
 | ||||
|   if (!Object.keys(state._clientConfig).length) { | ||||
|     console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')'); | ||||
|     console.info(""); | ||||
|   } | ||||
| 
 | ||||
|   if ((err && 'ENOENT' === err.code) || !Object.keys(state._clientConfig).length) { | ||||
|     if (!err || 'ENOENT' === err.code) { | ||||
|       //console.warn("Empty config file. Run 'telebit init' to configure.\n");
 | ||||
|     } else { | ||||
|       console.warn("Couldn't load config:\n\n\t" + err.message + "\n"); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function handleRemoteRequest(service, fn) { | ||||
| function handleRemoteRequest(service, fn) { | ||||
|   return function (err, body) { | ||||
|     if ('function' === typeof fn) { | ||||
|       fn(err, body); // XXX was resp
 | ||||
| @ -567,9 +623,9 @@ function parseConfig(err, text) { | ||||
|       console.info(); | ||||
|     } | ||||
|   }; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|   function getToken(fn) { | ||||
| function getToken(fn) { | ||||
|   state.relay = state.config.relay; | ||||
| 
 | ||||
|   // { _otp, config: {} }
 | ||||
| @ -671,77 +727,28 @@ function parseConfig(err, text) { | ||||
|       })); | ||||
|     } | ||||
|   }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|   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') | ||||
|     , headers: { "User-Agent": 'Telebit/' + pkg.version } | ||||
|     }).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 | ||||
|         , kid: false | ||||
|         , 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' | ||||
|           , "User-Agent": 'Telebit/' + pkg.version | ||||
|           } | ||||
|         }).then(function (resp) { | ||||
|           //nonce = resp.headers['replay-nonce'];
 | ||||
|           if (!resp.body || 'valid' !== resp.body.status) { | ||||
|             console.error('request jws:', jws); | ||||
|             console.error('response:'); | ||||
|             console.error(resp.headers); | ||||
|             console.error(resp.body); | ||||
|             throw new Error("did not successfully create or restore account"); | ||||
|           } | ||||
|           return RC.requestAsync({ service: 'config', method: 'GET' }).catch(function (err) { | ||||
|             if (err) { | ||||
|               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."); | ||||
|                 console.error(err); | ||||
|               } else if ('ENOTSOCK' === err.code) { | ||||
|                 console.error(err); | ||||
|                 return; | ||||
|               } else { | ||||
|                 console.error(err); | ||||
|               } | ||||
|               process.exit(101); | ||||
| function parseConfig(text) { | ||||
|   var _clientConfig; | ||||
|   try { | ||||
|     _clientConfig = JSON.parse(text || '{}'); | ||||
|   } catch(e1) { | ||||
|     try { | ||||
|       _clientConfig = YAML.safeLoad(text || '{}'); | ||||
|     } catch(e2) { | ||||
|       try { | ||||
|         _clientConfig = TOML.parse(text || ''); | ||||
|       } catch(e3) { | ||||
|         console.error(e1.message); | ||||
|         console.error(e2.message); | ||||
|         process.exit(1); | ||||
|         return; | ||||
|       } | ||||
|           }).then(handleConfig); | ||||
|         }); | ||||
|       }); | ||||
|     }).catch(RC.createErrorHandler(bootstrap, bootState, function (err) { | ||||
|       console.error(err); | ||||
|       process.exit(17); | ||||
|     })); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   bootstrap(); | ||||
|   return camelCopy(_clientConfig || {}) || {}; | ||||
| } | ||||
| 
 | ||||
| var parsers = { | ||||
| @ -821,12 +828,30 @@ var parsers = { | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| var keystore = require('../lib/keystore.js').create(state); | ||||
| state.keystore = keystore; | ||||
| state.keystoreSecure = !keystore.insecure; | ||||
| keystore.all().then(function (list) { | ||||
| //
 | ||||
| // Start by reading the config file, before all else
 | ||||
| //
 | ||||
| util.promisify(fs.readFile)(confpath, 'utf8').catch(function (err) { | ||||
|   if (err && 'ENOENT' !== err.code) { | ||||
|     console.warn("Couldn't load config:\n\n\t" + err.message + "\n"); | ||||
|   } | ||||
| }).then(function (text) { | ||||
|   state._clientConfig = parseConfig(text); | ||||
|   RC = require('../lib/rc/index.js').create(state); // adds state._ipc
 | ||||
|   if (!Object.keys(state._clientConfig).length) { | ||||
|     console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')'); | ||||
|     console.info(""); | ||||
|   } | ||||
|   RC.requestAsync = require('util').promisify(RC.request); | ||||
| }).then(function () { | ||||
|   var keystore = require('../lib/keystore.js').create(state); | ||||
|   state.keystore = keystore; | ||||
|   state.keystoreSecure = !keystore.insecure; | ||||
|   keystore.all().then(function (list) { | ||||
|     var keyext = '.key.jwk.json'; | ||||
|     var key; | ||||
|     var p; | ||||
| 
 | ||||
|     // 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) { | ||||
| @ -838,21 +863,65 @@ keystore.all().then(function (list) { | ||||
|     }); | ||||
| 
 | ||||
|     if (key) { | ||||
|     state.key = key; | ||||
|     state.pub = keypairs.neuter({ jwk: key }); | ||||
|     fs.readFile(confpath, 'utf8', parseConfig); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return keypairs.generate().then(function (pair) { | ||||
|       p = Promise.resolve(key); | ||||
|     } else { | ||||
|       p = 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); | ||||
|           jwk.kid = kid; | ||||
|           console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid); | ||||
|         state.key = jwk; | ||||
|         fs.readFile(confpath, 'utf8', parseConfig); | ||||
|           return keystore.set(kid + keyext, jwk).then(function () { | ||||
|             return jwk; | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     return p.then(function (key) { | ||||
|       state.key = key; | ||||
|       state.pub = keypairs.neuter({ jwk: key }); | ||||
|       // we don't have config yet
 | ||||
|       state.config = {}; | ||||
|       return bootstrap({ key: state.key, onlyReturnExisting: true }).catch(function (err) { | ||||
|         console.error("[DEBUG] local account not created?"); | ||||
|         console.error(err); | ||||
|         // Ask for email address. The prior email may have been bad
 | ||||
|         return require('util').promisify(askEmail).then(function (email) { | ||||
|           return bootstrap({ key: state.key, email: email }); | ||||
|         }); | ||||
|       }).catch(function (err) { | ||||
|         console.error(err); | ||||
|         console.error("You may need to go into the web interface and allow Telebit Client by ID '" + key.kid + "'"); | ||||
|         process.exit(10); | ||||
|       }).then(function (result) { | ||||
|         //#console.log("Telebit Account Bootstrap result:");
 | ||||
|         //#console.log(result.body);
 | ||||
|         state.config.email = (result.body.contact[0]||'').replace(/mailto:/, ''); | ||||
|         var p2; | ||||
|         if (state.key.sub === state.config.email) { | ||||
|           p2 = Promise.resolve(state.key); | ||||
|         } else { | ||||
|           state.key.sub = state.config.email; | ||||
|           p2 = keystore.set(state.key.kid + keyext, state.key); | ||||
|         } | ||||
|         return p2.then(function () { | ||||
|           return RC.requestAsync({ service: 'config', method: 'GET' }).catch(function (err) { | ||||
|             if (err) { | ||||
|               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."); | ||||
|                 console.error(err); | ||||
|               } else if ('ENOTSOCK' === err.code) { | ||||
|                 console.error(err); | ||||
|                 return; | ||||
|               } else { | ||||
|                 console.error(err); | ||||
|               } | ||||
|               process.exit(101); | ||||
|               return; | ||||
|             } | ||||
|           }).then(handleConfig); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @ -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,6 +186,7 @@ var telebitState = {}; | ||||
| var appMethods = { | ||||
|   initialize: function () { | ||||
|     console.log("call initialize"); | ||||
|     return requestAccountHelper().then(function (/*key*/) { | ||||
|       if (!appData.init.relay) { | ||||
|         appData.init.relay = DEFAULT_RELAY; | ||||
|       } | ||||
| @ -210,6 +211,7 @@ var appMethods = { | ||||
|         console.error(err); | ||||
|         window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2))); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| , advance: function () { | ||||
|     return doConfigure(); | ||||
| @ -473,14 +475,43 @@ new Vue({ | ||||
| , methods: appMethods | ||||
| }); | ||||
| 
 | ||||
| function run(key) { | ||||
| 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; | ||||
|     } | ||||
|     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(); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| 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
 | ||||
|     // 😁 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; | ||||
| @ -522,6 +553,7 @@ function run(key) { | ||||
|     }).catch(function (err) { | ||||
|       appData.views.flash.error = err.message || JSON.stringify(err, null, 2); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @ -543,19 +575,8 @@ 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'; | ||||
| @ -574,15 +595,14 @@ function requestAccount() { | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| window.api = api; | ||||
| requestAccount().then(function (jwk) { | ||||
|   run(jwk); | ||||
|   setTimeout(function () { | ||||
| run(); | ||||
| setTimeout(function () { | ||||
|   document.body.hidden = false; | ||||
|   }, 50); | ||||
| }); | ||||
| }, 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) { | ||||
|       /*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) { | ||||
|         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); | ||||
|         }); | ||||
|         if ('ENOENT' !== err.code && 'ECONNREFUSED' !== err.code) { | ||||
|           reject(err); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|       cb(err); | ||||
|         // 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) { reject(err); return; } | ||||
|           opts._taketwo = true; | ||||
|           setTimeout(function () { | ||||
|             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); }); | ||||
|       } | ||||
|       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