MAJOR: Updates for Authenticated Web UI and CLI #30
| @ -1,12 +1,14 @@ | |||||||
| #!/usr/bin/env node
 | #!/usr/bin/env node
 | ||||||
| (function () { | (function () { | ||||||
| 'use strict'; | 'use strict'; | ||||||
|  | /*global Promise*/ | ||||||
| 
 | 
 | ||||||
| var pkg = require('../package.json'); | var pkg = require('../package.json'); | ||||||
| var os = require('os'); | var os = require('os'); | ||||||
| 
 | 
 | ||||||
| //var url = require('url');
 | //var url = require('url');
 | ||||||
| var fs = require('fs'); | var fs = require('fs'); | ||||||
|  | var util = require('util'); | ||||||
| var path = require('path'); | var path = require('path'); | ||||||
| //var https = require('https');
 | //var https = require('https');
 | ||||||
| var YAML = require('js-yaml'); | var YAML = require('js-yaml'); | ||||||
| @ -104,7 +106,9 @@ if (!confpath || /^--/.test(confpath)) { | |||||||
|   process.exit(1); |   process.exit(1); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function askForConfig(state, mainCb) { | var Console = {}; | ||||||
|  | Console.setup = function (state) { | ||||||
|  |   if (Console.rl) { return; } | ||||||
|   var fs = require('fs'); |   var fs = require('fs'); | ||||||
|   var ttyname = '/dev/tty'; |   var ttyname = '/dev/tty'; | ||||||
|   var stdin = useTty ? fs.createReadStream(ttyname, { |   var stdin = useTty ? fs.createReadStream(ttyname, { | ||||||
| @ -119,6 +123,34 @@ function askForConfig(state, mainCb) { | |||||||
|   , terminal: !/^win/i.test(os.platform()) && !useTty |   , terminal: !/^win/i.test(os.platform()) && !useTty | ||||||
|   }); |   }); | ||||||
|   state._useTty = 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
 |   // NOTE: Use of setTimeout
 | ||||||
|   // We're using setTimeout just to make the user experience a little
 |   // 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)
 |   // <= 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)
 |   // ~ 150-250ms is the sweet spot for most humans (long enough to notice change and not be jarred, but stay on task)
 | ||||||
|   var firstSet = [ |   var firstSet = [ | ||||||
|     function askEmail(cb) { |     askEmail | ||||||
|       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); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , function askRelay(cb) { |   , function askRelay(cb) { | ||||||
|       function checkRelay(relay) { |       function checkRelay(relay) { | ||||||
|         // TODO parse and check https://{{relay}}/.well-known/telebit.cloud/directives.json
 |         // TODO parse and check https://{{relay}}/.well-known/telebit.cloud/directives.json
 | ||||||
| @ -173,7 +193,7 @@ function askForConfig(state, mainCb) { | |||||||
|       console.info(""); |       console.info(""); | ||||||
|       console.info("What relay will you be using? (press enter for default)"); |       console.info("What relay will you be using? (press enter for default)"); | ||||||
|       console.info(""); |       console.info(""); | ||||||
|       rl.question('relay [default: telebit.cloud]: ', checkRelay); |       Console.rl.question('relay [default: telebit.cloud]: ', checkRelay); | ||||||
|     } |     } | ||||||
|   , function checkRelay(cb) { |   , function checkRelay(cb) { | ||||||
|       nextSet = []; |       nextSet = []; | ||||||
| @ -201,7 +221,7 @@ function askForConfig(state, mainCb) { | |||||||
|       console.info(""); |       console.info(""); | ||||||
|       console.info("Type 'y' or 'yes' to accept these Terms of Service."); |       console.info("Type 'y' or 'yes' to accept these Terms of Service."); | ||||||
|       console.info(""); |       console.info(""); | ||||||
|       rl.question('agree to all? [y/N]: ', function (resp) { |       Console.rl.question('agree to all? [y/N]: ', function (resp) { | ||||||
|         resp = resp.trim(); |         resp = resp.trim(); | ||||||
|         if (!/^y(es)?$/i.test(resp) && 'true' !== resp) { |         if (!/^y(es)?$/i.test(resp) && 'true' !== resp) { | ||||||
|           throw new Error("You didn't accept the Terms of Service... not sure what to do..."); |           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(""); | ||||||
|       console.info("What updates would you like to receive? (" + options.join(',') + ")"); |       console.info("What updates would you like to receive? (" + options.join(',') + ")"); | ||||||
|       console.info(""); |       console.info(""); | ||||||
|       rl.question('messages (default: important): ', function (updates) { |       Console.rl.question('messages (default: important): ', function (updates) { | ||||||
|         state._updates = (updates || '').trim().toLowerCase(); |         state._updates = (updates || '').trim().toLowerCase(); | ||||||
|         if (!state._updates) { state._updates = 'important'; } |         if (!state._updates) { state._updates = 'important'; } | ||||||
|         if (-1 === options.indexOf(state._updates)) { askUpdates(cb); return; } |         if (-1 === options.indexOf(state._updates)) { askUpdates(cb); return; } | ||||||
| @ -240,7 +260,7 @@ function askForConfig(state, mainCb) { | |||||||
|       console.info(""); |       console.info(""); | ||||||
|       console.info("Contribute project telemetry data? (press enter for default [yes])"); |       console.info("Contribute project telemetry data? (press enter for default [yes])"); | ||||||
|       console.info(""); |       console.info(""); | ||||||
|       rl.question('telemetry [Y/n]: ', function (telemetry) { |       Console.rl.question('telemetry [Y/n]: ', function (telemetry) { | ||||||
|         if (!telemetry || /^y(es)?$/i.test(telemetry)) { |         if (!telemetry || /^y(es)?$/i.test(telemetry)) { | ||||||
|           state.config.telemetry = true; |           state.config.telemetry = true; | ||||||
|         } |         } | ||||||
| @ -263,7 +283,7 @@ function askForConfig(state, mainCb) { | |||||||
|       console.info("\tShared Secret (HMAC hex)"); |       console.info("\tShared Secret (HMAC hex)"); | ||||||
|       //console.info("\tPrivate key (hex)");
 |       //console.info("\tPrivate key (hex)");
 | ||||||
|       console.info(""); |       console.info(""); | ||||||
|       rl.question('auth: ', function (resp) { |       Console.rl.question('auth: ', function (resp) { | ||||||
|         resp = (resp || '').trim(); |         resp = (resp || '').trim(); | ||||||
|         try { |         try { | ||||||
|           JWT.decode(resp); |           JWT.decode(resp); | ||||||
| @ -291,7 +311,7 @@ function askForConfig(state, mainCb) { | |||||||
|       console.info("What servername(s) will you be relaying here?"); |       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("(use a comma-separated list such as example.com,example.net)"); | ||||||
|       console.info(""); |       console.info(""); | ||||||
|       rl.question('domain(s): ', function (resp) { |       Console.rl.question('domain(s): ', function (resp) { | ||||||
|         resp = (resp || '').trim().split(/,/g); |         resp = (resp || '').trim().split(/,/g); | ||||||
|         if (!resp.length) { askServernames(); return; } |         if (!resp.length) { askServernames(); return; } | ||||||
|         // TODO validate the domains
 |         // 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("What tcp port(s) will you be relaying here?"); | ||||||
|       console.info("(use a comma-separated list such as 2222,5050)"); |       console.info("(use a comma-separated list such as 2222,5050)"); | ||||||
|       console.info(""); |       console.info(""); | ||||||
|       rl.question('port(s) [default:none]: ', function (resp) { |       Console.rl.question('port(s) [default:none]: ', function (resp) { | ||||||
|         resp = (resp || '').trim().split(/,/g); |         resp = (resp || '').trim().split(/,/g); | ||||||
|         if (!resp.length) { askPorts(); return; } |         if (!resp.length) { askPorts(); return; } | ||||||
|         // TODO validate the domains
 |         // TODO validate the domains
 | ||||||
| @ -320,10 +340,7 @@ function askForConfig(state, mainCb) { | |||||||
|   function next() { |   function next() { | ||||||
|     var q = nextSet.shift(); |     var q = nextSet.shift(); | ||||||
|     if (!q) { |     if (!q) { | ||||||
|       // https://github.com/nodejs/node/issues/21319
 |       Console.teardown(); | ||||||
|       if (useTty) { try { stdin.push(null); } catch(e) { /*ignore*/ } } |  | ||||||
|       rl.close(); |  | ||||||
|       if (useTty) { try { stdin.close(); } catch(e) { /*ignore*/ } } |  | ||||||
|       mainCb(null, state); |       mainCb(null, state); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @ -335,8 +352,76 @@ function askForConfig(state, mainCb) { | |||||||
| 
 | 
 | ||||||
| var RC; | var RC; | ||||||
| 
 | 
 | ||||||
| function parseConfig(err, text) { | function bootstrap(opts) { | ||||||
|   function handleConfig(config) { |   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; |   state.config = config; | ||||||
|   var verstrd = [ pkg.name + ' daemon v' + state.config.version ]; |   var verstrd = [ pkg.name + ' daemon v' + state.config.version ]; | ||||||
|   if (state.config.version && state.config.version !== pkg.version) { |   if (state.config.version && state.config.version !== pkg.version) { | ||||||
| @ -345,6 +430,10 @@ function parseConfig(err, text) { | |||||||
|     console.info(verstr.join(' ')); |     console.info(verstr.join(' ')); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   if (!state.config.email && _config) { | ||||||
|  |     state.config.email = _config.email; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   //
 |   //
 | ||||||
|   // check for init first, before anything else
 |   // check for init first, before anything else
 | ||||||
|   // because it has arguments that may help in
 |   // because it has arguments that may help in
 | ||||||
| @ -408,9 +497,9 @@ function parseConfig(err, text) { | |||||||
| 
 | 
 | ||||||
|   //console.log("no questioning:");
 |   //console.log("no questioning:");
 | ||||||
|   parseCli(state); |   parseCli(state); | ||||||
|   } | } | ||||||
| 
 | 
 | ||||||
|   function parseCli(/*state*/) { | function parseCli(/*state*/) { | ||||||
|   var special = [ |   var special = [ | ||||||
|     'false', 'none', 'off', 'disable' |     'false', 'none', 'off', 'disable' | ||||||
|   , 'true', 'auto', 'on', 'enable' |   , 'true', 'auto', 'on', 'enable' | ||||||
| @ -462,42 +551,9 @@ function parseConfig(err, text) { | |||||||
| 
 | 
 | ||||||
|   help(); |   help(); | ||||||
|   process.exit(11); |   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 || {}) || {}; | function handleRemoteRequest(service, fn) { | ||||||
|   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) { |  | ||||||
|   return function (err, body) { |   return function (err, body) { | ||||||
|     if ('function' === typeof fn) { |     if ('function' === typeof fn) { | ||||||
|       fn(err, body); // XXX was resp
 |       fn(err, body); // XXX was resp
 | ||||||
| @ -567,9 +623,9 @@ function parseConfig(err, text) { | |||||||
|       console.info(); |       console.info(); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|   } | } | ||||||
| 
 | 
 | ||||||
|   function getToken(fn) { | function getToken(fn) { | ||||||
|   state.relay = state.config.relay; |   state.relay = state.config.relay; | ||||||
| 
 | 
 | ||||||
|   // { _otp, config: {} }
 |   // { _otp, config: {} }
 | ||||||
| @ -671,77 +727,28 @@ function parseConfig(err, text) { | |||||||
|       })); |       })); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|   } | } | ||||||
| 
 | 
 | ||||||
|   var bootState = {}; | function parseConfig(text) { | ||||||
|   function bootstrap() { |   var _clientConfig; | ||||||
|     // Create / retrieve account (sign-in, more or less)
 |   try { | ||||||
|     // TODO hit directory resource /.well-known/openid-configuration -> acme_uri (?)
 |     _clientConfig = JSON.parse(text || '{}'); | ||||||
|     // Occassionally rotate the key just for the sake of testing the key rotation
 |   } catch(e1) { | ||||||
|     return urequestAsync({ |     try { | ||||||
|       method: 'HEAD' |       _clientConfig = YAML.safeLoad(text || '{}'); | ||||||
|     , url: RC.resolve('/acme/new-nonce') |     } catch(e2) { | ||||||
|     , headers: { "User-Agent": 'Telebit/' + pkg.version } |       try { | ||||||
|     }).then(function (resp) { |         _clientConfig = TOML.parse(text || ''); | ||||||
|       var nonce = resp.headers['replay-nonce']; |       } catch(e3) { | ||||||
|       var newAccountUrl = RC.resolve('/acme/new-acct'); |         console.error(e1.message); | ||||||
|       return keypairs.signJws({ |         console.error(e2.message); | ||||||
|         jwk: state.key |         process.exit(1); | ||||||
|       , 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); |  | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|           }).then(handleConfig); |     } | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }).catch(RC.createErrorHandler(bootstrap, bootState, function (err) { |  | ||||||
|       console.error(err); |  | ||||||
|       process.exit(17); |  | ||||||
|     })); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   bootstrap(); |   return camelCopy(_clientConfig || {}) || {}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var parsers = { | var parsers = { | ||||||
| @ -821,12 +828,30 @@ var parsers = { | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| var keystore = require('../lib/keystore.js').create(state); | //
 | ||||||
| state.keystore = keystore; | // Start by reading the config file, before all else
 | ||||||
| state.keystoreSecure = !keystore.insecure; | //
 | ||||||
| keystore.all().then(function (list) { | 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 keyext = '.key.jwk.json'; | ||||||
|     var key; |     var key; | ||||||
|  |     var p; | ||||||
|  | 
 | ||||||
|     // TODO create map by account and index into that map to get the master key
 |     // TODO create map by account and index into that map to get the master key
 | ||||||
|     // and sort keys in the process
 |     // and sort keys in the process
 | ||||||
|     list.some(function (el) { |     list.some(function (el) { | ||||||
| @ -838,21 +863,65 @@ keystore.all().then(function (list) { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (key) { |     if (key) { | ||||||
|     state.key = key; |       p = Promise.resolve(key); | ||||||
|     state.pub = keypairs.neuter({ jwk: key }); |     } else { | ||||||
|     fs.readFile(confpath, 'utf8', parseConfig); |       p = keypairs.generate().then(function (pair) { | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return keypairs.generate().then(function (pair) { |  | ||||||
|         var jwk = pair.private; |         var jwk = pair.private; | ||||||
|         return keypairs.thumbprint({ jwk: jwk }).then(function (kid) { |         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); |           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); |           console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid); | ||||||
|         state.key = jwk; |           return keystore.set(kid + keyext, jwk).then(function () { | ||||||
|         fs.readFile(confpath, 'utf8', parseConfig); |             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) { | 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,6 +186,7 @@ var telebitState = {}; | |||||||
| var appMethods = { | var appMethods = { | ||||||
|   initialize: function () { |   initialize: function () { | ||||||
|     console.log("call initialize"); |     console.log("call initialize"); | ||||||
|  |     return requestAccountHelper().then(function (/*key*/) { | ||||||
|       if (!appData.init.relay) { |       if (!appData.init.relay) { | ||||||
|         appData.init.relay = DEFAULT_RELAY; |         appData.init.relay = DEFAULT_RELAY; | ||||||
|       } |       } | ||||||
| @ -210,6 +211,7 @@ var appMethods = { | |||||||
|         console.error(err); |         console.error(err); | ||||||
|         window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2))); |         window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2))); | ||||||
|       }); |       }); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| , advance: function () { | , advance: function () { | ||||||
|     return doConfigure(); |     return doConfigure(); | ||||||
| @ -473,14 +475,43 @@ new Vue({ | |||||||
| , methods: appMethods | , 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; |     api._key = key; | ||||||
|  |     // TODO create session instance of Telebit
 | ||||||
|  |     Telebit._key = key; | ||||||
|     // 😁 1. Get ACME directory
 |     // 😁 1. Get ACME directory
 | ||||||
|     // 😁 2. Fetch ACME account
 |     // 😁 2. Fetch ACME account
 | ||||||
|     // 3. Test if account has access
 |     // 3. Test if account has access
 | ||||||
|     // 4. Show command line auth instructions to auth
 |     // 4. Show command line auth instructions to auth
 | ||||||
|   // 5. Sign requests / use JWT
 |     // 😁 5. Sign requests / use JWT
 | ||||||
|   // 6. Enforce token required for config, status, etc
 |     // 😁 6. Enforce token required for config, status, etc
 | ||||||
|     // 7. Move admin interface to standard ports (admin.foo-bar-123.telebit.xyz)
 |     // 7. Move admin interface to standard ports (admin.foo-bar-123.telebit.xyz)
 | ||||||
|     api.config().then(function (config) { |     api.config().then(function (config) { | ||||||
|       telebitState.config = config; |       telebitState.config = config; | ||||||
| @ -522,6 +553,7 @@ function run(key) { | |||||||
|     }).catch(function (err) { |     }).catch(function (err) { | ||||||
|       appData.views.flash.error = err.message || JSON.stringify(err, null, 2); |       appData.views.flash.error = err.message || JSON.stringify(err, null, 2); | ||||||
|     }); |     }); | ||||||
|  |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -543,19 +575,8 @@ 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'; | ||||||
| @ -574,15 +595,14 @@ function requestAccount() { | |||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|   }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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) { | ||||||
|  |       /*global Promise*/ | ||||||
|  |       var p = new Promise(function (resolve, reject) { | ||||||
|         // ENOENT - never started, cleanly exited last start, or creating socket at a different path
 |         // ENOENT - never started, cleanly exited last start, or creating socket at a different path
 | ||||||
|         // ECONNREFUSED - leftover socket just needs to be restarted
 |         // ECONNREFUSED - leftover socket just needs to be restarted
 | ||||||
|       if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { |         if ('ENOENT' !== err.code && 'ECONNREFUSED' !== err.code) { | ||||||
|         if (opts._taketwo) { |           reject(err); | ||||||
|           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; |           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) { |   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