MAJOR: Updates for Authenticated Web UI and CLI #30
| @ -25,6 +25,7 @@ var camelCopy = recase.camelCopy.bind(recase); | |||||||
| //var snakeCopy = recase.snakeCopy.bind(recase);
 | //var snakeCopy = recase.snakeCopy.bind(recase);
 | ||||||
| 
 | 
 | ||||||
| var urequest = require('@coolaj86/urequest'); | var urequest = require('@coolaj86/urequest'); | ||||||
|  | var urequestAsync = require('util').promisify(urequest); | ||||||
| var common = require('../lib/cli-common.js'); | var common = require('../lib/cli-common.js'); | ||||||
| 
 | 
 | ||||||
| var defaultConfPath = path.join(os.homedir(), '.config/telebit'); | var defaultConfPath = path.join(os.homedir(), '.config/telebit'); | ||||||
| @ -335,9 +336,7 @@ function askForConfig(state, mainCb) { | |||||||
| var RC; | var RC; | ||||||
| 
 | 
 | ||||||
| function parseConfig(err, text) { | function parseConfig(err, text) { | ||||||
|   function handleConfig(err, config) { |   function handleConfig(config) { | ||||||
|     if (err) { throw err; } |  | ||||||
| 
 |  | ||||||
|     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) { | ||||||
| @ -346,20 +345,6 @@ function parseConfig(err, text) { | |||||||
|       console.info(verstr.join(' ')); |       console.info(verstr.join(' ')); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     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; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     //
 |     //
 | ||||||
|     // 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
 | ||||||
| @ -492,6 +477,7 @@ function parseConfig(err, text) { | |||||||
| 
 | 
 | ||||||
|   state._clientConfig = camelCopy(state._clientConfig || {}) || {}; |   state._clientConfig = camelCopy(state._clientConfig || {}) || {}; | ||||||
|   RC = require('../lib/rc/index.js').create(state); |   RC = require('../lib/rc/index.js').create(state); | ||||||
|  |   RC.requestAsync = require('util').promisify(RC.request); | ||||||
| 
 | 
 | ||||||
|   if (!Object.keys(state._clientConfig).length) { |   if (!Object.keys(state._clientConfig).length) { | ||||||
|     console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')'); |     console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')'); | ||||||
| @ -682,7 +668,63 @@ function parseConfig(err, text) { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   RC.request({ service: 'config', method: 'GET' }, handleConfig); |   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') }).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 | ||||||
|  |         , 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' } | ||||||
|  |         }).then(function (resp) { | ||||||
|  |           //nonce = resp.headers['replay-nonce'];
 | ||||||
|  |           if (!resp.body || 'valid' !== resp.body.status) { | ||||||
|  |             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; | ||||||
|  |             } | ||||||
|  |           }).then(handleConfig); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }).catch(RC.createErrorHandler(bootstrap, bootState, function (err) { | ||||||
|  |       console.error(err); | ||||||
|  |       process.exit(17); | ||||||
|  |     })); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   bootstrap(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var parsers = { | var parsers = { | ||||||
|  | |||||||
							
								
								
									
										204
									
								
								bin/telebitd.js
									
									
									
									
									
								
							
							
						
						
									
										204
									
								
								bin/telebitd.js
									
									
									
									
									
								
							| @ -16,6 +16,7 @@ var crypto = require('crypto'); | |||||||
| var path = require('path'); | var path = require('path'); | ||||||
| var os = require('os'); | var os = require('os'); | ||||||
| var fs = require('fs'); | var fs = require('fs'); | ||||||
|  | var fsp = fs.promises; | ||||||
| var urequest = require('@coolaj86/urequest'); | var urequest = require('@coolaj86/urequest'); | ||||||
| var urequestAsync = require('util').promisify(urequest); | var urequestAsync = require('util').promisify(urequest); | ||||||
| var common = require('../lib/cli-common.js'); | var common = require('../lib/cli-common.js'); | ||||||
| @ -110,8 +111,20 @@ function getServername(servernames, sub) { | |||||||
|   })[0]; |   })[0]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /*global Promise*/ | ||||||
|  | var _savingConfig = Promise.resolve(); | ||||||
| function saveConfig(cb) { | function saveConfig(cb) { | ||||||
|   fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), cb); |   // simple sequencing chain so that write corruption is not possible
 | ||||||
|  |   _savingConfig = _savingConfig.then(function () { | ||||||
|  |     return fsp.writeFile(confpath, YAML.safeDump(snakeCopy(state.config))).then(function () { | ||||||
|  |       try { | ||||||
|  |         cb(); | ||||||
|  |       } catch(e) { | ||||||
|  |         console.error(e.stack); | ||||||
|  |         process.exit(47); | ||||||
|  |       } | ||||||
|  |     }).catch(cb); | ||||||
|  |   }); | ||||||
| } | } | ||||||
| var controllers = {}; | var controllers = {}; | ||||||
| controllers.http = function (req, res) { | controllers.http = function (req, res) { | ||||||
| @ -366,7 +379,7 @@ controllers.relay = function (req, res) { | |||||||
| }; | }; | ||||||
| controllers._nonces = {}; | controllers._nonces = {}; | ||||||
| controllers._requireNonce = function (req, res, next) { | controllers._requireNonce = function (req, res, next) { | ||||||
|   var nonce = req.jws && req.jws.protected && req.jws.protected.nonce; |   var nonce = req.jws && req.jws.header && req.jws.header.nonce; | ||||||
|   var active = (Date.now() - controllers._nonces[nonce]) < (4 * 60 * 60 * 1000); |   var active = (Date.now() - controllers._nonces[nonce]) < (4 * 60 * 60 * 1000); | ||||||
|   if (!active) { |   if (!active) { | ||||||
|     // TODO proper headers and error message
 |     // TODO proper headers and error message
 | ||||||
| @ -381,31 +394,133 @@ controllers._issueNonce = function (req, res) { | |||||||
|   var nonce = toUrlSafe(crypto.randomBytes(16).toString('base64')); |   var nonce = toUrlSafe(crypto.randomBytes(16).toString('base64')); | ||||||
|   // TODO associate with a TLS session
 |   // TODO associate with a TLS session
 | ||||||
|   controllers._nonces[nonce] = Date.now(); |   controllers._nonces[nonce] = Date.now(); | ||||||
|   res.headers.set("Replay-Nonce", nonce); |   res.setHeader("Replay-Nonce", nonce); | ||||||
|   return nonce; |   return nonce; | ||||||
| }; | }; | ||||||
| controllers.newNonce = function (req, res) { | controllers.newNonce = function (req, res) { | ||||||
|   res.statusCode = 200; |   res.statusCode = 200; | ||||||
|   res.headers.set("Cache-Control", "max-age=0, no-cache, no-store"); |   res.setHeader("Cache-Control", "max-age=0, no-cache, no-store"); | ||||||
|   // TODO
 |   // TODO
 | ||||||
|   //res.headers.set("Date", "Sun, 10 Mar 2019 08:04:45 GMT");
 |   //res.setHeader("Date", "Sun, 10 Mar 2019 08:04:45 GMT");
 | ||||||
|   // is this the expiration of the nonce itself? methinks maybe so
 |   // is this the expiration of the nonce itself? methinks maybe so
 | ||||||
|   //res.headers.set("Expires", "Sun, 10 Mar 2019 08:04:45 GMT");
 |   //res.setHeader("Expires", "Sun, 10 Mar 2019 08:04:45 GMT");
 | ||||||
|   // TODO use one of the registered domains
 |   // TODO use one of the registered domains
 | ||||||
|   //var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index"
 |   //var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index"
 | ||||||
|   var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined); |   var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined); | ||||||
|   var indexUrl = "http://localhost:" + port + "/index"; |   var indexUrl = "http://localhost:" + port + "/index"; | ||||||
|   res.headers.set("Link", "Link: <" + indexUrl + ">;rel=\"index\""); |   res.setHeader("Link", "<" + indexUrl + ">;rel=\"index\""); | ||||||
|   res.headers.set("Pragma", "no-cache"); |   res.setHeader("Cache-Control", "max-age=0, no-cache, no-store"); | ||||||
|   //res.headers.set("Strict-Transport-Security", "max-age=604800");
 |   res.setHeader("Pragma", "no-cache"); | ||||||
|   res.headers.set("X-Frame-Options", "DENY"); |   //res.setHeader("Strict-Transport-Security", "max-age=604800");
 | ||||||
|  |   res.setHeader("X-Frame-Options", "DENY"); | ||||||
| 
 | 
 | ||||||
|  |   controllers._issueNonce(req, res); | ||||||
|   res.end(""); |   res.end(""); | ||||||
| }; | }; | ||||||
| controllers.newAccount = function (req, res) { | controllers.newAccount = function (req, res) { | ||||||
|   controllers._requireNonce(req, res, function () { |   controllers._requireNonce(req, res, function () { | ||||||
|     res.statusCode = 500; |     // TODO clean up error messages to be similar to ACME
 | ||||||
|     res.end("not implemented yet"); | 
 | ||||||
|  |     // check if there's a public key
 | ||||||
|  |     if (!req.jws || !req.jws.header.kid || !req.jws.header.jwk) { | ||||||
|  |       res.statusCode = 422; | ||||||
|  |       res.send({ error: { message: "jws body was not present or could not be validated" } }); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // TODO mx record email validation
 | ||||||
|  |     if (!Array.isArray(req.body.contact) || !req.body.contact.length && '127.0.0.1' !== req.connection.remoteAddress) { | ||||||
|  |       // req.body.contact: [ 'mailto:email' ]
 | ||||||
|  |       res.statusCode = 422; | ||||||
|  |       res.send({ error: { message: "jws signed payload should contain a valid mailto:email in the contact array" } }); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!req.body.termsOfServiceAgreed) { | ||||||
|  |       // req.body.termsOfServiceAgreed: true
 | ||||||
|  |       res.statusCode = 422; | ||||||
|  |       res.send({ error: { message: "jws signed payload should have termsOfServiceAgreed: true" } }); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // We verify here regardless of whether or not it was verified before,
 | ||||||
|  |     // because it needs to be signed by the presenter of the public key,
 | ||||||
|  |     // not just a trusted key
 | ||||||
|  |     return verifyJws(req.jws.header.jwk, req.jws).then(function (verified) { | ||||||
|  |       if (!verified) { | ||||||
|  |         res.statusCode = 422; | ||||||
|  |         res.send({ error: { message: "jws body was not present or could not be validated" } }); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       var jwk = req.jws.header.jwk; | ||||||
|  |       return keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { | ||||||
|  |         // Note: we can get any number of account requests
 | ||||||
|  |         // and these need to be stored for some space of time
 | ||||||
|  |         // to await verification.
 | ||||||
|  |         // we'll have to expire them somehow and prevent DoS
 | ||||||
|  | 
 | ||||||
|  |         // check if this account already exists
 | ||||||
|  |         var account; | ||||||
|  |         DB.accounts.some(function (acc) { | ||||||
|  |           // TODO calculate thumbprint from jwk
 | ||||||
|  |           // find a key with matching jwk
 | ||||||
|  |           if (acc.thumb === thumb) { | ||||||
|  |             account = acc; | ||||||
|  |             return true; | ||||||
|  |           } | ||||||
|  |           // TODO ACME requires kid to be the account URL (STUPID!!!)
 | ||||||
|  |           // rather than the key id (as decided by the key issuer)
 | ||||||
|  |           // not sure if it's necessary to handle it that way though
 | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         var myBaseUrl = (req.connection.encrypted ? 'https' : 'http') + '://' + req.headers.host; | ||||||
|  |         if (!account) { | ||||||
|  |           // fail if onlyReturnExisting is not false
 | ||||||
|  |           if (req.body.onlyReturnExisting) { | ||||||
|  |             res.statusCode = 422; | ||||||
|  |             res.send({ error: { message: "onlyReturnExisting is set, so there's nothing to do" } }); | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |           res.statusCode = 201; | ||||||
|  |           account = {}; | ||||||
|  |           account._id = crypto.randomBytes(16).toString('base64'); | ||||||
|  |           // TODO be better about this
 | ||||||
|  |           account.location = myBaseUrl + '/acme/accounts/' + account._id; | ||||||
|  |           account.thumb = thumb; | ||||||
|  |           account.pub = jwk; | ||||||
|  |           account.contact = req.body.contact; | ||||||
|  |           DB.accounts.push(account); | ||||||
|  |           state.config.accounts = DB.accounts; | ||||||
|  |           saveConfig(function () {}); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         var result = { | ||||||
|  |           status: 'valid' | ||||||
|  |         , contact: account.contact // [ "mailto:john.doe@gmail.com" ],
 | ||||||
|  |         , orders: account.location + '/orders' | ||||||
|  |           // optional / off-spec
 | ||||||
|  |         , id: account._id | ||||||
|  |         , jwk: account.pub | ||||||
|  |         /* | ||||||
|  |           // I'm not sure if we have the real IP through telebit's network wrapper at this point
 | ||||||
|  |           // TODO we also need to set X-Forwarded-Addr as a proxy
 | ||||||
|  |           "initialIp": req.connection.remoteAddress, //"128.187.116.28",
 | ||||||
|  |           "createdAt": (new Date()).toISOString(), // "2018-04-17T21:29:10.833305103Z",
 | ||||||
|  |         */ | ||||||
|  |         }; | ||||||
|  |         res.setHeader('Location', account.location); | ||||||
|  |         res.send(result); | ||||||
|  |         /* | ||||||
|  |           Cache-Control: max-age=0, no-cache, no-store | ||||||
|  |           Content-Type: application/json | ||||||
|  |           Expires: Tue, 17 Apr 2018 21:29:10 GMT | ||||||
|  |           Link: <https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf>;rel="terms-of-service"
 | ||||||
|  |           Location: https://acme-staging-v02.api.letsencrypt.org/acme/acct/5937234
 | ||||||
|  |           Pragma: no-cache | ||||||
|  |           Replay-nonce: DKxX61imF38y_qkKvVcnWyo9oxQlHll0t9dMwGbkcxw | ||||||
|  |          */ | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -472,6 +587,7 @@ function jwtEggspress(req, res, next) { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // TODO verify if possible
 |   // TODO verify if possible
 | ||||||
|  |   console.warn("[warn] JWT is not verified yet"); | ||||||
|   next(); |   next(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -479,7 +595,7 @@ function verifyJws(jwk, jws) { | |||||||
|   return keypairs.export({ jwk: jwk }).then(function (pem) { |   return keypairs.export({ jwk: jwk }).then(function (pem) { | ||||||
|     var alg = 'SHA' + jws.header.alg.replace(/[^\d]+/i, ''); |     var alg = 'SHA' + jws.header.alg.replace(/[^\d]+/i, ''); | ||||||
|     var sig = ecdsaAsn1SigToJwtSig(jws.header.alg, jws.signature); |     var sig = ecdsaAsn1SigToJwtSig(jws.header.alg, jws.signature); | ||||||
|     return require('crypto') |     return crypto | ||||||
|       .createVerify(alg) |       .createVerify(alg) | ||||||
|       .update(jws.protected + '.' + jws.payload) |       .update(jws.protected + '.' + jws.payload) | ||||||
|       .verify(pem, sig, 'base64'); |       .verify(pem, sig, 'base64'); | ||||||
| @ -487,11 +603,14 @@ function verifyJws(jwk, jws) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function jwsEggspress(req, res, next) { | function jwsEggspress(req, res, next) { | ||||||
|  |   // Check to see if this looks like a JWS
 | ||||||
|   // TODO check header application/jose+json ??
 |   // TODO check header application/jose+json ??
 | ||||||
|   if (!req.body || !(req.body.protected && req.body.payload && req.body.signature)) { |   if (!req.body || !(req.body.protected && req.body.payload && req.body.signature)) { | ||||||
|     next(); |     next(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   // Decode it a bit
 | ||||||
|   req.jws = req.body; |   req.jws = req.body; | ||||||
|   req.jws.header = JSON.parse(Buffer.from(req.jws.protected, 'base64')); |   req.jws.header = JSON.parse(Buffer.from(req.jws.protected, 'base64')); | ||||||
|   req.body = Buffer.from(req.jws.payload, 'base64'); |   req.body = Buffer.from(req.jws.payload, 'base64'); | ||||||
| @ -499,27 +618,40 @@ function jwsEggspress(req, res, next) { | |||||||
|     req.body = JSON.parse(req.body); |     req.body = JSON.parse(req.body); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   // Check if this is a key we already trust
 | ||||||
|   var vjwk; |   var vjwk; | ||||||
|   DB.pubs.some(function (jwk) { |   DB.pubs.some(function (jwk) { | ||||||
|     if (jwk.kid === req.jws.header.kid) { |     if (jwk.kid === req.jws.header.kid) { | ||||||
|       vjwk = jwk; |       vjwk = jwk; | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   // Check if there aren't any keys that we trust
 | ||||||
|  |   // and this has signed itself, then make it a key we trust
 | ||||||
|  |   // (TODO: move this all to the new account function)
 | ||||||
|   if ((0 === DB.pubs.length && req.jws.header.jwk)) { |   if ((0 === DB.pubs.length && req.jws.header.jwk)) { | ||||||
|     vjwk = req.jws.header.jwk; |     vjwk = req.jws.header.jwk; | ||||||
|     if (!vjwk.kid) { throw Error("Impossible: no key id"); } |     if (!vjwk.kid) { throw Error("Impossible: no key id"); } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   // Don't verify if it can't be verified
 | ||||||
|  |   if (!vjwk) { | ||||||
|  |     next(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Run the  verification
 | ||||||
|   return verifyJws(vjwk, req.jws).then(function (verified) { |   return verifyJws(vjwk, req.jws).then(function (verified) { | ||||||
|     if (true !== verified) { |     if (true !== verified) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     // Mark as verified
 | ||||||
|     req.jws.verified = verified; |     req.jws.verified = verified; | ||||||
| 
 | 
 | ||||||
|     if (0 !== DB.pubs.length) { |     // (double check) DO NOT save if there are existing pubs
 | ||||||
|       return; |     if (0 !== DB.pubs.length) { return; } | ||||||
|     } | 
 | ||||||
|     return keystore.set(vjwk.kid + '.pub.jwk.json', vjwk); |     return keystore.set(vjwk.kid + PUBEXT, vjwk); | ||||||
|   }).then(function () { |   }).then(function () { | ||||||
|     next(); |     next(); | ||||||
|   }); |   }); | ||||||
| @ -828,16 +960,35 @@ function handleApi() { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // TODO turn strings into regexes to match beginnings
 |   // TODO turn strings into regexes to match beginnings
 | ||||||
|  |   app.get('/.well-known/openid-configuration', function (req, res) { | ||||||
|  |     res.setHeader("Access-Control-Allow-Headers", "Content-Type"); | ||||||
|  |     res.setHeader("Access-Control-Allow-Origin", "*"); | ||||||
|  |     res.setHeader("Access-Control-Expose-Headers", "Link, Replay-Nonce, Location"); | ||||||
|  |     res.setHeader("Access-Control-Max-Age", "86400"); | ||||||
|  |     if ('OPTIONS' === req.method) { res.end(); return; } | ||||||
|  |     res.send({ | ||||||
|  |       jwks_uri: 'http://localhost/.well-known/jwks.json' | ||||||
|  |     , acme_uri: 'http://localhost/acme/directory' | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|   app.use('/acme', function acmeCors(req, res, next) { |   app.use('/acme', function acmeCors(req, res, next) { | ||||||
|     // Taken from New-Nonce
 |     // Taken from New-Nonce
 | ||||||
|     res.headers.set("Access-Control-Allow-Headers", "Content-Type"); |     res.setHeader("Access-Control-Allow-Headers", "Content-Type"); | ||||||
|     res.headers.set("Access-Control-Allow-Origin", "*"); |     res.setHeader("Access-Control-Allow-Origin", "*"); | ||||||
|     res.headers.set("Access-Control-Expose-Headers", "Link, Replay-Nonce, Location"); |     res.setHeader("Access-Control-Expose-Headers", "Link, Replay-Nonce, Location"); | ||||||
|     res.headers.set("Access-Control-Max-Age", "86400"); |     res.setHeader("Access-Control-Max-Age", "86400"); | ||||||
|  |     if ('OPTIONS' === req.method) { res.end(); return; } | ||||||
|     next(); |     next(); | ||||||
|   }); |   }); | ||||||
|   app.use('/acme/new-nonce', controllers.newNonce); |   app.get('/acme/directory', function (req, res) { | ||||||
|   app.use('/acme/new-acct', controllers.newAccount); |     res.send({ | ||||||
|  |       'new-nonce': '/acme/new-nonce' | ||||||
|  |     , 'new-account': '/acme/new-acct' | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |   app.head('/acme/new-nonce', controllers.newNonce); | ||||||
|  |   app.get('/acme/new-nonce', controllers.newNonce); | ||||||
|  |   app.post('/acme/new-acct', controllers.newAccount); | ||||||
|   app.use(/\b(relay)\b/, controllers.relay); |   app.use(/\b(relay)\b/, controllers.relay); | ||||||
|   app.get(/\b(config)\b/, getConfigOnly); |   app.get(/\b(config)\b/, getConfigOnly); | ||||||
|   app.use(/\b(init|config)\b/, initOrConfig); |   app.use(/\b(init|config)\b/, initOrConfig); | ||||||
| @ -872,6 +1023,7 @@ function serveControlsHelper() { | |||||||
| 
 | 
 | ||||||
|   app.use('/rpc/', apiHandler); |   app.use('/rpc/', apiHandler); | ||||||
|   app.use('/api/', apiHandler); |   app.use('/api/', apiHandler); | ||||||
|  |   app.use('/acme/', apiHandler); | ||||||
|   app.use('/', serveStatic); |   app.use('/', serveStatic); | ||||||
| 
 | 
 | ||||||
|   controlServer = http.createServer(app); |   controlServer = http.createServer(app); | ||||||
| @ -1011,6 +1163,7 @@ function parseConfig(err, text) { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   state.config = camelCopy(state.config || {}) || {}; |   state.config = camelCopy(state.config || {}) || {}; | ||||||
|  |   DB.accounts = state.config.accounts || []; | ||||||
| 
 | 
 | ||||||
|   run(); |   run(); | ||||||
| 
 | 
 | ||||||
| @ -1414,6 +1567,7 @@ state.net = state.net || { | |||||||
| 
 | 
 | ||||||
| var DB = {}; | var DB = {}; | ||||||
| DB.pubs = []; | DB.pubs = []; | ||||||
|  | DB.accounts = []; | ||||||
| var token; | var token; | ||||||
| var tokenname = "access_token.jwt"; | var tokenname = "access_token.jwt"; | ||||||
| try { | try { | ||||||
| @ -1512,8 +1666,8 @@ function ecdsaAsn1SigToJwtSig(alg, b64sig) { | |||||||
| 
 | 
 | ||||||
| function toUrlSafe(b64) { | function toUrlSafe(b64) { | ||||||
|   return b64 |   return b64 | ||||||
|     .replace(/-/g, '+') |     .replace(/\+/g, '-') | ||||||
|     .replace(/_/g, '/') |     .replace(/\//g, '_') | ||||||
|     .replace(/=/g, '') |     .replace(/=/g, '') | ||||||
|   ; |   ; | ||||||
| } | } | ||||||
|  | |||||||
| @ -33,11 +33,12 @@ module.exports = function eggspress() { | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (!req.url.match(todo[0])) { |       var urlstr = (req.url.replace(/\/$/, '') + '/'); | ||||||
|  |       if (!urlstr.match(todo[0])) { | ||||||
|         //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 !== req.url.match(todo[0]).index) { |       } else if ('string' === typeof todo[0] && 0 !== urlstr.match(todo[0]).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; | ||||||
| @ -70,7 +71,7 @@ module.exports = function eggspress() { | |||||||
|   app.use = function (pattern, fn) { |   app.use = function (pattern, fn) { | ||||||
|     return app._use('', pattern, fn); |     return app._use('', pattern, fn); | ||||||
|   }; |   }; | ||||||
|   [ 'GET', 'POST', 'DELETE' ].forEach(function (method) { |   [ 'HEAD', 'GET', 'POST', 'DELETE' ].forEach(function (method) { | ||||||
|     app[method.toLowerCase()] = function (pattern, fn) { |     app[method.toLowerCase()] = function (pattern, fn) { | ||||||
|       return app._use(method, pattern, fn); |       return app._use(method, pattern, fn); | ||||||
|     }; |     }; | ||||||
|  | |||||||
| @ -72,6 +72,49 @@ module.exports.create = function (state) { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   var RC = {}; |   var RC = {}; | ||||||
|  |   RC.resolve = function (pathstr) { | ||||||
|  |     // TODO use real hostname and return reqOpts rather than string?
 | ||||||
|  |     return 'http://localhost:' + (RC.port({}).port||'1').toString() + '/' + pathstr.replace(/^\//, ''); | ||||||
|  |   }; | ||||||
|  |   RC.port = function (reqOpts) { | ||||||
|  |     var fs = require('fs'); | ||||||
|  |     var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port'); | ||||||
|  |     if (fs.existsSync(portFile)) { | ||||||
|  |       reqOpts.host = 'localhost'; | ||||||
|  |       reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10); | ||||||
|  |       if (!state.ipc) { | ||||||
|  |         state.ipc = {}; | ||||||
|  |       } | ||||||
|  |       state.ipc.type = 'port'; | ||||||
|  |       state.ipc.path = path.dirname(state._ipc.path); | ||||||
|  |       state.ipc.port = reqOpts.port; | ||||||
|  |     } else { | ||||||
|  |       reqOpts.socketPath = state._ipc.path; | ||||||
|  |     } | ||||||
|  |     return reqOpts; | ||||||
|  |   }; | ||||||
|  |   RC.createErrorHandler = function (replay, opts, cb) { | ||||||
|  |     return function (err) { | ||||||
|  |       // ENOENT - never started, cleanly exited last start, or creating socket at a different path
 | ||||||
|  |       // ECONNREFUSED - leftover socket just needs to be restarted
 | ||||||
|  |       if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { | ||||||
|  |         if (opts._taketwo) { | ||||||
|  |           cb(err); | ||||||
|  |           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; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       cb(err); | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|   RC.request = function request(opts, fn) { |   RC.request = function request(opts, fn) { | ||||||
|     if (!opts) { opts = {}; } |     if (!opts) { opts = {}; } | ||||||
|     var service = opts.service || 'config'; |     var service = opts.service || 'config'; | ||||||
| @ -93,44 +136,12 @@ module.exports.create = function (state) { | |||||||
|       method: method |       method: method | ||||||
|     , path: url |     , path: url | ||||||
|     }; |     }; | ||||||
|     var fs = require('fs'); |     reqOpts = RC.port(reqOpts); | ||||||
|     var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port'); |  | ||||||
|     if (fs.existsSync(portFile)) { |  | ||||||
|       reqOpts.host = 'localhost'; |  | ||||||
|       reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10); |  | ||||||
|       if (!state.ipc) { |  | ||||||
|         state.ipc = {}; |  | ||||||
|       } |  | ||||||
|       state.ipc.type = 'port'; |  | ||||||
|       state.ipc.path = path.dirname(state._ipc.path); |  | ||||||
|       state.ipc.port = reqOpts.port; |  | ||||||
|     } else { |  | ||||||
|       reqOpts.socketPath = state._ipc.path; |  | ||||||
|     } |  | ||||||
|     var req = http.request(reqOpts, function (resp) { |     var req = http.request(reqOpts, function (resp) { | ||||||
|       makeResponder(service, resp, fn); |       makeResponder(service, resp, fn); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     req.on('error', function (err) { |     req.on('error', RC.createErrorHandler(RC.request, opts, fn)); | ||||||
|       // 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) { |  | ||||||
|           fn(err); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) { |  | ||||||
|           if (err) { fn(err); return; } |  | ||||||
|           opts._taketwo = true; |  | ||||||
|           setTimeout(function () { |  | ||||||
|             RC.request(opts, fn); |  | ||||||
|           }, 2500); |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       fn(err); |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     // Simple GET
 |     // Simple GET
 | ||||||
|     if ('POST' !== method || !opts.data) { |     if ('POST' !== method || !opts.data) { | ||||||
| @ -150,7 +161,8 @@ module.exports.create = function (state) { | |||||||
|         // alg will be filled out automatically
 |         // alg will be filled out automatically
 | ||||||
|         jwk: state.pub |         jwk: state.pub | ||||||
|       , nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server
 |       , nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server
 | ||||||
|       , url: 'https://' + reqOpts.host + reqOpts.path |         // TODO make localhost exceptional
 | ||||||
|  |       , url: RC.resolve(reqOpts.path) | ||||||
|       } |       } | ||||||
|     , payload: JSON.stringify(opts.data) |     , payload: JSON.stringify(opts.data) | ||||||
|     }).then(function (jws) { |     }).then(function (jws) { | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -435,9 +435,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "keypairs": { |     "keypairs": { | ||||||
|       "version": "1.2.12", |       "version": "1.2.14", | ||||||
|       "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.12.tgz", |       "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz", | ||||||
|       "integrity": "sha512-zYjYdDvo7G4AIkkZVM3WEJBTRUIrFzYswYNqCxcCPHUsgbBBdewSHAH1CiaQ+VA6Yb7BLEPIv8gFrRz5wJrgsw==", |       "integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==", | ||||||
|       "requires": { |       "requires": { | ||||||
|         "eckles": "^1.4.1", |         "eckles": "^1.4.1", | ||||||
|         "rasha": "^1.2.4" |         "rasha": "^1.2.4" | ||||||
|  | |||||||
| @ -58,7 +58,7 @@ | |||||||
|     "greenlock": "^2.6.7", |     "greenlock": "^2.6.7", | ||||||
|     "js-yaml": "^3.11.0", |     "js-yaml": "^3.11.0", | ||||||
|     "keyfetch": "^1.1.8", |     "keyfetch": "^1.1.8", | ||||||
|     "keypairs": "^1.2.12", |     "keypairs": "^1.2.14", | ||||||
|     "mkdirp": "^0.5.1", |     "mkdirp": "^0.5.1", | ||||||
|     "proxy-packer": "^2.0.2", |     "proxy-packer": "^2.0.2", | ||||||
|     "ps-list": "^5.0.0", |     "ps-list": "^5.0.0", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user