moving some parts to letsencrypt-express
This commit is contained in:
parent
28ec4cc83a
commit
22e1d768af
108
index.js
108
index.js
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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()
|
||||
key: arr[0] // privkey.pem
|
||||
, 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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue