manage renewals perfectly :-)
This commit is contained in:
parent
a6b1b5cfa6
commit
563f3ae3eb
7
index.js
7
index.js
|
@ -137,8 +137,11 @@ LE.create = function (defaults, handlers, backend) {
|
|||
// If you do not check these things, then someone could attack you
|
||||
// and cause you, in return, to have your ip be rate-limit blocked
|
||||
//
|
||||
console.warn("\n[TODO]: node-letsencrypt: `validate(hostnames, cb)` needs to be implemented");
|
||||
console.warn("(it'll work fine without it, but for security - and convenience - it should be implemented\n");
|
||||
//console.warn("\n[TODO]: node-letsencrypt: `validate(hostnames, cb)` needs to be implemented");
|
||||
//console.warn("(it'll work fine without it, but for security - and convenience - it should be implemented\n");
|
||||
// UPDATE:
|
||||
// it's actually probably better that we don't do this here and instead
|
||||
// take care of it in the approveRegistrationCallback in letsencrypt-express
|
||||
cb(null, true);
|
||||
}
|
||||
, _registerHelper: function (args, cb) {
|
||||
|
|
|
@ -72,7 +72,8 @@ function createAccount(args, handlers) {
|
|||
});
|
||||
}
|
||||
|
||||
function getAccount(accountId, args, handlers) {
|
||||
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'];
|
||||
|
|
352
lib/core.js
352
lib/core.js
|
@ -31,131 +31,205 @@ function getAcmeUrls(args) {
|
|||
});
|
||||
}
|
||||
|
||||
function writeCertificateAsync(result, args, defaults, handlers) {
|
||||
if (args.debug) {
|
||||
console.log("got certificate!");
|
||||
}
|
||||
|
||||
result.fullchain = result.cert + '\n' + result.ca;
|
||||
|
||||
function readRenewalConfig(args) {
|
||||
var pyconf = PromiseA.promisifyAll(require('pyconf'));
|
||||
|
||||
return pyconf.readFileAsync(args.renewalPath).then(function (obj) {
|
||||
return obj;
|
||||
return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) {
|
||||
return pyobj;
|
||||
}, function () {
|
||||
return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (obj) {
|
||||
return obj;
|
||||
});
|
||||
}).then(function (obj) {
|
||||
obj.checkpoint = parseInt(obj.checkpoint, 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');
|
||||
|
||||
if (args.debug) {
|
||||
console.log('################ privkeyPath ################');
|
||||
console.log(privkeyPath);
|
||||
}
|
||||
|
||||
var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]);
|
||||
|
||||
var checkpoint = obj.checkpoint.toString();
|
||||
var certArchive = path.join(archiveDir, 'cert' + checkpoint + '.pem');
|
||||
var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoint + '.pem');
|
||||
var chainArchive = path.join(archiveDir, 'chain'+ checkpoint + '.pem');
|
||||
var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoint + '.pem');
|
||||
|
||||
return mkdirpAsync(archiveDir).then(function () {
|
||||
return PromiseA.all([
|
||||
sfs.writeFileAsync(certArchive, result.cert, 'ascii')
|
||||
, sfs.writeFileAsync(chainArchive, result.ca || result.chain, 'ascii')
|
||||
, sfs.writeFileAsync(fullchainArchive, result.fullchain, 'ascii')
|
||||
, sfs.writeFileAsync(privkeyArchive, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii')
|
||||
]);
|
||||
}).then(function () {
|
||||
return mkdirpAsync(liveDir);
|
||||
}).then(function () {
|
||||
return PromiseA.all([
|
||||
sfs.writeFileAsync(certPath, result.cert, 'ascii')
|
||||
, sfs.writeFileAsync(chainPath, result.ca || result.chain, 'ascii')
|
||||
, sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii')
|
||||
, sfs.writeFileAsync(privkeyPath, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii')
|
||||
]);
|
||||
}).then(function () {
|
||||
obj.checkpoint += 1;
|
||||
|
||||
var updates = {
|
||||
account: args.accountId || args.account.id
|
||||
|
||||
, cert: certPath
|
||||
, privkey: privkeyPath
|
||||
, chain: chainPath
|
||||
, fullchain: fullchainPath
|
||||
, configDir: args.configDir
|
||||
, workDir: args.workDir
|
||||
, tos: args.agreeTos && true
|
||||
, http01Port: args.http01Port
|
||||
, keyPath: args.domainPrivateKeyPath || args.privkeyPath
|
||||
, email: args.email
|
||||
, domains: args.domains
|
||||
, rsaKeySize: args.rsaKeySize
|
||||
, checkpoints: obj.checkpoint
|
||||
// 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
|
||||
// yes, it's an array. weird, right?
|
||||
, webrootPath: args.webrootPath && [args.webrootPath] || []
|
||||
, server: args.server || args.acmeDiscoveryUrl
|
||||
, 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 object or
|
||||
// annotations will be lost
|
||||
Object.keys(updates).forEach(function (key) {
|
||||
obj[key] = updates[key];
|
||||
});
|
||||
|
||||
return mkdirpAsync(path.dirname(args.renewalPath)).then(function () {
|
||||
return pyconf.writeFileAsync(args.renewalPath, obj);
|
||||
});
|
||||
}).then(function () {
|
||||
|
||||
return {
|
||||
certPath: certPath
|
||||
, chainPath: chainPath
|
||||
, fullchainPath: fullchainPath
|
||||
, privkeyPath: privkeyPath
|
||||
|
||||
// some ambiguity here...
|
||||
, privkey: result.key || result.privkey || args.domainPrivateKeyPem
|
||||
, fullchain: result.fullchain || result.cert
|
||||
, chain: result.ca || result.chain
|
||||
// especially this one... might be cert only, might be fullchain
|
||||
, cert: result.cert
|
||||
|
||||
, issuedAt: Date.now()
|
||||
, lifetime: defaults.lifetime || handlers.lifetime
|
||||
};
|
||||
return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) {
|
||||
return pyobj;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getCertificateAsync(account, args, defaults, handlers) {
|
||||
function writeRenewalConfig(args) {
|
||||
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');
|
||||
|
||||
if (args.debug) {
|
||||
console.log('################ privkeyPath ################');
|
||||
console.log(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 () {
|
||||
return pyobj;
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
if (args.debug) {
|
||||
console.log("got certificate!");
|
||||
}
|
||||
|
||||
var obj = args.pyobj;
|
||||
var result = args.pems;
|
||||
|
||||
result.fullchain = result.cert + '\n' + 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');
|
||||
|
||||
if (args.debug) {
|
||||
console.log('################ privkeyPath ################');
|
||||
console.log(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.ca || result.chain, 'ascii')
|
||||
, sfs.writeFileAsync(fullchainArchive, result.fullchain, 'ascii')
|
||||
, sfs.writeFileAsync(privkeyArchive, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii')
|
||||
]);
|
||||
}).then(function () {
|
||||
return mkdirpAsync(liveDir);
|
||||
}).then(function () {
|
||||
return PromiseA.all([
|
||||
sfs.writeFileAsync(certPath, result.cert, 'ascii')
|
||||
, sfs.writeFileAsync(chainPath, result.ca || result.chain, 'ascii')
|
||||
, sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii')
|
||||
, sfs.writeFileAsync(privkeyPath, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii')
|
||||
]);
|
||||
}).then(function () {
|
||||
obj.checkpoints += 1;
|
||||
args.checkpoints += 1;
|
||||
|
||||
return writeRenewalConfig(args);
|
||||
}).then(function () {
|
||||
|
||||
return {
|
||||
certPath: certPath
|
||||
, chainPath: chainPath
|
||||
, fullchainPath: fullchainPath
|
||||
, privkeyPath: privkeyPath
|
||||
|
||||
// some ambiguity here...
|
||||
, privkey: result.key || result.privkey || args.domainPrivateKeyPem
|
||||
, fullchain: result.fullchain || result.cert
|
||||
, chain: result.ca || result.chain
|
||||
// especially this one... might be cert only, might be fullchain
|
||||
, cert: result.cert
|
||||
|
||||
, issuedAt: Date.now()
|
||||
, lifetime: defaults.lifetime || handlers.lifetime
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getCertificateAsync(args, defaults, handlers) {
|
||||
var account = args.account;
|
||||
|
||||
return leCrypto.generateRsaKeypairAsync(args.rsaKeySize, 65537).then(function (domainKey) {
|
||||
if (args.debug) {
|
||||
console.log("get certificate");
|
||||
|
@ -214,11 +288,12 @@ function getCertificateAsync(account, args, defaults, handlers) {
|
|||
}
|
||||
});
|
||||
}).then(function (results) {
|
||||
return writeCertificateAsync(results, args, defaults, handlers);
|
||||
args.pems = results;
|
||||
return writeCertificateAsync(args, defaults, handlers);
|
||||
});
|
||||
}
|
||||
|
||||
function getOrCreateDomainCertificate(account, args, defaults, handlers) {
|
||||
function getOrCreateDomainCertificate(args, defaults, handlers) {
|
||||
return fetchFromConfigLiveDir(args).then(function (certs) {
|
||||
// if nothing, register and save
|
||||
// if something, check date (don't register unless 30+ days)
|
||||
|
@ -228,15 +303,15 @@ function getOrCreateDomainCertificate(account, args, defaults, handlers) {
|
|||
//console.log(certs);
|
||||
if (!certs) {
|
||||
// no certs, seems like a good time to get some
|
||||
return getCertificateAsync(account, args, defaults, handlers);
|
||||
return getCertificateAsync(args, defaults, handlers);
|
||||
}
|
||||
else if (certs.issuedAt > (27 * 24 * 60 * 60 * 1000)) {
|
||||
// cert is at least 27 days old we can renew that
|
||||
return getCertificateAsync(account, args, defaults, handlers);
|
||||
return getCertificateAsync(args, defaults, handlers);
|
||||
}
|
||||
else if (args.duplicate) {
|
||||
// YOLO! I be gettin' fresh certs 'erday! Yo!
|
||||
return getCertificateAsync(account, args, defaults, handlers);
|
||||
return getCertificateAsync(args, defaults, handlers);
|
||||
}
|
||||
else {
|
||||
console.warn('[WARN] Ignoring renewal attempt for certificate less than 27 days old. Use args.duplicate to force.');
|
||||
|
@ -244,16 +319,10 @@ function getOrCreateDomainCertificate(account, args, defaults, handlers) {
|
|||
return certs;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getOrCreateAcmeAccount(args, defaults, handlers) {
|
||||
var pyconf = PromiseA.promisifyAll(require('pyconf'));
|
||||
var server = args.server;
|
||||
var acmeHostname = require('url').parse(server).hostname;
|
||||
var configDir = args.configDir;
|
||||
|
||||
args.renewalPath = args.renewalPath || path.join(configDir, 'renewal', args.domains[0] + '.conf');
|
||||
args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory');
|
||||
|
||||
return pyconf.readFileAsync(args.renewalPath).then(function (renewal) {
|
||||
var accountId = renewal.account;
|
||||
|
@ -279,7 +348,8 @@ function getOrCreateAcmeAccount(args, defaults, handlers) {
|
|||
if (args.debug) {
|
||||
console.log('[LE] use account');
|
||||
}
|
||||
return Accounts.getAccount(accountId, args, handlers);
|
||||
args.accountId = accountId;
|
||||
return Accounts.getAccount(args, handlers);
|
||||
} else {
|
||||
if (args.debug) {
|
||||
console.log('[LE] create account');
|
||||
|
@ -322,13 +392,25 @@ module.exports.create = function (defaults, handlers) {
|
|||
copy = merge(args, defaults);
|
||||
tplCopy(copy);
|
||||
|
||||
if (args.debug) {
|
||||
if (copy.debug) {
|
||||
console.log('[LE DEBUG] reg domains', args.domains);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) {
|
||||
console.log("account", account);
|
||||
args.account = account;
|
||||
return getOrCreateDomainCertificate(account, copy, defaults, handlers);
|
||||
//console.log("account", account);
|
||||
copy.account = account;
|
||||
|
||||
return getOrCreateRenewal(copy).then(function (pyobj) {
|
||||
|
||||
copy.pyobj = pyobj;
|
||||
return getOrCreateDomainCertificate(copy, defaults, handlers);
|
||||
});
|
||||
});
|
||||
}
|
||||
, fetchAsync: function (args) {
|
||||
|
|
|
@ -40,7 +40,7 @@ authenticator = webroot
|
|||
domains = :hostnames #comma,delimited,list
|
||||
rsa_key_size = :rsa_key_size
|
||||
# starts at 0 and increments at every renewal
|
||||
checkpoints = :checkpoint_count
|
||||
checkpoints = -1
|
||||
manual_test_mode = False
|
||||
apache = False
|
||||
cert_path = :cert_path
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird');
|
||||
var pyconf = PromiseA.promisifyAll(require('pyconf'));
|
||||
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
|
||||
var path = require('path');
|
||||
|
||||
pyconf.readFileAsync(path.join(__dirname, 'lib', 'renewal.conf.tpl')).then(function (obj) {
|
||||
var domains = ['example.com', 'www.example.com'];
|
||||
var webrootPath = '/tmp/www/example.com';
|
||||
|
||||
console.log(obj);
|
||||
|
||||
var keys = obj.__keys;
|
||||
var lines = obj.__lines;
|
||||
|
||||
obj.__keys = null;
|
||||
obj.__lines = null;
|
||||
|
||||
var updates = {
|
||||
account: 'ACCOUNT_ID'
|
||||
|
||||
, cert: 'CERT_PATH'
|
||||
, privkey: 'PRIVATEKEY_PATH'
|
||||
, configDir: 'CONFIG_DIR'
|
||||
, tos: true
|
||||
, http01Port: 80
|
||||
, domains: domains
|
||||
};
|
||||
|
||||
// final section is completely dynamic
|
||||
// :hostname = :webroot_path
|
||||
domains.forEach(function (hostname) {
|
||||
updates[hostname] = webrootPath;
|
||||
});
|
||||
|
||||
// must write back to the original object or
|
||||
// annotations will be lost
|
||||
Object.keys(updates).forEach(function (key) {
|
||||
obj[key] = updates[key];
|
||||
});
|
||||
|
||||
var renewalPath = '/tmp/letsencrypt/renewal/example.com.conf';
|
||||
return mkdirpAsync(path.dirname(renewalPath)).then(function () {
|
||||
console.log(obj);
|
||||
obj.__keys = keys;
|
||||
obj.__lines = lines;
|
||||
return pyconf.writeFileAsync(renewalPath, obj);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue