From fd02f44c13f26427c49bec2d1ceace1338cb7086 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 4 Aug 2016 18:49:35 -0400 Subject: [PATCH] partial refactor --- examples/README.md | 7 +- examples/simple.js | 64 ++++ index.js | 69 ++--- lib/accounts.js | 195 ------------ lib/common.js | 79 ++--- lib/core.js | 646 ++++++++++++---------------------------- lib/default-handlers.js | 10 +- lib/pycompat.js | 517 ++++++++++++++++++++++++++++++++ package.json | 2 - 9 files changed, 828 insertions(+), 761 deletions(-) create mode 100644 examples/simple.js delete mode 100644 lib/accounts.js create mode 100644 lib/pycompat.js diff --git a/examples/README.md b/examples/README.md index 9976c81..47fb61c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -29,9 +29,6 @@ No, I wanted node-letsencrypt ============================= Well, take a look at the API in the main README -and you can also check out the [scraps](https://github.com/Daplie/node-letsencrypt/tree/master/scraps). +and you can also check out the code in the repos above. -Feel free to create issues for examples that don't work and pull requests if you fix one. - -And please, please, do open an issue. We haven't updated the scrap examples -(hence being moved), but we do have it on the roadmap to bring back some raw API examples. +Feel free to open an issues to request any particular type of example. diff --git a/examples/simple.js b/examples/simple.js new file mode 100644 index 0000000..84dc3fc --- /dev/null +++ b/examples/simple.js @@ -0,0 +1,64 @@ +'use strict'; + +//var le = require('letsencrypt'); +var LE = require('../'); +var db = {}; + +var config = { + server: LE.stagingServerUrl // or LE.productionServerUrl + +, configDir: require('homedir')() + '/letsencrypt/etc' // or /etc/letsencrypt or wherever + +, privkeyPath: ':config/live/:hostname/privkey.pem' // +, fullchainPath: ':config/live/:hostname/fullchain.pem' // Note: both that :config and :hostname +, certPath: ':config/live/:hostname/cert.pem' // will be templated as expected +, chainPath: ':config/live/:hostname/chain.pem' // + +, rsaKeySize: 2048 + +, debug: true +}; + +var handlers = { + setChallenge: function (opts, hostname, key, val, cb) { // called during the ACME server handshake, before validation + db[key] = { + hostname: hostname + , key: key + , val: val + }; + + cb(null); + } +, removeChallenge: function (opts, hostname, key, cb) { // called after validation on both success and failure + db[key] = null; + cb(null); + } +, getChallenge: function (opts, hostname, key, cb) { // this is special because it is called by the webserver + cb(null, db[key].val); // (see letsencrypt-cli/bin & letsencrypt-express/standalone), + // not by the library itself + } +, agreeToTerms: function (tosUrl, cb) { // gives you an async way to expose the legal agreement + cb(null, tosUrl); // (terms of use) to your users before accepting + } +}; + +var le = LE.create(config, handlers); + // checks :conf/renewal/:hostname.conf +le.register({ // and either renews or registers + domains: ['example.com'] // CHANGE TO YOUR DOMAIN +, email: 'user@email.com' // CHANGE TO YOUR EMAIL +, agreeTos: false // set to true to automatically accept an agreement + // which you have pre-approved (not recommended) +, rsaKeySize: 2048 +}, function (err) { + if (err) { + // Note: you must have a webserver running + // and expose handlers.getChallenge to it + // in order to pass validation + // See letsencrypt-cli and or letsencrypt-express + console.error('[Error]: node-letsencrypt/examples/standalone'); + console.error(err.stack); + } else { + console.log('success'); + } +}); diff --git a/index.js b/index.js index f7233db..f63cd7a 100644 --- a/index.js +++ b/index.js @@ -4,45 +4,34 @@ var PromiseA = require('bluebird'); var leCore = require('letiny-core'); +var utils = require('./lib/common'); var merge = require('./lib/common').merge; var tplCopy = require('./lib/common').tplCopy; var LE = module.exports; -LE.productionServerUrl = leCore.productionServerUrl; -LE.stagingServerUrl = leCore.stagingServerUrl; -LE.configDir = leCore.configDir; -LE.logsDir = leCore.logsDir; -LE.workDir = leCore.workDir; -LE.acmeChallengPrefix = leCore.acmeChallengPrefix; -LE.knownEndpoints = leCore.knownEndpoints; -LE.privkeyPath = ':config/live/:hostname/privkey.pem'; -LE.fullchainPath = ':config/live/:hostname/fullchain.pem'; -LE.certPath = ':config/live/:hostname/cert.pem'; -LE.chainPath = ':config/live/:hostname/chain.pem'; -LE.renewalPath = ':config/renewal/:hostname.conf'; -LE.accountsDir = ':config/accounts/:server'; +LE.merge = require('./lib/common').merge; + LE.defaults = { - privkeyPath: LE.privkeyPath -, fullchainPath: LE.fullchainPath -, certPath: LE.certPath -, chainPath: LE.chainPath -, renewalPath: LE.renewalPath -, accountsDir: LE.accountsDir -, server: LE.productionServerUrl + server: leCore.productionServerUrl +, stagingServer: leCore.stagingServerUrl +, liveServer: leCore.productionServerUrl + +, productionServerUrl: leCore.productionServerUrl +, stagingServerUrl: leCore.stagingServerUrl + +, acmeChallengePrefix: leCore.acmeChallengePrefix }; // backwards compat -LE.stagingServer = leCore.stagingServerUrl; -LE.liveServer = leCore.productionServerUrl; -LE.knownUrls = leCore.knownEndpoints; - -LE.merge = require('./lib/common').merge; -LE.tplConfigDir = require('./lib/common').tplConfigDir; +Object.keys(LE.defaults).forEach(function (key) { + LE[key] = LE.defaults[key]; +}); // backend, defaults, handlers LE.create = function (defaults, handlers, backend) { - if (!backend) { backend = require('./lib/core'); } + var Backend = require('./lib/core'); + if (!backend) { backend = require('./lib/pycompat').create(defaults); } if (!handlers) { handlers = {}; } if (!handlers.lifetime) { handlers.lifetime = 90 * 24 * 60 * 60 * 1000; } if (!handlers.renewWithin) { handlers.renewWithin = 3 * 24 * 60 * 60 * 1000; } @@ -64,14 +53,15 @@ LE.create = function (defaults, handlers, backend) { // the request it came from... it's kinda stateless in that way // but realistically there only needs to be one handler and one // "directory" for this. It's not that big of a deal. - var defaultos = LE.merge(defaults, {}); + var defaultos = LE.merge({}, defaults); var getChallenge = require('./lib/default-handlers').getChallenge; - var copy = merge(defaults, { domains: [hostname] }); + var copy = merge({ domains: [hostname] }, defaults); tplCopy(copy); defaultos.domains = [hostname]; if (3 === getChallenge.length) { + console.warn('[WARNING] Deprecated use. Define getChallenge as function (opts, domain, key, cb) { }'); getChallenge(defaultos, key, done); } else if (4 === getChallenge.length) { @@ -102,22 +92,10 @@ LE.create = function (defaults, handlers, backend) { } handlers.agreeToTerms = require('./lib/default-handlers').agreeToTerms; } - if ('function' === typeof backend.create) { - backend = backend.create(defaults, handlers); - } - else { - // ignore - // this backend was created the v1.0.0 way - } - - // replaces strings of workDir, certPath, etc - // if they have :config/etc/live or :conf/etc/archive - // to instead have the path of the configDir - LE.tplConfigDir(defaults.configDir, defaults); + backend = Backend.create(defaults, handlers); backend = PromiseA.promisifyAll(backend); - var utils = require('./lib/common'); //var attempts = {}; // should exist in master process only var le; @@ -151,7 +129,7 @@ LE.create = function (defaults, handlers, backend) { return; } - var copy = LE.merge(defaults, args); + var copy = LE.merge(args, defaults); var err; if (!utils.isValidDomain(args.domains[0])) { @@ -185,6 +163,11 @@ LE.create = function (defaults, handlers, backend) { if (defaults.debug || args.debug) { console.log('[LE] fetch'); } + + // TODO figure out what TPLs are needed + var copy = merge(args, defaults); + tplCopy(copy); + return backend.fetchAsync(args).then(function (certInfo) { if (args.debug) { console.log('[LE] raw fetch certs', certInfo && Object.keys(certInfo)); diff --git a/lib/accounts.js b/lib/accounts.js deleted file mode 100644 index 7f8f235..0000000 --- a/lib/accounts.js +++ /dev/null @@ -1,195 +0,0 @@ -'use strict'; - -var PromiseA = require('bluebird'); -var crypto = require('crypto'); -var LeCore = require('letiny-core'); -var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA); -var path = require('path'); -var mkdirpAsync = PromiseA.promisify(require('mkdirp')); -var fs = PromiseA.promisifyAll(require('fs')); - -function createAccount(args, handlers) { - var os = require("os"); - var localname = os.hostname(); - - // arg.rsaBitLength args.rsaExponent - return RSA.generateKeypairAsync(args.rsaKeySize || 2048, 65537, { public: true, pem: true }).then(function (keypair) { - - 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: 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'); - var accountId = keypair.publicKeyMd5; - var accountDir = path.join(args.accountsDir, accountId); - var regr = { body: body }; - - args.accountId = accountId; - args.accountDir = accountDir; - - return mkdirpAsync(accountDir).then(function () { - - var isoDate = new Date().toISOString(); - var accountMeta = { - creation_host: localname - , creation_dt: isoDate - }; - - // TODO abstract file writing - return PromiseA.all([ - // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"} - fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(accountMeta), 'utf8') - // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" } - , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(RSA.exportPrivateJwk(keypair)), 'utf8') - // regr.json: - /* - { body: { contact: [ 'mailto:coolaj86@gmail.com' ], - agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', - key: { e: 'AQAB', kty: 'RSA', n: '...' } }, - uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272', - new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz', - terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' } - */ - , fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify(regr), 'utf8') - ]).then(function () { - var pems = {}; - - // pems.private_key; - pems.meta = accountMeta; - pems.keypair = keypair; - pems.regr = regr; - pems.accountId = accountId; - pems.id = accountId; - return pems; - }); - }); - }); - }); -} - -function getAccount(args, handlers) { - var accountId = args.accountId; - var accountDir = path.join(args.accountsDir, accountId); - var files = {}; - var configs = ['meta.json', 'private_key.json', 'regr.json']; - - return PromiseA.all(configs.map(function (filename) { - var keyname = filename.slice(0, -5); - - return fs.readFileAsync(path.join(accountDir, filename), 'utf8').then(function (text) { - var data; - - try { - data = JSON.parse(text); - } catch(e) { - files[keyname] = { error: e }; - return; - } - - files[keyname] = data; - }, function (err) { - files[keyname] = { error: err }; - }); - })).then(function () { - - if (!Object.keys(files).every(function (key) { - return !files[key].error; - })) { - // TODO log renewal.conf - console.warn("Account '" + accountId + "' was corrupt. No big deal (I think?). Creating a new one..."); - //console.log(accountId, files); - return createAccount(args, handlers); - } - - var keypair = { privateKeyJwk: files.private_key }; - keypair.privateKeyPem = RSA.exportPrivatePem(keypair); - keypair.publicKeyPem = RSA.exportPublicPem(keypair); - - //files.private_key; - //files.regr; - //files.meta; - files.accountId = accountId; // preserve current account id - files.id = accountId; - files.keypair = keypair; - - return files; - }); -} - -function getAccountIdByEmail(args) { - // If we read 10,000 account directories looking for - // just one email address, that could get crazy. - // We should have a folder per email and list - // each account as a file in the folder - // TODO - var email = args.email; - if ('string' !== typeof email) { - if (args.debug) { - console.log("[LE] No email given"); - } - return PromiseA.resolve(null); - } - return fs.readdirAsync(args.accountsDir).then(function (nodes) { - if (args.debug) { - console.log("[LE] arg.accountsDir success"); - } - - return PromiseA.all(nodes.map(function (node) { - return fs.readFileAsync(path.join(args.accountsDir, node, 'regr.json'), 'utf8').then(function (text) { - var regr = JSON.parse(text); - regr.__accountId = node; - - return regr; - }); - })).then(function (regrs) { - var accountId; - - /* - if (args.debug) { - console.log('read many regrs'); - console.log('regrs', regrs); - } - */ - - regrs.some(function (regr) { - return regr.body.contact.some(function (contact) { - var match = contact.toLowerCase() === 'mailto:' + email.toLowerCase(); - if (match) { - accountId = regr.__accountId; - return true; - } - }); - }); - - if (!accountId) { - return null; - } - - return accountId; - }); - }).then(function (accountId) { - return accountId; - }, function (err) { - if ('ENOENT' === err.code) { - // ignore error - return null; - } - - return PromiseA.reject(err); - }); -} - -module.exports.getAccountIdByEmail = getAccountIdByEmail; -module.exports.getAccount = getAccount; -module.exports.createAccount = createAccount; diff --git a/lib/common.js b/lib/common.js index 1351d17..9af37e3 100644 --- a/lib/common.js +++ b/lib/common.js @@ -22,7 +22,7 @@ module.exports.isValidDomain = function (domain) { return ''; }; -module.exports.tplConfigDir = function merge(configDir, defaults) { +module.exports.tplConfigDir = function (configDir, defaults) { var homedir = require('homedir')(); Object.keys(defaults).forEach(function (key) { if ('string' === typeof defaults[key]) { @@ -32,12 +32,17 @@ module.exports.tplConfigDir = function merge(configDir, defaults) { }); }; -module.exports.merge = function merge(defaults, args) { +module.exports.merge = function (/*defaults, args*/) { + var allDefaults = Array.prototype.slice.apply(arguments); + var args = args.shift(); var copy = {}; - Object.keys(defaults).forEach(function (key) { - copy[key] = defaults[key]; + allDefaults.forEach(function (defaults) { + Object.keys(defaults).forEach(function (key) { + copy[key] = defaults[key]; + }); }); + Object.keys(args).forEach(function (key) { copy[key] = args[key]; }); @@ -45,7 +50,19 @@ module.exports.merge = function merge(defaults, args) { return copy; }; -module.exports.tplCopy = function merge(copy) { +module.exports.tplCopy = function (copy) { + var url = require('url'); + var acmeLocation = url.parse(copy.server); + var acmeHostpath = path.join(acmeLocation.hostname, acmeLocation.pathname); + copy.accountsDir = copy.accountsDir || path.join(copy.configDir, 'accounts', acmeHostpath); + // TODO move these defaults elsewhere? + //args.renewalDir = args.renewalDir || ':config/renewal/'; + args.renewalPath = args.renewalPath || ':config/renewal/:hostname.conf'; + // Note: the /directory is part of the server url and, as such, bleeds into the pathname + // So :config/accounts/:server/directory is *incorrect*, but the following *is* correct: + args.accountsDir = args.accountsDir || ':config/accounts/:server'; + hargs.renewalDir = hargs.renewalDir || ':config/renewal/'; + copy.renewalPath = copy.renewalPath || path.join(copy.configDir, 'renewal', copy.domains[0] + '.conf'); var homedir = require('homedir')(); var tpls = { hostname: (copy.domains || [])[0] @@ -71,55 +88,3 @@ module.exports.tplCopy = function merge(copy) { //return copy; }; - -module.exports.fetchFromDisk = function (args) { - // TODO NO HARD-CODED DEFAULTS - if (!args.fullchainPath || !args.privkeyPath || !args.certPath || !args.chainPath) { - console.warn("missing one or more of args.privkeyPath, args.fullchainPath, args.certPath, args.chainPath"); - console.warn("hard-coded conventional pathnames were for debugging and are not a stable part of the API"); - } - - //, fs.readFileAsync(fullchainPath, 'ascii') - // note: if this ^^ gets added back in, the arrays below must change - return PromiseA.all([ - fs.readFileAsync(args.privkeyPath, 'ascii') // 0 - , fs.readFileAsync(args.certPath, 'ascii') // 1 - , fs.readFileAsync(args.chainPath, 'ascii') // 2 - - // stat the file, not the link - , fs.statAsync(args.certPath) // 3 - ]).then(function (arr) { - var cert = arr[1]; - var getCertInfo = require('./cert-info').getCertInfo; - - // XXX Note: Parsing the certificate info comes at a great cost (~500kb) - var certInfo = getCertInfo(cert); - - return { - key: arr[0] // privkey.pem - , privkey: arr[0] // privkey.pem - - , fullchain: arr[1] + '\n' + arr[2] // fullchain.pem - , cert: cert // cert.pem - - , chain: arr[2] // chain.pem - , ca: arr[2] // chain.pem - - , privkeyPath: args.privkeyPath - , fullchainPath: args.fullchainPath - , certPath: args.certPath - , chainPath: args.chainPath - - //, issuedAt: arr[3].mtime.valueOf() - , issuedAt: Date(certInfo.notBefore.value).valueOf() // Date.now() - , expiresAt: Date(certInfo.notAfter.value).valueOf() - , lifetime: args.lifetime - }; - }, function (err) { - if (args.debug) { - console.error("[letsencrypt/lib/common.js] fetchFromDisk"); - console.error(err.stack); - } - return null; - }); -}; diff --git a/lib/core.js b/lib/core.js index 75f7dcd..85f5001 100644 --- a/lib/core.js +++ b/lib/core.js @@ -1,454 +1,228 @@ 'use strict'; -var PromiseA = require('bluebird'); -var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA); -var mkdirpAsync = PromiseA.promisify(require('mkdirp')); -var path = require('path'); -var fs = PromiseA.promisifyAll(require('fs')); -var sfs = require('safe-replace'); var LE = require('../'); -var LeCore = PromiseA.promisifyAll(require('letiny-core')); -var Accounts = require('./accounts'); - -var merge = require('./common').merge; -var tplCopy = require('./common').tplCopy; -var fetchFromConfigLiveDir = require('./common').fetchFromDisk; - var ipc = {}; // in-process cache -function getAcmeUrls(args) { - var now = Date.now(); +module.exports.create = function (defaults, handlers, backend) { + defaults.server = defaults.server || LE.liveServer; + handlers.merge = require('./common').merge; + handlers.tplCopy = require('./common').tplCopy; - // TODO check response header on request for cache time - if ((now - ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { - return PromiseA.resolve(ipc.acmeUrls); - } + var PromiseA = require('bluebird'); + var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA); + var LeCore = PromiseA.promisifyAll(require('letiny-core')); + var crypto = require('crypto'); - return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { - ipc.acmeUrlsUpdatedAt = Date.now(); - ipc.acmeUrls = data; + function createAccount(args, handlers) { + // arg.rsaBitLength args.rsaExponent + return RSA.generateKeypairAsync(args.rsaKeySize || 2048, 65537, { public: true, pem: true }).then(function (keypair) { - return 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 -function readRenewalConfig(args) { - var pyconf = PromiseA.promisifyAll(require('pyconf')); + , 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'); - return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) { - return pyobj; - }, function () { - return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) { - return pyobj; - }); - }); -} + var accountId = keypair.publicKeyMd5; + var regr = { body: body }; + var account = {}; -function writeRenewalConfig(args) { - function log() { - if (args.debug) { - console.log.apply(console, arguments); - } - } + args.accountId = accountId; - var pyobj = args.pyobj; - pyobj.checkpoints = parseInt(pyobj.checkpoints, 10) || 0; + account.keypair = keypair; + account.regr = regr; + account.accountId = accountId; + account.id = accountId; - var pyconf = PromiseA.promisifyAll(require('pyconf')); + args.account = account; - var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]); - - var certPath = args.certPath || pyobj.cert || path.join(liveDir, 'cert.pem'); - var fullchainPath = args.fullchainPath || pyobj.fullchain || path.join(liveDir, 'fullchain.pem'); - var chainPath = args.chainPath || pyobj.chain || path.join(liveDir, 'chain.pem'); - var privkeyPath = args.privkeyPath || pyobj.privkey - //|| args.domainPrivateKeyPath || args.domainKeyPath || pyobj.keyPath - || path.join(liveDir, 'privkey.pem'); - - log('[le/core.js] privkeyPath', privkeyPath); - - var updates = { - account: args.account.id - , configDir: args.configDir - , domains: args.domains - - , email: args.email - , tos: args.agreeTos && true - // yes, it's an array. weird, right? - , webrootPath: args.webrootPath && [args.webrootPath] || [] - , server: args.server || args.acmeDiscoveryUrl - - , privkey: privkeyPath - , fullchain: fullchainPath - , cert: certPath - , chain: chainPath - - , http01Port: args.http01Port - , keyPath: args.domainPrivateKeyPath || args.privkeyPath - , rsaKeySize: args.rsaKeySize - , checkpoints: pyobj.checkpoints - /* // TODO XXX what's the deal with these? they don't make sense - // are they just old junk? or do they have a meaning that I don't know about? - , fullchainPath: path.join(args.configDir, 'chain.pem') - , certPath: path.join(args.configDir, 'cert.pem') - , chainPath: path.join(args.configDir, 'chain.pem') - */ // TODO XXX end - , workDir: args.workDir - , logsDir: args.logsDir - }; - - // final section is completely dynamic - // :hostname = :webroot_path - args.domains.forEach(function (hostname) { - updates[hostname] = args.webrootPath; - }); - - // must write back to the original pyobject or - // annotations will be lost - Object.keys(updates).forEach(function (key) { - pyobj[key] = updates[key]; - }); - - return mkdirpAsync(path.dirname(args.renewalPath)).then(function () { - return pyconf.writeFileAsync(args.renewalPath, pyobj); - }).then(function () { - // NOTE - // writing twice seems to causes a bug, - // so instead we re-read the file from the disk - return pyconf.readFileAsync(args.renewalPath); - }); -} - -function getOrCreateRenewal(args) { - return readRenewalConfig(args).then(function (pyobj) { - var minver = pyobj.checkpoints >= 0; - - args.pyobj = pyobj; - - if (!minver) { - args.checkpoints = 0; - pyobj.checkpoints = 0; - return writeRenewalConfig(args); - } - - // args.account.id = pyobj.account - // args.configDir = args.configDir || pyobj.configDir; - - args.checkpoints = pyobj.checkpoints; - - args.agreeTos = (args.agreeTos || pyobj.tos) && true; - args.email = args.email || pyobj.email; - args.domains = args.domains || pyobj.domains; - - // yes, it's an array. weird, right? - args.webrootPath = args.webrootPath || pyobj.webrootPath[0]; - args.server = args.server || args.acmeDiscoveryUrl || pyobj.server; - - args.certPath = args.certPath || pyobj.cert; - args.privkeyPath = args.privkeyPath || pyobj.privkey; - args.chainPath = args.chainPath || pyobj.chain; - args.fullchainPath = args.fullchainPath || pyobj.fullchain; - - //, workDir: args.workDir - //, logsDir: args.logsDir - args.rsaKeySize = args.rsaKeySize || pyobj.rsaKeySize; - args.http01Port = args.http01Port || pyobj.http01Port; - args.domainKeyPath = args.domainPrivateKeyPath || args.domainKeyPath || args.keyPath || pyobj.keyPath; - - return writeRenewalConfig(args); - }); -} - -function writeCertificateAsync(args, defaults, handlers) { - function log() { - if (args.debug) { - console.log.apply(console, arguments); - } - } - - log("[le/core.js] got certificate!"); - - var obj = args.pyobj; - var result = args.pems; - - result.fullchain = result.cert + '\n' + (result.chain || result.ca); - obj.checkpoints = parseInt(obj.checkpoints, 10) || 0; - - var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]); - - var certPath = args.certPath || obj.cert || path.join(liveDir, 'cert.pem'); - var fullchainPath = args.fullchainPath || obj.fullchain || path.join(liveDir, 'fullchain.pem'); - var chainPath = args.chainPath || obj.chain || path.join(liveDir, 'chain.pem'); - var privkeyPath = args.privkeyPath || obj.privkey - //|| args.domainPrivateKeyPath || args.domainKeyPath || obj.keyPath - || path.join(liveDir, 'privkey.pem'); - - log('[le/core.js] privkeyPath', privkeyPath); - - var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]); - - var checkpoints = obj.checkpoints.toString(); - var certArchive = path.join(archiveDir, 'cert' + checkpoints + '.pem'); - var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoints + '.pem'); - var chainArchive = path.join(archiveDir, 'chain'+ checkpoints + '.pem'); - var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoints + '.pem'); - - return mkdirpAsync(archiveDir).then(function () { - return PromiseA.all([ - sfs.writeFileAsync(certArchive, result.cert, 'ascii') - , sfs.writeFileAsync(chainArchive, (result.chain || result.ca), 'ascii') - , sfs.writeFileAsync(fullchainArchive, result.fullchain, 'ascii') - , sfs.writeFileAsync( - privkeyArchive - // TODO nix args.key, args.domainPrivateKeyPem ?? - , (result.privkey || result.key) || RSA.exportPrivatePem(args.domainKeypair) - , 'ascii' - ) - ]); - }).then(function () { - return mkdirpAsync(liveDir); - }).then(function () { - return PromiseA.all([ - sfs.writeFileAsync(certPath, result.cert, 'ascii') - , sfs.writeFileAsync(chainPath, (result.chain || result.ca), 'ascii') - , sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii') - , sfs.writeFileAsync( - privkeyPath - // TODO nix args.key, args.domainPrivateKeyPem ?? - , (result.privkey || result.key) || RSA.exportPrivatePem(args.domainKeypair) - , 'ascii' - ) - ]); - }).then(function () { - obj.checkpoints += 1; - args.checkpoints += 1; - - return writeRenewalConfig(args); - }).then(function () { - var getCertInfo = require('./cert-info').getCertInfo; - - // XXX Note: Parsing the certificate info comes at a great cost (~500kb) - var certInfo = getCertInfo(result.cert); - - return { - certPath: certPath - , chainPath: chainPath - , fullchainPath: fullchainPath - , privkeyPath: privkeyPath - - // TODO nix keypair - , keypair: args.domainKeypair - - // TODO nix args.key, args.domainPrivateKeyPem ?? - // some ambiguity here... - , privkey: (result.privkey || result.key) || RSA.exportPrivatePem(args.domainKeypair) - , fullchain: result.fullchain || (result.cert + '\n' + result.chain) - , chain: (result.chain || result.ca) - // especially this one... might be cert only, might be fullchain - , cert: result.cert - - , issuedAt: Date(certInfo.notBefore.value).valueOf() // Date.now() - , expiresAt: Date(certInfo.notAfter.value).valueOf() - , lifetime: defaults.lifetime || handlers.lifetime - }; - }); -} - -function getCertificateAsync(args, defaults, handlers) { - function log() { - if (args.debug || defaults.debug) { - console.log.apply(console, arguments); - } - } - - var account = args.account; - var promise; - var keypairOpts = { public: true, pem: true }; - - log('[le/core.js] domainKeyPath:', args.domainKeyPath); - - promise = fs.readFileAsync(args.domainKeyPath, 'ascii').then(function (pem) { - return RSA.import({ privateKeyPem: pem }); - }, function (/*err*/) { - return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { - return mkdirpAsync(path.dirname(args.domainKeyPath)).then(function () { - return fs.writeFileAsync(args.domainKeyPath, keypair.privateKeyPem, 'ascii').then(function () { - return keypair; + return backend.setAccountAsync(args, account).then(function () { + return account; }); }); }); - }); - - 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 - - // - // 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 = merge(defaults, { domains: [domain] }); - tplCopy(copy); - - args.domains = [domain]; - args.webrootPath = args.webrootPath; - if (4 === handlers.setChallenge.length) { - handlers.setChallenge(copy, key, value, done); - } - else if (5 === handlers.setChallenge.length) { - handlers.setChallenge(copy, domain, key, value, done); - } - else { - done(new Error("handlers.setChallenge receives the wrong number of arguments")); - } - } - , removeChallenge: function (domain, key, done) { - var copy = merge(defaults, { domains: [domain] }); - tplCopy(copy); - - if (3 === handlers.removeChallenge.length) { - handlers.removeChallenge(copy, key, done); - } - else if (4 === handlers.removeChallenge.length) { - handlers.removeChallenge(copy, domain, key, done); - } - else { - done(new Error("handlers.removeChallenge receives the wrong number of arguments")); - } - } - }); - }).then(function (results) { - // { cert, chain, fullchain, privkey } - args.pems = results; - return writeCertificateAsync(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 fetchFromConfigLiveDir(args).then(function (certs) { - var halfLife = (certs.expiresAt - certs.issuedAt) / 2; + function getAcmeUrls(args) { + var now = Date.now(); - if (!certs || (Date.now() - certs.issuedAt) > halfLife) { - // There is no cert available - // Or the cert is more than half-expired + // TODO check response header on request for cache time + if ((now - ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { + return PromiseA.resolve(ipc.acmeUrls); + } + + return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { + ipc.acmeUrlsUpdatedAt = Date.now(); + ipc.acmeUrls = data; + + return ipc.acmeUrls; + }); + } + + function getCertificateAsync(args, defaults, handlers) { + function log() { + if (args.debug || defaults.debug) { + console.log.apply(console, arguments); + } + } + + 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 + + // + // 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); + handlers.tplCopy(copy); + + args.domains = [domain]; + //args.domains = args.domains || [domain]; + if (4 === handlers.setChallenge.length) { + console.warn('[WARNING] deprecated use. Define setChallenge as function (opts, domain, key, val, cb) { }'); + handlers.setChallenge(copy, key, value, done); + } + else if (5 === handlers.setChallenge.length) { + handlers.setChallenge(copy, domain, key, value, done); + } + else { + done(new Error("handlers.setChallenge receives the wrong number of arguments")); + } + } + , removeChallenge: function (domain, key, done) { + var copy = handlers.merge({ domains: [domain] }, defaults); + handlers.tplCopy(copy); + + if (3 === handlers.removeChallenge.length) { + handlers.removeChallenge(copy, key, done); + } + else if (4 === handlers.removeChallenge.length) { + handlers.removeChallenge(copy, domain, key, done); + } + else { + done(new Error("handlers.removeChallenge receives the wrong number of arguments")); + } + } + }); + }).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 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." - )); - }); -} + return backend.getRegistration(args).then(function (certs) { + var halfLife = (certs.expiresAt - certs.issuedAt) / 2; -// 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); - } + 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." + )); + }); } - var pyconf = PromiseA.promisifyAll(require('pyconf')); - - return pyconf.readFileAsync(args.renewalPath).then(function (renewal) { - var accountId = renewal.account; - renewal = renewal.account; - - return accountId; - }, function (err) { - if ("ENOENT" === err.code) { - log("[le/core.js] try email"); - return Accounts.getAccountIdByEmail(args, handlers); + // 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 PromiseA.reject(err); - }).then(function (accountId) { + 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; + // 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'); + if (accountId) { + log('[le/core.js] use account'); - args.accountId = accountId; - return Accounts.getAccount(args, handlers); - } else { - log('[le/core.js] create account'); - return Accounts.createAccount(args, handlers); + args.accountId = accountId; + return Accounts.getAccount(args, handlers); + } else { + log('[le/core.js] create account'); + return Accounts.createAccount(args, handlers); + } + }); + }).then(function (account) { + /* + if (renewal.account !== account) { + // the account has become corrupt, re-register + return; } + */ + log('[le/core.js] created account'); + return account; }); - }).then(function (account) { - /* - if (renewal.account !== account) { - // the account has become corrupt, re-register - return; - } - */ - log('[le/core.js] created account'); - return account; - }); -/* - return fs.readdirAsync(accountsDir, function (nodes) { - return PromiseA.all(nodes.map(function (node) { - var reMd5 = /[a-f0-9]{32}/i; - if (reMd5.test(node)) { - } - })); - }); -*/ -} - -module.exports.create = function (defaults, handlers) { - defaults.server = defaults.server || LE.liveServer; + } var wrapped = { - registerAsync: function (args) { - var copy; - // TODO move these defaults elsewhere? - //args.renewalDir = args.renewalDir || ':config/renewal/'; - args.renewalPath = args.renewalPath || ':config/renewal/:hostname.conf'; - // Note: the /directory is part of the server url and, as such, bleeds into the pathname - // So :config/accounts/:server/directory is *incorrect*, but the following *is* correct: - args.accountsDir = args.accountsDir || ':config/accounts/:server'; - copy = merge(args, defaults); - tplCopy(copy); - - var url = require('url'); - var acmeLocation = url.parse(copy.server); - var acmeHostpath = path.join(acmeLocation.hostname, acmeLocation.pathname); - copy.renewalPath = copy.renewalPath || path.join(copy.configDir, 'renewal', copy.domains[0] + '.conf'); - copy.accountsDir = copy.accountsDir || path.join(copy.configDir, 'accounts', acmeHostpath); + registerAsync: function (args) { + var copy = handlers.merge(args, defaults); + handlers.tplCopy(copy); return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { copy.account = account; - return getOrCreateRenewal(copy).then(function (pyobj) { + return backend.getOrCreateRenewal(copy).then(function (pyobj) { copy.pyobj = pyobj; return getOrCreateDomainCertificate(copy, defaults, handlers); @@ -459,55 +233,19 @@ module.exports.create = function (defaults, handlers) { return PromiseA.reject(err); }); } - , fetchAsync: function (args) { - var copy = merge(args, defaults); - tplCopy(copy); - - return fetchFromConfigLiveDir(copy, defaults); + , getOrCreateAccount: function (args) { + // TODO + keypair.privateKeyPem = RSA.exportPrivatePem(keypair); + keypair.publicKeyPem = RSA.exportPublicPem(keypair); + return createAccount(args, handlers); } , configureAsync: function (hargs) { - hargs.renewalPath = hargs.renewalPath || ':config/renewal/:hostname.conf'; var copy = merge(hargs, defaults); tplCopy(copy); return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { copy.account = account; - return getOrCreateRenewal(copy); - }); - } - , getConfigAsync: function (hargs) { - hargs.renewalPath = hargs.renewalPath || ':config/renewal/:hostname.conf'; - hargs.domains = []; - - var copy = merge(hargs, defaults); - tplCopy(copy); - - return readRenewalConfig(copy).then(function (pyobj) { - var exists = pyobj.checkpoints >= 0; - if (!exists) { - return null; - } - - return pyobj; - }); - } - , getConfigsAsync: function (hargs) { - hargs.renewalDir = hargs.renewalDir || ':config/renewal/'; - hargs.renewalPath = hargs.renewalPath || ':config/renewal/:hostname.conf'; - hargs.domains = []; - - var copy = merge(hargs, defaults); - tplCopy(copy); - - return fs.readdirAsync(copy.renewalDir).then(function (nodes) { - nodes = nodes.filter(function (node) { - return /^[a-z0-9]+.*\.conf$/.test(node); - }); - - return PromiseA.all(nodes.map(function (node) { - copy.domains = [node.replace(/\.conf$/, '')]; - return wrapped.getConfigAsync(copy); - })); + return backend.getOrCreateRenewal(copy); }); } }; diff --git a/lib/default-handlers.js b/lib/default-handlers.js index 17852a2..9982539 100644 --- a/lib/default-handlers.js +++ b/lib/default-handlers.js @@ -3,11 +3,11 @@ var fs = require('fs'); var path = require('path'); -module.exports.agreeToTerms = function (args, agree) { - agree(null, args.agreeTos); +module.exports.agreeToTerms = function (args, agreeCb) { + agreeCb(null, args.agreeTos); }; -module.exports.setChallenge = function (args, challengePath, keyAuthorization, done) { +module.exports.setChallenge = function (args, domain, challengePath, keyAuthorization, done) { //var hostname = args.domains[0]; var mkdirp = require('mkdirp'); @@ -26,14 +26,14 @@ module.exports.setChallenge = function (args, challengePath, keyAuthorization, d }); }; -module.exports.getChallenge = function (args, key, done) { +module.exports.getChallenge = function (args, domain, key, done) { //var hostname = args.domains[0]; //console.log("getting the challenge", args, key); fs.readFile(path.join(args.webrootPath, key), 'utf8', done); }; -module.exports.removeChallenge = function (args, key, done) { +module.exports.removeChallenge = function (args, domain, key, done) { //var hostname = args.domains[0]; fs.unlink(path.join(args.webrootPath, key), done); diff --git a/lib/pycompat.js b/lib/pycompat.js new file mode 100644 index 0000000..a19bfd6 --- /dev/null +++ b/lib/pycompat.js @@ -0,0 +1,517 @@ +'use strict'; + +var PromiseA = require('bluebird'); +var mkdirpAsync = PromiseA.promisify(require('mkdirp')); +var path = require('path'); +var fs = PromiseA.promisifyAll(require('fs')); +var sfs = require('safe-replace'); + +var fetchFromConfigLiveDir = function (args) { + // TODO NO HARD-CODED DEFAULTS + if (!args.fullchainPath || !args.privkeyPath || !args.certPath || !args.chainPath) { + console.warn("missing one or more of args.privkeyPath, args.fullchainPath, args.certPath, args.chainPath"); + console.warn("hard-coded conventional pathnames were for debugging and are not a stable part of the API"); + } + + //, fs.readFileAsync(fullchainPath, 'ascii') + // note: if this ^^ gets added back in, the arrays below must change + return PromiseA.all([ + fs.readFileAsync(args.privkeyPath, 'ascii') // 0 + , fs.readFileAsync(args.certPath, 'ascii') // 1 + , fs.readFileAsync(args.chainPath, 'ascii') // 2 + + // stat the file, not the link + , fs.statAsync(args.certPath) // 3 + ]).then(function (arr) { + var cert = arr[1]; + var getCertInfo = require('./cert-info').getCertInfo; + + // XXX Note: Parsing the certificate info comes at a great cost (~500kb) + var certInfo = getCertInfo(cert); + + return { + key: arr[0] // privkey.pem + , privkey: arr[0] // privkey.pem + + , fullchain: arr[1] + '\n' + arr[2] // fullchain.pem + , cert: cert // cert.pem + + , chain: arr[2] // chain.pem + , ca: arr[2] // chain.pem + + , privkeyPath: args.privkeyPath + , fullchainPath: args.fullchainPath + , certPath: args.certPath + , chainPath: args.chainPath + + //, issuedAt: arr[3].mtime.valueOf() + , issuedAt: Date(certInfo.notBefore.value).valueOf() // Date.now() + , expiresAt: Date(certInfo.notAfter.value).valueOf() + , lifetime: args.lifetime + }; + }, function (err) { + if (args.debug) { + console.error("[letsencrypt/lib/common.js] fetchFromDisk"); + console.error(err.stack); + } + return null; + }); +}; + +function getAccount(args) { + var accountId = args.accountId; + var accountDir = path.join(args.accountsDir, accountId); + var files = {}; + var configs = [ 'meta.json', 'private_key.json', 'regr.json' ]; + + return PromiseA.all(configs.map(function (filename) { + var keyname = filename.slice(0, -5); + + return fs.readFileAsync(path.join(accountDir, filename), 'utf8').then(function (text) { + var data; + + try { + data = JSON.parse(text); + } catch(e) { + files[keyname] = { error: e }; + return; + } + + files[keyname] = data; + }, function (err) { + files[keyname] = { error: err }; + }); + })).then(function () { + var err; + + if (!Object.keys(files).every(function (key) { + return !files[key].error; + }) || !files.private_key || !files.private_key.n) { + err = new Error("Account '" + accountId + "' was corrupt. No big deal (I think?). Creating a new one..."); + err.code = 'E_ACCOUNT_CORRUPT'; + err.data = files; + return PromiseA.reject(err); + } + + //files.private_key; + //files.regr; + //files.meta; + files.accountId = accountId; // preserve current account id + files.id = accountId; + files.keypair = { privateKeyJwk: files.private_key }; + + return files; + }); +} + +function getAccountIdByEmail(args) { + // If we read 10,000 account directories looking for + // just one email address, that could get crazy. + // We should have a folder per email and list + // each account as a file in the folder + // TODO + var email = args.email; + if ('string' !== typeof email) { + if (args.debug) { + console.log("[LE] No email given"); + } + return PromiseA.resolve(null); + } + return fs.readdirAsync(args.accountsDir).then(function (nodes) { + if (args.debug) { + console.log("[LE] arg.accountsDir success"); + } + + return PromiseA.all(nodes.map(function (node) { + return fs.readFileAsync(path.join(args.accountsDir, node, 'regr.json'), 'utf8').then(function (text) { + var regr = JSON.parse(text); + regr.__accountId = node; + + return regr; + }); + })).then(function (regrs) { + var accountId; + + /* + if (args.debug) { + console.log('read many regrs'); + console.log('regrs', regrs); + } + */ + + regrs.some(function (regr) { + return regr.body.contact.some(function (contact) { + var match = contact.toLowerCase() === 'mailto:' + email.toLowerCase(); + if (match) { + accountId = regr.__accountId; + return true; + } + }); + }); + + if (!accountId) { + return null; + } + + return accountId; + }); + }).then(function (accountId) { + return accountId; + }, function (err) { + if ('ENOENT' === err.code) { + // ignore error + return null; + } + + return PromiseA.reject(err); + }); +} + +function readRenewalConfig(args) { + var pyconf = PromiseA.promisifyAll(require('pyconf')); + + return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) { + return pyobj; + }, function () { + return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) { + return pyobj; + }); + }); +} + +function writeRenewalConfig(args) { + function log() { + if (args.debug) { + console.log.apply(console, arguments); + } + } + + var pyobj = args.pyobj; + pyobj.checkpoints = parseInt(pyobj.checkpoints, 10) || 0; + + var pyconf = PromiseA.promisifyAll(require('pyconf')); + + var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]); + + var certPath = args.certPath || pyobj.cert || path.join(liveDir, 'cert.pem'); + var fullchainPath = args.fullchainPath || pyobj.fullchain || path.join(liveDir, 'fullchain.pem'); + var chainPath = args.chainPath || pyobj.chain || path.join(liveDir, 'chain.pem'); + var privkeyPath = args.privkeyPath || pyobj.privkey + //|| args.domainPrivateKeyPath || args.domainKeyPath || pyobj.keyPath + || path.join(liveDir, 'privkey.pem'); + + log('[le/core.js] privkeyPath', privkeyPath); + + var updates = { + account: args.account.id + , configDir: args.configDir + , domains: args.domains + + , email: args.email + , tos: args.agreeTos && true + // yes, it's an array. weird, right? + , webrootPath: args.webrootPath && [args.webrootPath] || [] + , server: args.server || args.acmeDiscoveryUrl + + , privkey: privkeyPath + , fullchain: fullchainPath + , cert: certPath + , chain: chainPath + + , http01Port: args.http01Port + , keyPath: args.domainPrivateKeyPath || args.privkeyPath + , rsaKeySize: args.rsaKeySize + , checkpoints: pyobj.checkpoints + /* // TODO XXX what's the deal with these? they don't make sense + // are they just old junk? or do they have a meaning that I don't know about? + , fullchainPath: path.join(args.configDir, 'chain.pem') + , certPath: path.join(args.configDir, 'cert.pem') + , chainPath: path.join(args.configDir, 'chain.pem') + */ // TODO XXX end + , workDir: args.workDir + , logsDir: args.logsDir + }; + + // final section is completely dynamic + // :hostname = :webroot_path + args.domains.forEach(function (hostname) { + updates[hostname] = args.webrootPath; + }); + + // must write back to the original pyobject or + // annotations will be lost + Object.keys(updates).forEach(function (key) { + pyobj[key] = updates[key]; + }); + + return mkdirpAsync(path.dirname(args.renewalPath)).then(function () { + return pyconf.writeFileAsync(args.renewalPath, pyobj); + }).then(function () { + // NOTE + // writing twice seems to causes a bug, + // so instead we re-read the file from the disk + return pyconf.readFileAsync(args.renewalPath); + }); +} + +function getOrCreateRenewal(args) { + return readRenewalConfig(args).then(function (pyobj) { + var minver = pyobj.checkpoints >= 0; + + args.pyobj = pyobj; + + if (!minver) { + args.checkpoints = 0; + pyobj.checkpoints = 0; + return writeRenewalConfig(args); + } + + // args.account.id = pyobj.account + // args.configDir = args.configDir || pyobj.configDir; + + args.checkpoints = pyobj.checkpoints; + + args.agreeTos = (args.agreeTos || pyobj.tos) && true; + args.email = args.email || pyobj.email; + args.domains = args.domains || pyobj.domains; + + // yes, it's an array. weird, right? + args.webrootPath = args.webrootPath || pyobj.webrootPath[0]; + args.server = args.server || args.acmeDiscoveryUrl || pyobj.server; + + args.certPath = args.certPath || pyobj.cert; + args.privkeyPath = args.privkeyPath || pyobj.privkey; + args.chainPath = args.chainPath || pyobj.chain; + args.fullchainPath = args.fullchainPath || pyobj.fullchain; + + //, workDir: args.workDir + //, logsDir: args.logsDir + args.rsaKeySize = args.rsaKeySize || pyobj.rsaKeySize; + args.http01Port = args.http01Port || pyobj.http01Port; + args.domainKeyPath = args.domainPrivateKeyPath || args.domainKeyPath || args.keyPath || pyobj.keyPath; + + return writeRenewalConfig(args); + }); +} + +function writeCertificateAsync(args) { + function log() { + if (args.debug) { + console.log.apply(console, arguments); + } + } + + log("[le/core.js] got certificate!"); + + var obj = args.pyobj; + var pems = args.pems; + + pems.fullchain = pems.cert + '\n' + (pems.chain || pems.ca); + obj.checkpoints = parseInt(obj.checkpoints, 10) || 0; + + var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]); + + var certPath = args.certPath || obj.cert || path.join(liveDir, 'cert.pem'); + var fullchainPath = args.fullchainPath || obj.fullchain || path.join(liveDir, 'fullchain.pem'); + var chainPath = args.chainPath || obj.chain || path.join(liveDir, 'chain.pem'); + var privkeyPath = args.privkeyPath || obj.privkey + //|| args.domainPrivateKeyPath || args.domainKeyPath || obj.keyPath + || path.join(liveDir, 'privkey.pem'); + + log('[le/core.js] privkeyPath', privkeyPath); + + var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]); + + var checkpoints = obj.checkpoints.toString(); + var certArchive = path.join(archiveDir, 'cert' + checkpoints + '.pem'); + var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoints + '.pem'); + var chainArchive = path.join(archiveDir, 'chain'+ checkpoints + '.pem'); + var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoints + '.pem'); + + return mkdirpAsync(archiveDir).then(function () { + return PromiseA.all([ + sfs.writeFileAsync(certArchive, pems.cert, 'ascii') + , sfs.writeFileAsync(chainArchive, (pems.chain || pems.ca), 'ascii') + , sfs.writeFileAsync(fullchainArchive, pems.fullchain, 'ascii') + , sfs.writeFileAsync( + privkeyArchive + // TODO nix args.key, args.domainPrivateKeyPem ?? + , (pems.privkey || pems.key) // || RSA.exportPrivatePem(args.domainKeypair) + , 'ascii' + ) + ]); + }).then(function () { + return mkdirpAsync(liveDir); + }).then(function () { + return PromiseA.all([ + sfs.writeFileAsync(certPath, pems.cert, 'ascii') + , sfs.writeFileAsync(chainPath, (pems.chain || pems.ca), 'ascii') + , sfs.writeFileAsync(fullchainPath, pems.fullchain, 'ascii') + , sfs.writeFileAsync( + privkeyPath + // TODO nix args.key, args.domainPrivateKeyPem ?? + , (pems.privkey || pems.key) // || RSA.exportPrivatePem(args.domainKeypair) + , 'ascii' + ) + ]); + }).then(function () { + obj.checkpoints += 1; + args.checkpoints += 1; + + return writeRenewalConfig(args); + }).then(function () { + var getCertInfo = require('./cert-info').getCertInfo; + + // XXX Note: Parsing the certificate info comes at a great cost (~500kb) + var certInfo = getCertInfo(pems.cert); + + return { + certPath: certPath + , chainPath: chainPath + , fullchainPath: fullchainPath + , privkeyPath: privkeyPath + + // TODO nix keypair + , keypair: args.domainKeypair + + // TODO nix args.key, args.domainPrivateKeyPem ?? + // some ambiguity here... + , privkey: (pems.privkey || pems.key) //|| RSA.exportPrivatePem(args.domainKeypair) + , fullchain: pems.fullchain || (pems.cert + '\n' + pems.chain) + , chain: (pems.chain || pems.ca) + // especially this one... might be cert only, might be fullchain + , cert: pems.cert + + , issuedAt: Date(certInfo.notBefore.value).valueOf() // Date.now() + , expiresAt: Date(certInfo.notAfter.value).valueOf() + }; + }); +} + +module.exports.create = function (/*defaults*/) { + function getConfigAsync(copy) { + copy.domains = []; + + return readRenewalConfig(copy).then(function (pyobj) { + var exists = pyobj.checkpoints >= 0; + if (!exists) { + return null; + } + + return pyobj; + }); + } + + return { + getDefaults: function () { +LE.tplConfigDir = require('./lib/common').tplConfigDir; + // replaces strings of workDir, certPath, etc + // if they have :config/etc/live or :conf/etc/archive + // to instead have the path of the configDir + LE.tplConfigDir(defaults.configDir, defaults); + return { + configDir: require('homedir')() + '/letsencrypt/etc' // /etc/letsencrypt/ + , logsDir: ':config/log' // /var/log/letsencrypt/ + , workDir: leCore.workDir // /var/lib/letsencrypt/ + , accountsDir: ':config/accounts/:server' + , renewalPath: ':config/renewal/:hostname.conf' + , renewalDir: ':config/renewal/' + , privkeyPath: ':config/live/:hostname/privkey.pem' + , fullchainPath: ':config/live/:hostname/fullchain.pem' + , certPath: ':config/live/:hostname/cert.pem' + , chainPath: ':config/live/:hostname/chain.pem' + , renewalPath: ':config/renewal/:hostname.conf' + , accountsDir: ':config/accounts/:server' + }; + } + , getPrivatePemAsync: function (args) { + return fs.readFileAsync(args.domainKeyPath, 'ascii'); + } + , setPrivatePemAsync: function (args, keypair) { + return mkdirpAsync(path.dirname(args.domainKeyPath)).then(function () { + return fs.writeFileAsync(args.domainKeyPath, keypair.privateKeyPem, 'ascii').then(function () { + return keypair; + }); + }); + } + , setRegistrationAsync: function (args) { + return writeCertificateAsync(args); + } + + , getRegistrationAsync: function (args) { + return fetchFromConfigLiveDir(args); + } + , getOrCreateRenewalAsync: function (args) { + return getOrCreateRenewal(args); + } + , getConfigAsync: getConfigAsync + , getConfigsAsync: function (copy) { + copy.domains = []; + + return fs.readdirAsync(copy.renewalDir).then(function (nodes) { + nodes = nodes.filter(function (node) { + return /^[a-z0-9]+.*\.conf$/.test(node); + }); + + return PromiseA.all(nodes.map(function (node) { + copy.domains = [node.replace(/\.conf$/, '')]; + return getConfigAsync(copy); + })); + }); + } + , fetchAsync: function (args) { + return fetchFromConfigLiveDir(args); + } + , getAccountIdByEmailAsync: getAccountIdByEmail + , getAccountAsync: getAccount + , setAccountAsync: function (args, account) { + var isoDate = new Date().toISOString(); + var os = require("os"); + var localname = os.hostname(); + var accountDir = path.join(args.accountsDir, account.accountId); + + account.meta = account.meta || { + creation_host: localname + , creation_dt: isoDate + }; + + return mkdirpAsync(accountDir).then(function () { + var RSA = require('rsa-compat').RSA; + + // TODO abstract file writing + return PromiseA.all([ + // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"} + fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(account.meta), 'utf8') + // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" } + , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(RSA.exportPrivateJwk(account.keypair)), 'utf8') + // regr.json: + /* + { body: { contact: [ 'mailto:coolaj86@gmail.com' ], + agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', + key: { e: 'AQAB', kty: 'RSA', n: '...' } }, + uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272', + new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz', + terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' } + */ + , fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify(account.regr), 'utf8') + ]); + }); + } + , getAccountIdAsync: function (args) { + var pyconf = PromiseA.promisifyAll(require('pyconf')); + + return pyconf.readFileAsync(args.renewalPath).then(function (renewal) { + var accountId = renewal.account; + renewal = renewal.account; + + return accountId; + }, function (err) { + if ("ENOENT" === err.code) { + return getAccountIdByEmail(args); + } + + return PromiseA.reject(err); + }); + } + }; +}; diff --git a/package.json b/package.json index 70f9046..134140d 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,6 @@ }, "homepage": "https://github.com/Daplie/node-letsencrypt#readme", "devDependencies": { - "express": "^4.13.3", - "localhost.daplie.com-certificates": "^1.1.2" }, "optionalDependencies": {}, "dependencies": {