moving some parts to letsencrypt-express

This commit is contained in:
AJ ONeal 2015-12-17 05:44:41 +00:00
parent 28ec4cc83a
commit 22e1d768af
4 changed files with 168 additions and 212 deletions

108
index.js
View File

@ -34,39 +34,6 @@ LE.merge = function merge(defaults, args) {
return copy;
};
LE.cacheCertInfo = function (args, certInfo, ipc, handlers) {
// TODO IPC via process and worker to guarantee no races
// rather than just "really good odds"
var hostname = args.domains[0];
var now = Date.now();
// Stagger randomly by plus 0% to 25% to prevent all caches expiring at once
var rnd1 = (crypto.randomBytes(1)[0] / 255);
var memorizeFor = Math.floor(handlers.memorizeFor + ((handlers.memorizeFor / 4) * rnd1));
// Stagger randomly to renew between n and 2n days before renewal is due
// this *greatly* reduces the risk of multiple cluster processes renewing the same domain at once
var rnd2 = (crypto.randomBytes(1)[0] / 255);
var bestIfUsedBy = certInfo.expiresAt - (handlers.renewWithin + Math.floor(handlers.renewWithin * rnd2));
// Stagger randomly by plus 0 to 5 min to reduce risk of multiple cluster processes
// renewing at once on boot when the certs have expired
var rnd3 = (crypto.randomBytes(1)[0] / 255);
var renewTimeout = Math.floor((5 * 60 * 1000) * rnd3);
certInfo.context = tls.createSecureContext({
key: certInfo.key
, cert: certInfo.cert
//, ciphers // node's defaults are great
});
certInfo.loadedAt = now;
certInfo.memorizeFor = memorizeFor;
certInfo.bestIfUsedBy = bestIfUsedBy;
certInfo.renewTimeout = renewTimeout;
ipc[hostname] = certInfo;
return ipc[hostname];
};
// backend, defaults, handlers
LE.create = function (defaults, handlers, backend) {
var d, b, h;
@ -267,21 +234,22 @@ LE.create = function (defaults, handlers, backend) {
}
//console.log("[NLE]: begin registration");
return backend.registerAsync(copy).then(function () {
return backend.registerAsync(copy).then(function (pems) {
//console.log("[NLE]: end registration");
// calls fetch because fetch calls cacheCertInfo
return le.fetch(args, cb);
cb(null, pems);
//return le.fetch(args, cb);
}, cb);
});
}
, _fetchHelper: function (args, cb) {
return backend.fetchAsync(args).then(function (certInfo) {
if (!certInfo) {
cb(null, null);
return;
if (args.debug) {
console.log('[LE] debug is on');
}
var now = Date.now();
if (true || args.debug) {
console.log('[LE] raw fetch certs', certInfo);
}
if (!certInfo) { cb(null, null); return; }
// key, cert, issuedAt, lifetime, expiresAt
if (!certInfo.expiresAt) {
@ -290,53 +258,19 @@ LE.create = function (defaults, handlers, backend) {
if (!certInfo.lifetime) {
certInfo.lifetime = (certInfo.lifetime || handlers.lifetime);
}
// a pretty good hard buffer
certInfo.expiresAt -= (1 * 24 * 60 * 60 * 100);
certInfo = LE.cacheCertInfo(args, certInfo, ipc, handlers);
if (now > certInfo.bestIfUsedBy && !certInfo.timeout) {
// EXPIRING
if (now > certInfo.expiresAt) {
// EXPIRED
certInfo.renewTimeout = Math.floor(certInfo.renewTimeout / 2);
}
certInfo.timeout = setTimeout(function () {
le.register(args, cb);
}, certInfo.renewTimeout);
}
cb(null, certInfo.context);
cb(null, certInfo);
}, cb);
}
, fetch: function (args, cb) {
var hostname = args.domains[0];
// TODO don't call now() every time because this is hot code
var now = Date.now();
var certInfo = ipc[hostname];
// TODO once ECDSA is available, wait for cert renewal if its due
if (certInfo) {
if (now > certInfo.bestIfUsedBy && !certInfo.timeout) {
// EXPIRING
if (now > certInfo.expiresAt) {
// EXPIRED
certInfo.renewTimeout = Math.floor(certInfo.renewTimeout / 2);
}
certInfo.timeout = setTimeout(function () {
le.register(args, cb);
}, certInfo.renewTimeout);
}
cb(null, certInfo.context);
if ((now - certInfo.loadedAt) < (certInfo.memorizeFor)) {
// these aren't stale, so don't fall through
return;
}
}
le._fetchHelper(args, cb);
}
, renew: function (args, cb) {
args.duplicate = false;
le.register(args, cb);
}
, register: function (args, cb) {
if (!Array.isArray(args.domains)) {
cb(new Error('args.domains should be an array of domains'));
@ -349,16 +283,10 @@ LE.create = function (defaults, handlers, backend) {
le._fetchHelper(args, function (err, hit) {
var hostname = args.domains[0];
if (err) {
cb(err);
return;
}
else if (hit) {
cb(null, hit);
return;
}
if (err) { cb(err); return; }
else if (hit) { cb(null, hit); return; }
return le._registerHelper(args, function (err) {
return le._registerHelper(args, function (err, pems) {
if (err) {
cb(err);
return;
@ -366,7 +294,7 @@ LE.create = function (defaults, handlers, backend) {
le._fetchHelper(args, function (err, cache) {
if (cache) {
cb(null, cache.context);
cb(null, cache);
return;
}

118
lib/account.js Normal file
View File

@ -0,0 +1,118 @@
'use strict';
var PromiseA = require('bluebird');
var LeCore = require('letiny-core');
var leCrypto = LeCore.leCrypto;
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();
// TODO support ECDSA
// arg.rsaBitLength args.rsaExponent
return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) {
/* pems = { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } */
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);
}
, accountPrivateKeyPem: pems.privateKeyPem
, debug: args.debug || handlers.debug
}).then(function (body) {
var accountDir = path.join(args.accountsDir, pems.publicKeyMd5);
return mkdirpAsync(accountDir).then(function () {
var isoDate = new Date().toISOString();
var accountMeta = {
creation_host: localname
, creation_dt: isoDate
};
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(pems.privateKeyJwk), '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({ body: body }), 'utf8')
]).then(function () {
return pems;
});
});
});
});
}
function getAccount(accountId, args, handlers) {
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 currupt. No big deal (I think?). Creating a new one...");
return createAccount(args, handlers);
}
return leCrypto.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) {
files.accountId = accountId; // md5sum(publicKeyPem)
files.publicKeyMd5 = accountId; // md5sum(publicKeyPem)
files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN...
files.privateKeyPem = keypair.privateKeyPem; // ascii PEM: ----BEGIN...
files.privateKeyJson = keypair.private_key; // json { n: ..., e: ..., iq: ..., etc }
return files;
});
});
}
function getAccountByEmail(/*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
return PromiseA.resolve(null);
}
module.exports.getAccountByEmail = getAccountByEmail;
module.exports.getAccount = getAccount;

View File

@ -5,24 +5,38 @@ var PromiseA = require('bluebird');
module.exports.fetchFromDisk = function (args, defaults) {
var hostname = args.domains[0];
var crtpath = (args.fullchainPath || defaults.fullchainPath)
var certPath = (args.fullchainPath || defaults.fullchainPath)
|| (defaults.configDir
+ (args.fullchainTpl || defaults.fullchainTpl || ':hostname/fullchain.pem').replace(/:hostname/, hostname));
var privpath = (args.privkeyPath || defaults.privkeyPath)
var privkeyPath = (args.privkeyPath || defaults.privkeyPath)
|| (defaults.configDir
+ (args.privkeyTpl || defaults.privkeyTpl || ':hostname/privkey.pem').replace(/:hostname/, hostname));
var chainPath = (args.chainPath || defaults.chainPath)
|| (defaults.configDir
+ (args.chainTpl || defaults.chainTpl || ':hostname/chain.pem').replace(/:hostname/, hostname));
/*
var fullchainPath = (args.fullchainPath || defaults.fullchainPath)
|| (defaults.configDir
+ (args.fullchainTpl || defaults.fullchainTpl || ':hostname/fullchain.pem').replace(/:hostname/, hostname));
*/
return PromiseA.all([
fs.readFileAsync(privpath, 'ascii')
, fs.readFileAsync(crtpath, 'ascii')
fs.readFileAsync(privkeyPath, 'ascii')
, fs.readFileAsync(certPath, 'ascii')
, fs.readFileAsync(chainPath, 'ascii')
//, fs.readFileAsync(fullchainPath, 'ascii')
// stat the file, not the link
, fs.statAsync(crtpath)
, fs.statAsync(certPath)
]).then(function (arr) {
// TODO parse certificate to determine lifetime and expiresAt
return {
key: arr[0] // privkey.pem
, cert: arr[1] // fullchain.pem
// TODO parse centificate for lifetime / expiresAt
, issuedAt: arr[2].mtime.valueOf()
, cert: arr[1] // cert.pem
, chain: arr[2] // chain.pem
, fullchain: arr[1] + '\n' + arr[2] // fullchain.pem
, issuedAt: arr[4].mtime.valueOf() // ???
};
}, function () {
return null;

View File

@ -3,12 +3,11 @@
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 LE = require('../');
var LeCore = PromiseA.promisifyAll(require('letiny-core'));
var leCrypto = PromiseA.promisifyAll(LeCore.leCrypto);
var Accounts = require('./accounts');
var fetchFromConfigLiveDir = require('./common').fetchFromDisk;
@ -30,115 +29,10 @@ function getAcmeUrls(args) {
});
}
function createAccount(args, handlers) {
var os = require("os");
var localname = os.hostname();
// TODO support ECDSA
// arg.rsaBitLength args.rsaExponent
return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) {
/* pems = { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } */
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);
}
, accountPrivateKeyPem: pems.privateKeyPem
, debug: args.debug || handlers.debug
}).then(function (body) {
var accountDir = path.join(args.accountsDir, pems.publicKeyMd5);
return mkdirpAsync(accountDir).then(function () {
var isoDate = new Date().toISOString();
var accountMeta = {
creation_host: localname
, creation_dt: isoDate
};
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(pems.privateKeyJwk), '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({ body: body }), 'utf8')
]).then(function () {
return pems;
});
});
});
});
}
function getAccount(accountId, args, handlers) {
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 currupt. No big deal (I think?). Creating a new one...");
return createAccount(args, handlers);
}
return leCrypto.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) {
files.accountId = accountId; // md5sum(publicKeyPem)
files.publicKeyMd5 = accountId; // md5sum(publicKeyPem)
files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN...
files.privateKeyPem = keypair.privateKeyPem; // ascii PEM: ----BEGIN...
files.privateKeyJson = keypair.private_key; // json { n: ..., e: ..., iq: ..., etc }
return files;
});
});
}
function getAccountByEmail(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
return PromiseA.resolve(null);
}
function getCertificateAsync(account, args, defaults, handlers) {
var pyconf = PromiseA.promisifyAll(require('pyconf'));
//var pyconf = PromiseA.promisifyAll(require('pyconf'));
return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (domain) {
return LeCore.getCertificateAsync({
@ -156,7 +50,8 @@ function getCertificateAsync(account, args, defaults, handlers) {
handlers.setChallenge(args, key, value, done);
}
else if (5 === handlers.setChallenge.length) {
handlers.setChallenge(args, domain, key, value, done);
// TODO merge templates with domain
handlers.setChallenge(defaults, domain, key, value, done);
}
else {
done(new Error("handlers.setChallenge receives the wrong number of arguments"));
@ -169,7 +64,8 @@ function getCertificateAsync(account, args, defaults, handlers) {
handlers.removeChallenge(args, key, done);
}
else if (4 === handlers.removeChallenge.length) {
handlers.removeChallenge(args, domain, key, done);
// TODO merge templates with domain
handlers.removeChallenge(defaults, domain, key, done);
}
else {
done(new Error("handlers.removeChallenge receives the wrong number of arguments"));
@ -225,7 +121,7 @@ function registerWithAcme(args, defaults, handlers) {
return accountId;
}, function (err) {
if ("ENOENT" === err.code) {
return getAccountByEmail(args, handlers);
return Accounts.getAccountByEmail(args, handlers);
}
return PromiseA.reject(err);
@ -235,9 +131,9 @@ function registerWithAcme(args, defaults, handlers) {
args._acmeUrls = urls;
if (accountId) {
return getAccount(accountId, args, handlers);
return Accounts.getAccount(accountId, args, handlers);
} else {
return createAccount(args, handlers);
return Accounts.createAccount(args, handlers);
}
});
}).then(function (account) {