From 5ff91ec37e3d6e8dbf12b3a19a90be4569337c00 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 5 Aug 2016 18:11:19 -0400 Subject: [PATCH 01/14] most basic middleware function --- lib/middleware.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lib/middleware.js diff --git a/lib/middleware.js b/lib/middleware.js new file mode 100644 index 0000000..13af19c --- /dev/null +++ b/lib/middleware.js @@ -0,0 +1,31 @@ +'use strict'; + +module.exports = function (le) { + return function () { + var prefix = le.acmeChallengePrefix; // /.well-known/acme-challenge/:token + + return function (req, res, next) { + if (0 !== req.url.indexOf(prefix)) { + next(); + return; + } + + var key = req.url.slice(prefix.length); + var hostname = req.hostname || (req.headers.host || '').toLowerCase().replace(/:*/, ''); + + // TODO tpl copy? + le.challenger.getAsync(le, hostname, key).then(function (token) { + if (!token) { + res.status = 404; + res.send("Error: These aren't the tokens you're looking for. Move along."); + return; + } + + res.send(token); + }, function (/*err*/) { + res.status = 404; + res.send("Error: These aren't the tokens you're looking for. Move along."); + }); + }; + }; +}; From 63b9cb5ec9e4cd1213623389360390e463474aad Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 5 Aug 2016 18:16:29 -0400 Subject: [PATCH 02/14] getting pretty close --- index.js | 126 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 75 insertions(+), 51 deletions(-) diff --git a/index.js b/index.js index 077a42c..b9bcfa6 100644 --- a/index.js +++ b/index.js @@ -23,58 +23,82 @@ Object.keys(LE.defaults).forEach(function (key) { LE[key] = LE.defaults[key]; }); -LE.create = function (defaults, handlers, backend) { - var Core = require('./lib/core'); - var core; - if (!backend) { backend = require('./lib/pycompat'); } - if (!handlers) { handlers = {}; } - if (!handlers.renewWithin) { handlers.renewWithin = 3 * 24 * 60 * 60 * 1000; } - if (!handlers.memorizeFor) { handlers.memorizeFor = 1 * 24 * 60 * 60 * 1000; } - if (!handlers.sniRegisterCallback) { - handlers.sniRegisterCallback = function (args, cache, cb) { - // TODO when we have ECDSA, just do this automatically - cb(null, null); - }; - } - - if (backend.create) { - backend = backend.create(defaults); - } - backend = PromiseA.promisifyAll(backend); - core = Core.create(defaults, handlers, backend); - - var le = { - backend: backend - , core: core - // register - , create: function (args, cb) { - return core.registerAsync(args).then(function (pems) { - cb(null, pems); - }, cb); +var u; // undefined +LE._undefined = { + store: u +, challenger: u +, register: u +, check: u +, renewWithin: u +, memorizeFor: u +, acmeChallengePrefix: u +}; +LE._undefine = function (le) { + Object.keys(LE._undefined).forEach(function (key) { + if (!(key in le)) { + le[key] = u; } - // fetch - , domain: function (args, cb) { - // TODO must return email, domains, tos, pems - return core.fetchAsync(args).then(function (certInfo) { - cb(null, certInfo); - }, cb); - } - , domains: function (args, cb) { - // TODO show all domains or limit by account - throw new Error('not implemented'); - } - , accounts: function (args, cb) { - // TODO show all accounts or limit by domain - throw new Error('not implemented'); - } - , account: function (args, cb) { - // TODO return one account - throw new Error('not implemented'); - } - }; - - // exists - // get + }); + + return le; +}; +LE.create = function (le) { + le = LE._undefine(le); + var store = le.store || require('le-store-certbot').create({ debug: le.debug }); + var challenger = le.challenge || require('le-store-certbot').create({ debug: le.debug }); + var core = le.core = require('./lib/core'); + + le.acmeChallengePrefix = LE.acmeChallengePrefix; + + if (!le.renewWithin) { le.renewWithin = 3 * 24 * 60 * 60 * 1000; } + if (!le.memorizeFor) { le.memorizeFor = 1 * 24 * 60 * 60 * 1000; } + + if (!le.server) { + throw new Error("opts.server must be set to 'staging' or a production url, such as LE.productionServerUrl'"); + } + if ('staging' === le.server) { + le.server = LE.stagingServerUrl; + } + else if ('production' === le.server) { + le.server = LE.productionServerUrl; + } + + if (store.create) { + store = store.create(le); + } + store = PromiseA.promisifyAll(store); + le._storeOpts = store.getOptions(); + Object.keys(le._storeOpts).forEach(function (key) { + if (!(key in le._storeOpts)) { + le[key] = le._storeOpts[key]; + } + }); + + if (challenger.create) { + challenger = challenger.create(le); + } + challenger = PromiseA.promisifyAll(challenger); + le._challengerOpts = challenger.getOptions(); + Object.keys(le._storeOpts).forEach(function (key) { + if (!(key in le._challengerOpts)) { + le[key] = le._challengerOpts[key]; + } + }); + + core = le.core = core.create(le); + + le.register = function (args) { + return core.registerAsync(args); + }; + + le.check = function (args) { + // TODO must return email, domains, tos, pems + return core.fetchAsync(args); + }; + + le.middleware = function () { + return require('./lib/middleware')(le); + }; return le; }; From a654891cfae9c93238d72c8fc1859d1a1ae144d9 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 5 Aug 2016 18:21:10 -0400 Subject: [PATCH 03/14] update --- README.md | 47 +++++++++++++++++++++++------------------------ index.js | 17 ++++++++++------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index cdc11d1..6ec547c 100644 --- a/README.md +++ b/README.md @@ -154,31 +154,30 @@ le.exists({ domain: 'example.com' }).then(function (results) { return; } + // Register Certificate manually - le.register( + le.register({ - { domains: ['example.com'] // CHANGE TO YOUR DOMAIN (list for SANS) - , email: 'user@email.com' // CHANGE TO YOUR EMAIL - , agreeTos: '' // set to tosUrl string to pre-approve (and skip agreeToTerms) - , rsaKeySize: 2048 // 1024 or 2048 - , challengeType: 'http-01' // http-01, tls-sni-01, or dns-01 - } + domains: ['example.com'] // CHANGE TO YOUR DOMAIN (list for SANS) + , email: 'user@email.com' // CHANGE TO YOUR EMAIL + , agreeTos: '' // set to tosUrl string to pre-approve (and skip agreeToTerms) + , rsaKeySize: 2048 // 2048 or higher + , challengeType: 'http-01' // http-01, tls-sni-01, or dns-01 - , function (err, results) { - if (err) { - // Note: you must either use le.middleware() with express, - // manually use le.getChallenge(domain, key, val, done) - // or have a webserver running and responding - // to /.well-known/acme-challenge at `webrootPath` - console.error('[Error]: node-letsencrypt/examples/standalone'); - console.error(err.stack); - return; - } + }).then(function (results) { - console.log('success'); - } + console.log('success'); - ); + }, function (err) { + + // Note: you must either use le.middleware() with express, + // manually use le.getChallenge(domain, key, val, done) + // or have a webserver running and responding + // to /.well-known/acme-challenge at `webrootPath` + console.error('[Error]: node-letsencrypt/examples/standalone'); + console.error(err.stack); + + }); }); ``` @@ -241,7 +240,7 @@ TODO double check and finish * accounts.get * accounts.exists * certs - * certs.byDomain + * certs.byAccount * certs.all * certs.get * certs.exists @@ -250,9 +249,9 @@ TODO double check and finish TODO finish -* setChallenge(opts, domain, key, value, done); // opts will be saved with domain/key -* getChallenge(domain, key, done); // opts will be retrieved by domain/key -* removeChallenge(domain, key, done); // opts will be retrieved by domain/key +* `.set(opts, domain, key, value, done);` // opts will be saved with domain/key +* `.get(opts, domain, key, done);` // opts will be retrieved by domain/key +* `.remove(opts, domain, key, done);` // opts will be retrieved by domain/key Change History ============== diff --git a/index.js b/index.js index b9bcfa6..0582be7 100644 --- a/index.js +++ b/index.js @@ -1,20 +1,17 @@ 'use strict'; -// TODO handle www and no-www together somehow? - var PromiseA = require('bluebird'); var leCore = require('letiny-core'); var LE = module.exports; LE.defaults = { - server: leCore.productionServerUrl -, stagingServer: leCore.stagingServerUrl -, liveServer: leCore.productionServerUrl - -, productionServerUrl: leCore.productionServerUrl + productionServerUrl: leCore.productionServerUrl , stagingServerUrl: leCore.stagingServerUrl +, rsaKeySize: leCore.rsaKeySize || 2048 +, challengeType: leCore.challengeType || 'http-01' + , acmeChallengePrefix: leCore.acmeChallengePrefix }; @@ -23,6 +20,7 @@ Object.keys(LE.defaults).forEach(function (key) { LE[key] = LE.defaults[key]; }); +// show all possible options var u; // undefined LE._undefined = { store: u @@ -32,6 +30,9 @@ LE._undefined = { , renewWithin: u , memorizeFor: u , acmeChallengePrefix: u +, rsaKeySize: u +, challengeType: u +, server: u }; LE._undefine = function (le) { Object.keys(LE._undefined).forEach(function (key) { @@ -49,6 +50,8 @@ LE.create = function (le) { var core = le.core = require('./lib/core'); le.acmeChallengePrefix = LE.acmeChallengePrefix; + le.rsaKeySize = le.rsaKeySize || LE.rsaKeySize; + le.challengeType = le.challengeType || LE.challengeType; if (!le.renewWithin) { le.renewWithin = 3 * 24 * 60 * 60 * 1000; } if (!le.memorizeFor) { le.memorizeFor = 1 * 24 * 60 * 60 * 1000; } From 56736c4f9800fbd846a41b10599c901d7f06bb22 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 5 Aug 2016 18:50:42 -0400 Subject: [PATCH 04/14] normalize --- index.js | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 0582be7..13821b9 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,10 @@ 'use strict'; -var PromiseA = require('bluebird'); var leCore = require('letiny-core'); var LE = module.exports; +// in-process cache, shared between all instances +var ipc = {}; LE.defaults = { productionServerUrl: leCore.productionServerUrl @@ -33,6 +34,7 @@ LE._undefined = { , rsaKeySize: u , challengeType: u , server: u +, _ipc: u }; LE._undefine = function (le) { Object.keys(LE._undefined).forEach(function (key) { @@ -44,14 +46,17 @@ LE._undefine = function (le) { return le; }; LE.create = function (le) { - le = LE._undefine(le); - var store = le.store || require('le-store-certbot').create({ debug: le.debug }); - var challenger = le.challenge || require('le-store-certbot').create({ debug: le.debug }); - var core = le.core = require('./lib/core'); + var PromiseA = require('bluebird'); + le.store = le.store || require('le-store-certbot').create({ debug: le.debug }); + le.challenger = le.challenger || require('le-store-certbot').create({ debug: le.debug }); + le.core = require('./lib/core'); + + le = LE._undefine(le); le.acmeChallengePrefix = LE.acmeChallengePrefix; le.rsaKeySize = le.rsaKeySize || LE.rsaKeySize; le.challengeType = le.challengeType || LE.challengeType; + le._ipc = ipc; if (!le.renewWithin) { le.renewWithin = 3 * 24 * 60 * 60 * 1000; } if (!le.memorizeFor) { le.memorizeFor = 1 * 24 * 60 * 60 * 1000; } @@ -66,37 +71,39 @@ LE.create = function (le) { le.server = LE.productionServerUrl; } - if (store.create) { - store = store.create(le); + if (le.store.create) { + le.store = le.store.create(le); } - store = PromiseA.promisifyAll(store); - le._storeOpts = store.getOptions(); + le.store = PromiseA.promisifyAll(le.store); + le._storeOpts = le.store.getOptions(); Object.keys(le._storeOpts).forEach(function (key) { if (!(key in le._storeOpts)) { le[key] = le._storeOpts[key]; } }); - if (challenger.create) { - challenger = challenger.create(le); + if (le.challenger.create) { + le.challenger = le.challenger.create(le); } - challenger = PromiseA.promisifyAll(challenger); - le._challengerOpts = challenger.getOptions(); + le.challenger = PromiseA.promisifyAll(le.challenger); + le._challengerOpts = le.challenger.getOptions(); Object.keys(le._storeOpts).forEach(function (key) { if (!(key in le._challengerOpts)) { le[key] = le._challengerOpts[key]; } }); - core = le.core = core.create(le); + if (le.core.create) { + le.core = le.core.create(le); + } le.register = function (args) { - return core.registerAsync(args); + return le.core.registerAsync(args); }; le.check = function (args) { // TODO must return email, domains, tos, pems - return core.fetchAsync(args); + return le.core.fetchAsync(args); }; le.middleware = function () { From ae91c08dd1bee3850917c56bc60f16c65c5b718e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 5 Aug 2016 18:54:28 -0400 Subject: [PATCH 05/14] almost there --- lib/core.js | 27 +++++++-------------------- lib/{common.js => utils.js} | 0 2 files changed, 7 insertions(+), 20 deletions(-) rename lib/{common.js => utils.js} (100%) diff --git a/lib/core.js b/lib/core.js index 93d73b6..a6ec745 100644 --- a/lib/core.js +++ b/lib/core.js @@ -1,16 +1,8 @@ 'use strict'; -var LE = require('../'); -var ipc = {}; // in-process cache - -module.exports.create = function (defaults, handlers, backend) { - var backendDefaults = backend.getDefaults && backend.getDefaults || backend.defaults || {}; - - defaults.server = defaults.server || LE.liveServer; - handlers.merge = require('./common').merge; - handlers.tplCopy = require('./common').tplCopy; - +module.exports.create = function (le) { var PromiseA = require('bluebird'); + var utils = require('./utils'); // merge, tplCopy; var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA); var LeCore = PromiseA.promisifyAll(require('letiny-core')); var crypto = require('crypto'); @@ -28,8 +20,6 @@ module.exports.create = function (defaults, handlers, backend) { } function createAccount(args, handlers) { - args.rsaKeySize = args.rsaKeySize || 2048; - return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { return LeCore.registerNewAccountAsync({ @@ -73,21 +63,19 @@ module.exports.create = function (defaults, handlers, backend) { var now = Date.now(); // TODO check response header on request for cache time - if ((now - ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { - return PromiseA.resolve(ipc.acmeUrls); + if ((now - le._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { + return PromiseA.resolve(le._ipc.acmeUrls); } return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { - ipc.acmeUrlsUpdatedAt = Date.now(); - ipc.acmeUrls = data; + le._ipc.acmeUrlsUpdatedAt = Date.now(); + le._ipc.acmeUrls = data; - return ipc.acmeUrls; + return le._ipc.acmeUrls; }); } function getCertificateAsync(args, defaults, handlers) { - args.rsaKeySize = args.rsaKeySize || 2048; - args.challengeType = args.challengeType || 'http-01'; function log() { if (args.debug || defaults.debug) { @@ -223,7 +211,6 @@ module.exports.create = function (defaults, handlers, backend) { var wrapped = { registerAsync: function (args) { - var utils = require('./lib/common'); var err; if (!Array.isArray(args.domains)) { diff --git a/lib/common.js b/lib/utils.js similarity index 100% rename from lib/common.js rename to lib/utils.js From a0d01815dffa34c424ac081c9cf8bf6b92664970 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 6 Aug 2016 01:32:59 -0400 Subject: [PATCH 06/14] move attachCertInfo to utils.js --- lib/utils.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/utils.js b/lib/utils.js index e00919f..d98b779 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -5,6 +5,18 @@ var homeRe = new RegExp("^~(\\/|\\\|\\" + path.sep + ")"); var re = /^[a-zA-Z0-9\.\-]+$/; var punycode = require('punycode'); +module.exports.attachCertInfo = function (results) { + var getCertInfo = require('./cert-info').getCertInfo; + // XXX Note: Parsing the certificate info comes at a great cost (~500kb) + var certInfo = getCertInfo(results.cert); + + //results.issuedAt = arr[3].mtime.valueOf() + results.issuedAt = Date(certInfo.notBefore.value).valueOf(); // Date.now() + results.expiresAt = Date(certInfo.notAfter.value).valueOf(); + + return results; +}; + module.exports.isValidDomain = function (domain) { if (re.test(domain)) { return domain; From afa10a2c63c5f47552aeab10ec926820c67bb0a9 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 6 Aug 2016 01:33:19 -0400 Subject: [PATCH 07/14] add agreeToTerms --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 13821b9..3b231bb 100644 --- a/index.js +++ b/index.js @@ -34,6 +34,7 @@ LE._undefined = { , rsaKeySize: u , challengeType: u , server: u +, agreeToTerms: u , _ipc: u }; LE._undefine = function (le) { From 16bfac31bb0f1cab93f263ce46772af1e335d100 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 6 Aug 2016 01:34:34 -0400 Subject: [PATCH 08/14] rename in api --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6ec547c..43ef29c 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ le = LE.create({ // Check in-memory cache of certificates for the named domain -le.exists({ domain: 'example.com' }).then(function (results) { +le.check({ domain: 'example.com' }).then(function (results) { if (results) { // we already have certificates return; @@ -156,11 +156,11 @@ le.exists({ domain: 'example.com' }).then(function (results) { // Register Certificate manually - le.register({ + le.get({ domains: ['example.com'] // CHANGE TO YOUR DOMAIN (list for SANS) , email: 'user@email.com' // CHANGE TO YOUR EMAIL - , agreeTos: '' // set to tosUrl string to pre-approve (and skip agreeToTerms) + , agreeTos: '' // set to tosUrl string (or true) to pre-approve (and skip agreeToTerms) , rsaKeySize: 2048 // 2048 or higher , challengeType: 'http-01' // http-01, tls-sni-01, or dns-01 @@ -199,6 +199,12 @@ API The full end-user API is exposed in the example above and includes all relevant options. +``` +le.register +le.get // checkAndRegister +le.check +``` + ### Helper Functions We do expose a few helper functions: From d1c0043f349868f3ced7584212032437131a5886 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 6 Aug 2016 02:05:04 -0400 Subject: [PATCH 09/14] just a little further... --- lib/core.js | 426 ++++++++++++++++++++++++++-------------------------- 1 file changed, 216 insertions(+), 210 deletions(-) diff --git a/lib/core.js b/lib/core.js index a6ec745..bcfbfd2 100644 --- a/lib/core.js +++ b/lib/core.js @@ -7,210 +7,234 @@ module.exports.create = function (le) { var LeCore = PromiseA.promisifyAll(require('letiny-core')); var crypto = require('crypto'); - function attachCertInfo(results) { - var getCertInfo = require('./cert-info').getCertInfo; - // XXX Note: Parsing the certificate info comes at a great cost (~500kb) - var certInfo = getCertInfo(results.cert); + var core = { + // + // Helpers + // + getAcmeUrlsAsync: function (args) { + var now = Date.now(); - //results.issuedAt = arr[3].mtime.valueOf() - results.issuedAt = Date(certInfo.notBefore.value).valueOf(); // Date.now() - results.expiresAt = Date(certInfo.notAfter.value).valueOf(); + // TODO check response header on request for cache time + if ((now - le._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { + return PromiseA.resolve(le._ipc.acmeUrls); + } - return results; - } + return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { + le._ipc.acmeUrlsUpdatedAt = Date.now(); + le._ipc.acmeUrls = data; - function createAccount(args, handlers) { - return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { + return le._ipc.acmeUrls; + }); + } - return LeCore.registerNewAccountAsync({ - email: args.email - , newRegUrl: args._acmeUrls.newReg - , agreeToTerms: function (tosUrl, agree) { - // args.email = email; // already there - args.tosUrl = tosUrl; - handlers.agreeToTerms(args, agree); - } - , accountKeypair: keypair - , debug: defaults.debug || args.debug || handlers.debug - }).then(function (body) { - // TODO XXX use sha256 (the python client uses md5) - // TODO ssh fingerprint (noted on rsa-compat issues page, I believe) - keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex'); - keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex'); + // + // The Main Enchilada + // - var accountId = keypair.publicKeyMd5; - var regr = { body: body }; - var account = {}; + // + // Accounts + // + , accounts: { + registerAsync: function (args) { + return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { - args.accountId = accountId; + return LeCore.registerNewAccountAsync({ + email: args.email + , newRegUrl: args._acmeUrls.newReg + , agreeToTerms: function (tosUrl, agreeCb) { + if (true === args.agreeTos || tosUrl === args.agreeTos || tosUrl === le.agreeToTerms) { + agreeCb(null, tosUrl); + return; + } - account.keypair = keypair; - account.regr = regr; - account.accountId = accountId; - account.id = accountId; + // args.email = email; // already there + // args.domains = domains // already there + args.tosUrl = tosUrl; + le.agreeToTerms(args, agreeCb); + } + , accountKeypair: keypair - args.account = account; + , debug: le.debug || args.debug + }).then(function (body) { + // TODO XXX use sha256 (the python client uses md5) + // TODO ssh fingerprint (noted on rsa-compat issues page, I believe) + keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex'); + keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex'); - return backend.setAccountAsync(args, account).then(function () { - return account; + var accountId = keypair.publicKeyMd5; + var regr = { body: body }; + var account = {}; + + args.accountId = accountId; + + account.keypair = keypair; + account.regr = regr; + account.accountId = accountId; + account.id = accountId; + + args.account = account; + + return le.store.accounts.setAsync(args, account).then(function () { + return account; + }); + }); }); - }); - }); - } - - function getAcmeUrls(args) { - var now = Date.now(); - - // TODO check response header on request for cache time - if ((now - le._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { - return PromiseA.resolve(le._ipc.acmeUrls); - } - - return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { - le._ipc.acmeUrlsUpdatedAt = Date.now(); - le._ipc.acmeUrls = data; - - return le._ipc.acmeUrls; - }); - } - - function getCertificateAsync(args, defaults, handlers) { - - function log() { - if (args.debug || defaults.debug) { - console.log.apply(console, arguments); } - } + // getOrCreateAcmeAccount + , getAsync: function (args) { + return core.accounts.checkAsync(args).then(function (account) { + if (account) { + return le.store.accounts.checkAccount(args); + } else { + return core.accounts.registerAsync(args); + } + }); + } + , checkAsync: function (args) { + return le.store.accounts.checkAccountId(args).then(function (accountId) { - var account = args.account; - var promise; - var keypairOpts = { public: true, pem: true }; - - promise = backend.getPrivatePem(args).then(function (pem) { - return RSA.import({ privateKeyPem: pem }); - }, function (/*err*/) { - return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { - keypair.privateKeyPem = RSA.exportPrivatePem(keypair); - keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); - return backend.setPrivatePem(args, keypair); - }); - }); - - return promise.then(function (domainKeypair) { - log("[le/core.js] get certificate"); - - args.domainKeypair = domainKeypair; - //args.registration = domainKey; - - return LeCore.getCertificateAsync({ - debug: args.debug - - , newAuthzUrl: args._acmeUrls.newAuthz - , newCertUrl: args._acmeUrls.newCert - - , accountKeypair: RSA.import(account.keypair) - , domainKeypair: domainKeypair - , domains: args.domains - , challengeType: args.challengeType - - // - // IMPORTANT - // - // setChallenge and removeChallenge are handed defaults - // instead of args because getChallenge does not have - // access to args - // (args is per-request, defaults is per instance) - // - , setChallenge: function (domain, key, value, done) { - var copy = handlers.merge({ domains: [domain] }, defaults, backendDefaults); - handlers.tplCopy(copy); - - //args.domains = [domain]; - args.domains = args.domains || [domain]; - - if (5 !== handlers.setChallenge.length) { - done(new Error("handlers.setChallenge receives the wrong number of arguments." - + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); - return; + if (!accountId) { + return null; } - handlers.setChallenge(copy, domain, key, value, done); - } - , removeChallenge: function (domain, key, done) { - var copy = handlers.merge({ domains: [domain] }, defaults, backendDefaults); - handlers.tplCopy(copy); - - if (4 !== handlers.removeChallenge.length) { - done(new Error("handlers.removeChallenge receives the wrong number of arguments." - + " You must define removeChallenge as function (opts, domain, key, cb) { }")); - return; - } - - handlers.removeChallenge(copy, domain, key, done); - } - }).then(attachCertInfo); - }).then(function (results) { - // { cert, chain, fullchain, privkey } - - args.pems = results; - return backend.setRegistration(args, defaults, handlers); - }); - } - - function getOrCreateDomainCertificate(args, defaults, handlers) { - if (args.duplicate) { - // we're forcing a refresh via 'dupliate: true' - return getCertificateAsync(args, defaults, handlers); - } - - return wrapped.fetchAsync(args).then(function (certs) { - var halfLife = (certs.expiresAt - certs.issuedAt) / 2; - - if (!certs || (Date.now() - certs.issuedAt) > halfLife) { - // There is no cert available - // Or the cert is more than half-expired - return getCertificateAsync(args, defaults, handlers); - } - - return PromiseA.reject(new Error( - "[ERROR] Certificate issued at '" - + new Date(certs.issuedAt).toISOString() + "' and expires at '" - + new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '" - + new Date(certs.issuedA + halfLife).toISOString() + "'. Set { duplicate: true } to force." - )); - }); - } - - // returns 'account' from lib/accounts { meta, regr, keypair, accountId (id) } - function getOrCreateAcmeAccount(args, defaults, handlers) { - function log() { - if (args.debug) { - console.log.apply(console, arguments); - } - } - - return backend.getAccountId(args).then(function (accountId) { - - // Note: the ACME urls are always fetched fresh on purpose - return getAcmeUrls(args).then(function (urls) { - args._acmeUrls = urls; - - if (accountId) { - log('[le/core.js] use account'); - args.accountId = accountId; - return backend.getAccount(args, handlers); - } else { - log('[le/core.js] create account'); - return createAccount(args, handlers); - } - }); - }); - } - var wrapped = { - registerAsync: function (args) { + // Note: the ACME urls are always fetched fresh on purpose + return core.getAcmeUrlsAsync(args).then(function (urls) { + args._acmeUrls = urls; + + + // return le.store.accounts.checkAccountId(args).then(function (accountId) { + return le.store.accounts.checkAsync(args); + }); + }); + } + } + + , certificates: { + // getCertificateAsync: + registerAsync: function (args) { + + function log() { + if (args.debug || le.debug) { + console.log.apply(console, arguments); + } + } + + var account = args.account; + var keypairOpts = { public: true, pem: true }; + + var promise = le.store.certificates.checkKeypairAsync(args).then(function (keypair) { + return RSA.import(keypair); + }, function (/*err*/) { + return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { + keypair.privateKeyPem = RSA.exportPrivatePem(keypair); + keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); + return le.store.certificates.setKeypairAsync(args, keypair); + }); + }); + + return promise.then(function (domainKeypair) { + log("[le/core.js] get certificate"); + + args.domainKeypair = domainKeypair; + //args.registration = domainKey; + + return LeCore.getCertificateAsync({ + debug: args.debug || le.debug + + , newAuthzUrl: args._acmeUrls.newAuthz + , newCertUrl: args._acmeUrls.newCert + + , accountKeypair: RSA.import(account.keypair) + , domainKeypair: domainKeypair + , domains: args.domains + , challengeType: args.challengeType + + // + // IMPORTANT + // + // setChallenge and removeChallenge are handed defaults + // instead of args because getChallenge does not have + // access to args + // (args is per-request, defaults is per instance) + // + , setChallenge: function (domain, key, value, done) { + var copy = utils.merge({ domains: [domain] }, le); + utils.tplCopy(copy); + + //args.domains = [domain]; + args.domains = args.domains || [domain]; + + if (5 !== le.challenger.set.length) { + done(new Error("le.challenger.set receives the wrong number of arguments." + + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); + return; + } + + le.challenger.set(copy, domain, key, value, done); + } + , removeChallenge: function (domain, key, done) { + var copy = utils.merge({ domains: [domain] }, le); + utils.tplCopy(copy); + + if (4 !== le.challenger.remove.length) { + done(new Error("le.challenger.remove receives the wrong number of arguments." + + " You must define removeChallenge as function (opts, domain, key, cb) { }")); + return; + } + + le.challenger.remove(copy, domain, key, done); + } + }).then(utils.attachCertInfo); + }).then(function (results) { + // { cert, chain, fullchain, privkey } + + args.pems = results; + return le.store.certificates.setAsync(args); + }); + } + // checkAsync + , checkAsync: function (args) { + var copy = utils.merge(args, le); + utils.tplCopy(copy); + + return le.store.certificates.checkAsync(copy).then(utils.attachCertInfo); + } + // getOrCreateDomainCertificate + , getAsync: function (args) { + var copy = utils.merge(args, le); + utils.tplCopy(copy); + + if (args.duplicate) { + // we're forcing a refresh via 'dupliate: true' + return core.certificates.registerAsync(args); + } + + return core.certificates.checkAsync(args).then(function (certs) { + var renewableAt = certs.expiresAt - le.renewWithin; + //var halfLife = (certs.expiresAt - certs.issuedAt) / 2; + //var renewable = (Date.now() - certs.issuedAt) > halfLife; + + if (!certs || Date.now() >= renewableAt) { + // There is no cert available + // Or the cert is more than half-expired + return core.certificates.registerAsync(args); + } + + return PromiseA.reject(new Error( + "[ERROR] Certificate issued at '" + + new Date(certs.issuedAt).toISOString() + "' and expires at '" + + new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '" + + new Date(renewableAt).toISOString() + "'. Set { duplicate: true } to force." + )); + }); + } + } + + // returns 'account' from lib/accounts { meta, regr, keypair, accountId (id) } + , registerAsync: function (args) { var err; if (!Array.isArray(args.domains)) { @@ -226,16 +250,16 @@ module.exports.create = function (le) { return PromiseA.reject(err); } - var copy = handlers.merge(args, defaults, backendDefaults); - handlers.tplCopy(copy); + var copy = utils.merge(args, le); + utils.tplCopy(copy); - return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { + return core.accounts.getAsync(copy).then(function (account) { copy.account = account; return backend.getOrCreateRenewal(copy).then(function (pyobj) { copy.pyobj = pyobj; - return getOrCreateDomainCertificate(copy, defaults, handlers); + return core.certificates.getAsync(copy); }); }).then(function (result) { return result; @@ -243,25 +267,7 @@ module.exports.create = function (le) { return PromiseA.reject(err); }); } - , getOrCreateAccount: function (args) { - return createAccount(args, handlers); - } - , configureAsync: function (hargs) { - var copy = handlers.merge(hargs, defaults, backendDefaults); - handlers.tplCopy(copy); - - return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { - copy.account = account; - return backend.getOrCreateRenewal(copy); - }); - } - , fetchAsync: function (args) { - var copy = handlers.merge(args, defaults); - handlers.tplCopy(copy); - - return backend.fetchAsync(copy).then(attachCertInfo); - } }; - return wrapped; + return core; }; From 635b130ab37dae892b0acfba024e56d476fafb36 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 7 Aug 2016 02:02:02 -0400 Subject: [PATCH 10/14] just a little more... --- index.js | 4 +- lib/core.js | 203 +++++++++++++++++++++++---------------------------- lib/utils.js | 2 + 3 files changed, 96 insertions(+), 113 deletions(-) diff --git a/index.js b/index.js index 3b231bb..341d1dc 100644 --- a/index.js +++ b/index.js @@ -99,12 +99,12 @@ LE.create = function (le) { } le.register = function (args) { - return le.core.registerAsync(args); + return le.core.certificates.getAsync(args); }; le.check = function (args) { // TODO must return email, domains, tos, pems - return le.core.fetchAsync(args); + return le.core.certificates.checkAsync(args); }; le.middleware = function () { diff --git a/lib/core.js b/lib/core.js index bcfbfd2..ed1f9d3 100644 --- a/lib/core.js +++ b/lib/core.js @@ -113,99 +113,111 @@ module.exports.create = function (le) { } , certificates: { - // getCertificateAsync: registerAsync: function (args) { + var err; + var copy = utils.merge(args, le); + args = utils.tplCopy(copy); - function log() { - if (args.debug || le.debug) { - console.log.apply(console, arguments); - } + if (!Array.isArray(args.domains)) { + return PromiseA.reject(new Error('args.domains should be an array of domains')); } - var account = args.account; - var keypairOpts = { public: true, pem: true }; + if (!(args.domains.length && args.domains.every(utils.isValidDomain))) { + // NOTE: this library can't assume to handle the http loopback + // (or dns-01 validation may be used) + // so we do not check dns records or attempt a loopback here + err = new Error("invalid domain name(s): '" + args.domains + "'"); + err.code = "INVALID_DOMAIN"; + return PromiseA.reject(err); + } - var promise = le.store.certificates.checkKeypairAsync(args).then(function (keypair) { - return RSA.import(keypair); - }, function (/*err*/) { - return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { - keypair.privateKeyPem = RSA.exportPrivatePem(keypair); - keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); - return le.store.certificates.setKeypairAsync(args, keypair); + return core.accounts.getAsync(copy).then(function (account) { + copy.account = account; + + //var account = args.account; + var keypairOpts = { public: true, pem: true }; + + var promise = le.store.certificates.checkKeypairAsync(args).then(function (keypair) { + return RSA.import(keypair); + }, function (/*err*/) { + return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { + keypair.privateKeyPem = RSA.exportPrivatePem(keypair); + keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); + return le.store.certificates.setKeypairAsync(args, keypair); + }); + }); + + return promise.then(function (domainKeypair) { + args.domainKeypair = domainKeypair; + //args.registration = domainKey; + + return LeCore.getCertificateAsync({ + debug: args.debug || le.debug + + , newAuthzUrl: args._acmeUrls.newAuthz + , newCertUrl: args._acmeUrls.newCert + + , accountKeypair: RSA.import(account.keypair) + , domainKeypair: domainKeypair + , domains: args.domains + , challengeType: args.challengeType + + // + // IMPORTANT + // + // setChallenge and removeChallenge are handed defaults + // instead of args because getChallenge does not have + // access to args + // (args is per-request, defaults is per instance) + // + , setChallenge: function (domain, key, value, done) { + var copy = utils.merge({ domains: [domain] }, le); + utils.tplCopy(copy); + + //args.domains = [domain]; + args.domains = args.domains || [domain]; + + if (5 !== le.challenger.set.length) { + done(new Error("le.challenger.set receives the wrong number of arguments." + + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); + return; + } + + le.challenger.set(copy, domain, key, value, done); + } + , removeChallenge: function (domain, key, done) { + var copy = utils.merge({ domains: [domain] }, le); + utils.tplCopy(copy); + + if (4 !== le.challenger.remove.length) { + done(new Error("le.challenger.remove receives the wrong number of arguments." + + " You must define removeChallenge as function (opts, domain, key, cb) { }")); + return; + } + + le.challenger.remove(copy, domain, key, done); + } + }).then(utils.attachCertInfo); + }).then(function (results) { + // { cert, chain, fullchain, privkey } + + args.pems = results; + return le.store.certificates.setAsync(args).then(function () { + return results; + }); }); }); - - return promise.then(function (domainKeypair) { - log("[le/core.js] get certificate"); - - args.domainKeypair = domainKeypair; - //args.registration = domainKey; - - return LeCore.getCertificateAsync({ - debug: args.debug || le.debug - - , newAuthzUrl: args._acmeUrls.newAuthz - , newCertUrl: args._acmeUrls.newCert - - , accountKeypair: RSA.import(account.keypair) - , domainKeypair: domainKeypair - , domains: args.domains - , challengeType: args.challengeType - - // - // IMPORTANT - // - // setChallenge and removeChallenge are handed defaults - // instead of args because getChallenge does not have - // access to args - // (args is per-request, defaults is per instance) - // - , setChallenge: function (domain, key, value, done) { - var copy = utils.merge({ domains: [domain] }, le); - utils.tplCopy(copy); - - //args.domains = [domain]; - args.domains = args.domains || [domain]; - - if (5 !== le.challenger.set.length) { - done(new Error("le.challenger.set receives the wrong number of arguments." - + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); - return; - } - - le.challenger.set(copy, domain, key, value, done); - } - , removeChallenge: function (domain, key, done) { - var copy = utils.merge({ domains: [domain] }, le); - utils.tplCopy(copy); - - if (4 !== le.challenger.remove.length) { - done(new Error("le.challenger.remove receives the wrong number of arguments." - + " You must define removeChallenge as function (opts, domain, key, cb) { }")); - return; - } - - le.challenger.remove(copy, domain, key, done); - } - }).then(utils.attachCertInfo); - }).then(function (results) { - // { cert, chain, fullchain, privkey } - - args.pems = results; - return le.store.certificates.setAsync(args); - }); } - // checkAsync , checkAsync: function (args) { var copy = utils.merge(args, le); utils.tplCopy(copy); + // returns pems return le.store.certificates.checkAsync(copy).then(utils.attachCertInfo); } - // getOrCreateDomainCertificate , getAsync: function (args) { var copy = utils.merge(args, le); - utils.tplCopy(copy); + args = utils.tplCopy(copy); if (args.duplicate) { // we're forcing a refresh via 'dupliate: true' @@ -229,44 +241,13 @@ module.exports.create = function (le) { + new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '" + new Date(renewableAt).toISOString() + "'. Set { duplicate: true } to force." )); + }).then(function (results) { + // returns pems + return results; }); } } - // returns 'account' from lib/accounts { meta, regr, keypair, accountId (id) } - , registerAsync: function (args) { - var err; - - if (!Array.isArray(args.domains)) { - return PromiseA.reject(new Error('args.domains should be an array of domains')); - } - - if (!(args.domains.length && args.domains.every(utils.isValidDomain))) { - // NOTE: this library can't assume to handle the http loopback - // (or dns-01 validation may be used) - // so we do not check dns records or attempt a loopback here - err = new Error("invalid domain name(s): '" + args.domains + "'"); - err.code = "INVALID_DOMAIN"; - return PromiseA.reject(err); - } - - var copy = utils.merge(args, le); - utils.tplCopy(copy); - - return core.accounts.getAsync(copy).then(function (account) { - copy.account = account; - - return backend.getOrCreateRenewal(copy).then(function (pyobj) { - - copy.pyobj = pyobj; - return core.certificates.getAsync(copy); - }); - }).then(function (result) { - return result; - }, function (err) { - return PromiseA.reject(err); - }); - } }; return core; diff --git a/lib/utils.js b/lib/utils.js index d98b779..1755f15 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -75,4 +75,6 @@ module.exports.tplCopy = function (copy) { copy[key] = copy[key].replace(':' + tplname, tpls[tplname]); }); }); + + return copy; }; From 6aaec506a879b70de80d6c6daa67736e9eea519d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 8 Aug 2016 09:13:34 -0600 Subject: [PATCH 11/14] Update README.md --- README.md | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 43ef29c..44ea7fd 100644 --- a/README.md +++ b/README.md @@ -7,23 +7,12 @@ | [letsencrypt-hapi](https://github.com/Daplie/letsencrypt-hapi) | -letsencrypt (v2) +letsencrypt =========== Automatic [Let's Encrypt](https://letsencrypt.org) HTTPS / TLS / SSL Certificates for node.js - * [Automatic HTTPS with ExpressJS](https://github.com/Daplie/letsencrypt-express) - * [Automatic live renewal](https://github.com/Daplie/letsencrypt-express#how-automatic) - * On-the-fly HTTPS certificates for Dynamic DNS (in-process, no server restart) - * Works with node cluster out of the box - * usable [via commandline](https://github.com/Daplie/letsencrypt-cli) as well - * Free SSL (HTTPS Certificates for TLS) - * [90-day certificates](https://letsencrypt.org/2015/11/09/why-90-days.html) - -**See Also** - -* [Let's Encrypt in (exactly) 90 seconds with Caddy](https://daplie.com/articles/lets-encrypt-in-literally-90-seconds/) -* [lego](https://github.com/xenolf/lego): Let's Encrypt for golang +Free SLL with [90-day](https://letsencrypt.org/2015/11/09/why-90-days.html) HTTPS / TLS Certificates STOP ==== From c82ccbbe3c34a61dbdf8c23e7c3a119ed85febc9 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 8 Aug 2016 09:14:08 -0600 Subject: [PATCH 12/14] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 44ea7fd..ea57244 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ It's very simple and easy to use, but also very complete and easy to extend and ### Overly Simplified Example -Against my better judgement I'm providing a terribly oversimplified exmaple +Against my better judgement I'm providing a terribly oversimplified example of how to use this library: ```javascript From 3590842ce8964395efcd574c847b14789f570723 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 8 Aug 2016 11:21:33 -0400 Subject: [PATCH 13/14] make shim of letiny-core possible #37 --- index.js | 21 +++++++++++++++++---- lib/core.js | 7 +++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 341d1dc..d1774fd 100644 --- a/index.js +++ b/index.js @@ -24,7 +24,8 @@ Object.keys(LE.defaults).forEach(function (key) { // show all possible options var u; // undefined LE._undefined = { - store: u + acme: u +, store: u , challenger: u , register: u , check: u @@ -49,6 +50,7 @@ LE._undefine = function (le) { LE.create = function (le) { var PromiseA = require('bluebird'); + le.acme = le.acme || leCore; le.store = le.store || require('le-store-certbot').create({ debug: le.debug }); le.challenger = le.challenger || require('le-store-certbot').create({ debug: le.debug }); le.core = require('./lib/core'); @@ -72,13 +74,24 @@ LE.create = function (le) { le.server = LE.productionServerUrl; } + if (le.acme.create) { + le.acme = le.acme.create(le); + } + le.acme = PromiseA.promisifyAll(le.acme); + le._acmeOpts = le.acme.getOptions(); + Object.keys(le._acmeOpts).forEach(function (key) { + if (!(key in le)) { + le[key] = le._acmeOpts[key]; + } + }); + if (le.store.create) { le.store = le.store.create(le); } le.store = PromiseA.promisifyAll(le.store); le._storeOpts = le.store.getOptions(); Object.keys(le._storeOpts).forEach(function (key) { - if (!(key in le._storeOpts)) { + if (!(key in le)) { le[key] = le._storeOpts[key]; } }); @@ -88,8 +101,8 @@ LE.create = function (le) { } le.challenger = PromiseA.promisifyAll(le.challenger); le._challengerOpts = le.challenger.getOptions(); - Object.keys(le._storeOpts).forEach(function (key) { - if (!(key in le._challengerOpts)) { + Object.keys(le._challengerOpts).forEach(function (key) { + if (!(key in le)) { le[key] = le._challengerOpts[key]; } }); diff --git a/lib/core.js b/lib/core.js index ed1f9d3..15ff111 100644 --- a/lib/core.js +++ b/lib/core.js @@ -4,7 +4,6 @@ module.exports.create = function (le) { var PromiseA = require('bluebird'); var utils = require('./utils'); // merge, tplCopy; var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA); - var LeCore = PromiseA.promisifyAll(require('letiny-core')); var crypto = require('crypto'); var core = { @@ -19,7 +18,7 @@ module.exports.create = function (le) { return PromiseA.resolve(le._ipc.acmeUrls); } - return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { + return le.acme.getAcmeUrlsAsync(args.server).then(function (data) { le._ipc.acmeUrlsUpdatedAt = Date.now(); le._ipc.acmeUrls = data; @@ -39,7 +38,7 @@ module.exports.create = function (le) { registerAsync: function (args) { return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { - return LeCore.registerNewAccountAsync({ + return le.acme.registerNewAccountAsync({ email: args.email , newRegUrl: args._acmeUrls.newReg , agreeToTerms: function (tosUrl, agreeCb) { @@ -151,7 +150,7 @@ module.exports.create = function (le) { args.domainKeypair = domainKeypair; //args.registration = domainKey; - return LeCore.getCertificateAsync({ + return le.acme.getCertificateAsync({ debug: args.debug || le.debug , newAuthzUrl: args._acmeUrls.newAuthz From 25f8b591db2d2d7dbb1140a5c1483dcd7851ff10 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 8 Aug 2016 15:17:09 -0400 Subject: [PATCH 14/14] now passes tests for invalid account --- index.js | 15 +-- lib/core.js | 230 +++++++++++++++++++++++----------------- lib/utils.js | 29 ++++- tests/create-account.js | 117 ++++++++++++++++++++ 4 files changed, 285 insertions(+), 106 deletions(-) create mode 100644 tests/create-account.js diff --git a/index.js b/index.js index d1774fd..d5b5595 100644 --- a/index.js +++ b/index.js @@ -1,19 +1,20 @@ 'use strict'; -var leCore = require('letiny-core'); +var ACME = require('le-acme-core').ACME; var LE = module.exports; +LE.LE = LE; // in-process cache, shared between all instances var ipc = {}; LE.defaults = { - productionServerUrl: leCore.productionServerUrl -, stagingServerUrl: leCore.stagingServerUrl + productionServerUrl: ACME.productionServerUrl +, stagingServerUrl: ACME.stagingServerUrl -, rsaKeySize: leCore.rsaKeySize || 2048 -, challengeType: leCore.challengeType || 'http-01' +, rsaKeySize: ACME.rsaKeySize || 2048 +, challengeType: ACME.challengeType || 'http-01' -, acmeChallengePrefix: leCore.acmeChallengePrefix +, acmeChallengePrefix: ACME.acmeChallengePrefix }; // backwards compat @@ -50,7 +51,7 @@ LE._undefine = function (le) { LE.create = function (le) { var PromiseA = require('bluebird'); - le.acme = le.acme || leCore; + le.acme = le.acme || ACME.create({ debug: le.debug }); le.store = le.store || require('le-store-certbot').create({ debug: le.debug }); le.challenger = le.challenger || require('le-store-certbot').create({ debug: le.debug }); le.core = require('./lib/core'); diff --git a/lib/core.js b/lib/core.js index 15ff111..92fdb1e 100644 --- a/lib/core.js +++ b/lib/core.js @@ -36,77 +36,101 @@ module.exports.create = function (le) { // , accounts: { registerAsync: function (args) { - return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { + var err; - return le.acme.registerNewAccountAsync({ - email: args.email - , newRegUrl: args._acmeUrls.newReg - , agreeToTerms: function (tosUrl, agreeCb) { - if (true === args.agreeTos || tosUrl === args.agreeTos || tosUrl === le.agreeToTerms) { - agreeCb(null, tosUrl); - return; - } + if (!args.email || !args.agreeTos || (parseInt(args.rsaKeySize, 10) < 2048)) { + err = new Error( + "In order to register an account both 'email' and 'agreeTos' must be present" + + " and 'rsaKeySize' must be 2048 or greater." + ); + err.code = 'E_ARGS'; + return PromiseA.reject(err); + } - // args.email = email; // already there - // args.domains = domains // already there - args.tosUrl = tosUrl; - le.agreeToTerms(args, agreeCb); - } - , accountKeypair: keypair + return utils.testEmail(args.email).then(function () { - , debug: le.debug || args.debug - }).then(function (body) { - // TODO XXX use sha256 (the python client uses md5) - // TODO ssh fingerprint (noted on rsa-compat issues page, I believe) - keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex'); - keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex'); + return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { + // Note: the ACME urls are always fetched fresh on purpose + // TODO is this the right place for this? + return core.getAcmeUrlsAsync(args).then(function (urls) { + args._acmeUrls = urls; - var accountId = keypair.publicKeyMd5; - var regr = { body: body }; - var account = {}; + return le.acme.registerNewAccountAsync({ + email: args.email + , newRegUrl: args._acmeUrls.newReg + , agreeToTerms: function (tosUrl, agreeCb) { + if (true === args.agreeTos || tosUrl === args.agreeTos || tosUrl === le.agreeToTerms) { + agreeCb(null, tosUrl); + return; + } - args.accountId = accountId; + // args.email = email; // already there + // args.domains = domains // already there + args.tosUrl = tosUrl; + le.agreeToTerms(args, agreeCb); + } + , accountKeypair: keypair - account.keypair = keypair; - account.regr = regr; - account.accountId = accountId; - account.id = accountId; + , debug: le.debug || args.debug + }).then(function (body) { + // TODO XXX use sha256 (the python client uses md5) + // TODO ssh fingerprint (noted on rsa-compat issues page, I believe) + keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex'); + keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex'); - args.account = account; + var accountId = keypair.publicKeyMd5; + var regr = { body: body }; + var account = {}; - return le.store.accounts.setAsync(args, account).then(function () { - return account; + args.accountId = accountId; + + account.keypair = keypair; + account.regr = regr; + account.accountId = accountId; + account.id = accountId; + + args.account = account; + + return le.store.accounts.setAsync(args, account).then(function () { + return account; + }); + }); }); }); }); } - // getOrCreateAcmeAccount + , getAsync: function (args) { return core.accounts.checkAsync(args).then(function (account) { - if (account) { - return le.store.accounts.checkAccount(args); - } else { - return core.accounts.registerAsync(args); - } + if (account) { + return account; + } else { + return core.accounts.registerAsync(args); + } }); } - , checkAsync: function (args) { - return le.store.accounts.checkAccountId(args).then(function (accountId) { - if (!accountId) { + , checkAsync: function (args) { + var requiredArgs = ['accountId', 'email', 'domains', 'domain']; + if (!requiredArgs.some(function (key) { return -1 !== Object.keys(args).indexOf(key) })) { + return PromiseA.reject(new Error( + "In order to register or retrieve an account one of '" + requiredArgs.join("', '") + "' must be present" + )); + } + + var copy = utils.merge(args, le); + args = utils.tplCopy(copy); + + return le.store.accounts.checkAsync(args).then(function (account) { + + if (!account) { return null; } - args.accountId = accountId; + args.account = account; + args.accountId = account.id; - // Note: the ACME urls are always fetched fresh on purpose - return core.getAcmeUrlsAsync(args).then(function (urls) { - args._acmeUrls = urls; - - - // return le.store.accounts.checkAccountId(args).then(function (accountId) { - return le.store.accounts.checkAsync(args); - }); + return account; }); } } @@ -150,55 +174,61 @@ module.exports.create = function (le) { args.domainKeypair = domainKeypair; //args.registration = domainKey; - return le.acme.getCertificateAsync({ - debug: args.debug || le.debug + // Note: the ACME urls are always fetched fresh on purpose + // TODO is this the right place for this? + return core.getAcmeUrlsAsync(args).then(function (urls) { + args._acmeUrls = urls; - , newAuthzUrl: args._acmeUrls.newAuthz - , newCertUrl: args._acmeUrls.newCert + return le.acme.getCertificateAsync({ + debug: args.debug || le.debug - , accountKeypair: RSA.import(account.keypair) - , domainKeypair: domainKeypair - , domains: args.domains - , challengeType: args.challengeType + , newAuthzUrl: args._acmeUrls.newAuthz + , newCertUrl: args._acmeUrls.newCert - // - // IMPORTANT - // - // setChallenge and removeChallenge are handed defaults - // instead of args because getChallenge does not have - // access to args - // (args is per-request, defaults is per instance) - // - , setChallenge: function (domain, key, value, done) { - var copy = utils.merge({ domains: [domain] }, le); - utils.tplCopy(copy); + , accountKeypair: RSA.import(account.keypair) + , domainKeypair: domainKeypair + , domains: args.domains + , challengeType: args.challengeType - //args.domains = [domain]; - args.domains = args.domains || [domain]; + // + // IMPORTANT + // + // setChallenge and removeChallenge are handed defaults + // instead of args because getChallenge does not have + // access to args + // (args is per-request, defaults is per instance) + // + , setChallenge: function (domain, key, value, done) { + var copy = utils.merge({ domains: [domain] }, le); + utils.tplCopy(copy); - if (5 !== le.challenger.set.length) { - done(new Error("le.challenger.set receives the wrong number of arguments." - + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); - return; + //args.domains = [domain]; + args.domains = args.domains || [domain]; + + if (5 !== le.challenger.set.length) { + done(new Error("le.challenger.set receives the wrong number of arguments." + + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); + return; + } + + le.challenger.set(copy, domain, key, value, done); } + , removeChallenge: function (domain, key, done) { + var copy = utils.merge({ domains: [domain] }, le); + utils.tplCopy(copy); - le.challenger.set(copy, domain, key, value, done); - } - , removeChallenge: function (domain, key, done) { - var copy = utils.merge({ domains: [domain] }, le); - utils.tplCopy(copy); + if (4 !== le.challenger.remove.length) { + done(new Error("le.challenger.remove receives the wrong number of arguments." + + " You must define removeChallenge as function (opts, domain, key, cb) { }")); + return; + } - if (4 !== le.challenger.remove.length) { - done(new Error("le.challenger.remove receives the wrong number of arguments." - + " You must define removeChallenge as function (opts, domain, key, cb) { }")); - return; + le.challenger.remove(copy, domain, key, done); } - - le.challenger.remove(copy, domain, key, done); - } - }).then(utils.attachCertInfo); + }).then(utils.attachCertInfo); + }); }).then(function (results) { - // { cert, chain, fullchain, privkey } + // { cert, chain, privkey } args.pems = results; return le.store.certificates.setAsync(args).then(function () { @@ -207,6 +237,10 @@ module.exports.create = function (le) { }); }); } + , renewAsync: function (args) { + // TODO fetch email address if not present + return core.certificates.registerAsync(args); + } , checkAsync: function (args) { var copy = utils.merge(args, le); utils.tplCopy(copy); @@ -218,20 +252,20 @@ module.exports.create = function (le) { var copy = utils.merge(args, le); args = utils.tplCopy(copy); - if (args.duplicate) { - // we're forcing a refresh via 'dupliate: true' - return core.certificates.registerAsync(args); - } - return core.certificates.checkAsync(args).then(function (certs) { + if (!certs) { + // There is no cert available + return core.certificates.registerAsync(args); + } + var renewableAt = certs.expiresAt - le.renewWithin; //var halfLife = (certs.expiresAt - certs.issuedAt) / 2; //var renewable = (Date.now() - certs.issuedAt) > halfLife; - if (!certs || Date.now() >= renewableAt) { - // There is no cert available - // Or the cert is more than half-expired - return core.certificates.registerAsync(args); + if (args.duplicate || Date.now() >= renewableAt) { + // The cert is more than half-expired + // We're forcing a refresh via 'dupliate: true' + return core.certificates.renewAsync(args); } return PromiseA.reject(new Error( diff --git a/lib/utils.js b/lib/utils.js index 1755f15..eaaad17 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,6 +4,8 @@ var path = require('path'); var homeRe = new RegExp("^~(\\/|\\\|\\" + path.sep + ")"); var re = /^[a-zA-Z0-9\.\-]+$/; var punycode = require('punycode'); +var PromiseA = require('bluebird'); +var dns = PromiseA.promisifyAll(require('dns')); module.exports.attachCertInfo = function (results) { var getCertInfo = require('./cert-info').getCertInfo; @@ -33,7 +35,7 @@ module.exports.isValidDomain = function (domain) { module.exports.merge = function (/*defaults, args*/) { var allDefaults = Array.prototype.slice.apply(arguments); - var args = args.shift(); + var args = allDefaults.shift(); var copy = {}; allDefaults.forEach(function (defaults) { @@ -78,3 +80,28 @@ module.exports.tplCopy = function (copy) { return copy; }; + +module.exports.testEmail = function (email) { + var parts = (email||'').split('@'); + var err; + + if (2 !== parts.length || !parts[0] || !parts[1]) { + err = new Error("malformed email address '" + email + "'"); + err.code = 'E_EMAIL'; + return PromiseA.reject(err); + } + + return dns.resolveMxAsync(parts[1]).then(function (records) { + // records only returns when there is data + if (!records.length) { + throw new Error("sanity check fail: success, but no MX records returned"); + } + return email; + }, function (err) { + if ('ENODATA' === err.code) { + err = new Error("no MX records found for '" + parts[1] + "'"); + err.code = 'E_EMAIL'; + return PromiseA.reject(err); + } + }); +}; diff --git a/tests/create-account.js b/tests/create-account.js new file mode 100644 index 0000000..32a775f --- /dev/null +++ b/tests/create-account.js @@ -0,0 +1,117 @@ +'use strict'; + +var LE = require('../').LE; +var le = LE.create({ + server: 'staging' +, acme: require('le-acme-core').ACME.create() +, store: require('le-store-certbot').create({ + configDir: '~/letsencrypt.test/etc/' + }) +}); + +var testId = Math.round(Date.now() / 1000).toString(); +var fakeEmail = 'coolaj86+le.' + testId + '@example.com'; +var testEmail = 'coolaj86+le.' + testId + '@example.com'; +var testAccount; + +var tests = [ + function () { + return le.core.accounts.checkAsync({ + email: testEmail + }).then(function (account) { + if (account) { + console.error(account); + throw new Error("Test account should not exist."); + } + }); + } +, function () { + return le.core.accounts.registerAsync({ + email: testEmail + , agreeTos: false + , rsaKeySize: 2048 + }).then(function (/*account*/) { + throw new Error("Should not register if 'agreeTos' is not truthy."); + }, function (err) { + if (err.code !== 'E_ARGS') { + throw err; + } + }); + } +, function () { + return le.core.accounts.registerAsync({ + email: testEmail + , agreeTos: true + , rsaKeySize: 1024 + }).then(function (/*account*/) { + throw new Error("Should not register if 'rsaKeySize' is less than 2048."); + }, function (err) { + if (err.code !== 'E_ARGS') { + throw err; + } + }); + } +, function () { + return le.core.accounts.registerAsync({ + email: fakeEmail + , agreeTos: true + , rsaKeySize: 2048 + }).then(function (/*account*/) { + // TODO test mx record + throw new Error("Registration should NOT succeed with a bad email address."); + }, function (err) { + if (err.code !== 'E_EMAIL') { + throw err; + } + }); + } +, function () { + throw new Error('NOT IMPLEMENTED'); + return le.core.accounts.registerAsync({ + email: 'coolaj86+le.' + testId + '@example.com' + , agreeTos: true + , rsaKeySize: 2048 + }).then(function (account) { + testAccount = account; + if (!account) { + throw new Error("Registration should always return a new account."); + } + if (!account.email) { + throw new Error("Registration should return the email."); + } + if (!account.id) { + throw new Error("Registration should return the account id."); + } + }); + } +, function () { + return le.core.accounts.checkAsync({ + email: testAccount.email + }).then(function (account) { + if (!account) { + throw new Error("Test account should exist when searched by email."); + } + }); + } +, function () { + return le.core.accounts.checkAsync({ + accountId: testAccount.id + }).then(function (account) { + if (!account) { + throw new Error("Test account should exist when searched by account id."); + } + }); + } +]; + +function run() { + var test = tests.shift(); + if (!test) { + console.info('All tests passed'); + return; + } + + test().then(run); +} + +run();