partial refactor

This commit is contained in:
AJ ONeal 2016-08-04 18:49:35 -04:00
parent 4a389606b0
commit fd02f44c13
9 changed files with 828 additions and 761 deletions

View File

@ -29,9 +29,6 @@ No, I wanted node-letsencrypt
============================= =============================
Well, take a look at the API in the main README 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. Feel free to open an issues to request any particular type of example.
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.

64
examples/simple.js Normal file
View File

@ -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');
}
});

View File

@ -4,45 +4,34 @@
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var leCore = require('letiny-core'); var leCore = require('letiny-core');
var utils = require('./lib/common');
var merge = require('./lib/common').merge; var merge = require('./lib/common').merge;
var tplCopy = require('./lib/common').tplCopy; var tplCopy = require('./lib/common').tplCopy;
var LE = module.exports; 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.merge = require('./lib/common').merge;
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.defaults = { LE.defaults = {
privkeyPath: LE.privkeyPath server: leCore.productionServerUrl
, fullchainPath: LE.fullchainPath , stagingServer: leCore.stagingServerUrl
, certPath: LE.certPath , liveServer: leCore.productionServerUrl
, chainPath: LE.chainPath
, renewalPath: LE.renewalPath , productionServerUrl: leCore.productionServerUrl
, accountsDir: LE.accountsDir , stagingServerUrl: leCore.stagingServerUrl
, server: LE.productionServerUrl
, acmeChallengePrefix: leCore.acmeChallengePrefix
}; };
// backwards compat // backwards compat
LE.stagingServer = leCore.stagingServerUrl; Object.keys(LE.defaults).forEach(function (key) {
LE.liveServer = leCore.productionServerUrl; LE[key] = LE.defaults[key];
LE.knownUrls = leCore.knownEndpoints; });
LE.merge = require('./lib/common').merge;
LE.tplConfigDir = require('./lib/common').tplConfigDir;
// backend, defaults, handlers // backend, defaults, handlers
LE.create = function (defaults, handlers, backend) { 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) { handlers = {}; }
if (!handlers.lifetime) { handlers.lifetime = 90 * 24 * 60 * 60 * 1000; } if (!handlers.lifetime) { handlers.lifetime = 90 * 24 * 60 * 60 * 1000; }
if (!handlers.renewWithin) { handlers.renewWithin = 3 * 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 // the request it came from... it's kinda stateless in that way
// but realistically there only needs to be one handler and one // but realistically there only needs to be one handler and one
// "directory" for this. It's not that big of a deal. // "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 getChallenge = require('./lib/default-handlers').getChallenge;
var copy = merge(defaults, { domains: [hostname] }); var copy = merge({ domains: [hostname] }, defaults);
tplCopy(copy); tplCopy(copy);
defaultos.domains = [hostname]; defaultos.domains = [hostname];
if (3 === getChallenge.length) { if (3 === getChallenge.length) {
console.warn('[WARNING] Deprecated use. Define getChallenge as function (opts, domain, key, cb) { }');
getChallenge(defaultos, key, done); getChallenge(defaultos, key, done);
} }
else if (4 === getChallenge.length) { else if (4 === getChallenge.length) {
@ -102,22 +92,10 @@ LE.create = function (defaults, handlers, backend) {
} }
handlers.agreeToTerms = require('./lib/default-handlers').agreeToTerms; 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); backend = PromiseA.promisifyAll(backend);
var utils = require('./lib/common');
//var attempts = {}; // should exist in master process only //var attempts = {}; // should exist in master process only
var le; var le;
@ -151,7 +129,7 @@ LE.create = function (defaults, handlers, backend) {
return; return;
} }
var copy = LE.merge(defaults, args); var copy = LE.merge(args, defaults);
var err; var err;
if (!utils.isValidDomain(args.domains[0])) { if (!utils.isValidDomain(args.domains[0])) {
@ -185,6 +163,11 @@ LE.create = function (defaults, handlers, backend) {
if (defaults.debug || args.debug) { if (defaults.debug || args.debug) {
console.log('[LE] fetch'); 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) { return backend.fetchAsync(args).then(function (certInfo) {
if (args.debug) { if (args.debug) {
console.log('[LE] raw fetch certs', certInfo && Object.keys(certInfo)); console.log('[LE] raw fetch certs', certInfo && Object.keys(certInfo));

View File

@ -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;

View File

@ -22,7 +22,7 @@ module.exports.isValidDomain = function (domain) {
return ''; return '';
}; };
module.exports.tplConfigDir = function merge(configDir, defaults) { module.exports.tplConfigDir = function (configDir, defaults) {
var homedir = require('homedir')(); var homedir = require('homedir')();
Object.keys(defaults).forEach(function (key) { Object.keys(defaults).forEach(function (key) {
if ('string' === typeof defaults[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 = {}; var copy = {};
allDefaults.forEach(function (defaults) {
Object.keys(defaults).forEach(function (key) { Object.keys(defaults).forEach(function (key) {
copy[key] = defaults[key]; copy[key] = defaults[key];
}); });
});
Object.keys(args).forEach(function (key) { Object.keys(args).forEach(function (key) {
copy[key] = args[key]; copy[key] = args[key];
}); });
@ -45,7 +50,19 @@ module.exports.merge = function merge(defaults, args) {
return copy; 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 homedir = require('homedir')();
var tpls = { var tpls = {
hostname: (copy.domains || [])[0] hostname: (copy.domains || [])[0]
@ -71,55 +88,3 @@ module.exports.tplCopy = function merge(copy) {
//return 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;
});
};

View File

@ -1,20 +1,58 @@
'use strict'; 'use strict';
var LE = require('../');
var ipc = {}; // in-process cache
module.exports.create = function (defaults, handlers, backend) {
defaults.server = defaults.server || LE.liveServer;
handlers.merge = require('./common').merge;
handlers.tplCopy = require('./common').tplCopy;
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA); 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 LeCore = PromiseA.promisifyAll(require('letiny-core'));
var Accounts = require('./accounts'); var crypto = require('crypto');
var merge = require('./common').merge; function createAccount(args, handlers) {
var tplCopy = require('./common').tplCopy; // arg.rsaBitLength args.rsaExponent
var fetchFromConfigLiveDir = require('./common').fetchFromDisk; return RSA.generateKeypairAsync(args.rsaKeySize || 2048, 65537, { public: true, pem: true }).then(function (keypair) {
var ipc = {}; // in-process cache return LeCore.registerNewAccountAsync({
email: args.email
, newRegUrl: args._acmeUrls.newReg
, agreeToTerms: function (tosUrl, agree) {
// args.email = email; // already there
args.tosUrl = tosUrl;
handlers.agreeToTerms(args, agree);
}
, accountKeypair: keypair
, debug: defaults.debug || args.debug || handlers.debug
}).then(function (body) {
// TODO XXX use sha256 (the python client uses md5)
// TODO ssh fingerprint (noted on rsa-compat issues page, I believe)
keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex');
keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex');
var accountId = keypair.publicKeyMd5;
var regr = { body: body };
var account = {};
args.accountId = accountId;
account.keypair = keypair;
account.regr = regr;
account.accountId = accountId;
account.id = accountId;
args.account = account;
return backend.setAccountAsync(args, account).then(function () {
return account;
});
});
});
}
function getAcmeUrls(args) { function getAcmeUrls(args) {
var now = Date.now(); var now = Date.now();
@ -32,228 +70,6 @@ function getAcmeUrls(args) {
}); });
} }
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, 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 getCertificateAsync(args, defaults, handlers) {
function log() { function log() {
if (args.debug || defaults.debug) { if (args.debug || defaults.debug) {
@ -265,17 +81,13 @@ function getCertificateAsync(args, defaults, handlers) {
var promise; var promise;
var keypairOpts = { public: true, pem: true }; var keypairOpts = { public: true, pem: true };
log('[le/core.js] domainKeyPath:', args.domainKeyPath); promise = backend.getPrivatePem(args).then(function (pem) {
promise = fs.readFileAsync(args.domainKeyPath, 'ascii').then(function (pem) {
return RSA.import({ privateKeyPem: pem }); return RSA.import({ privateKeyPem: pem });
}, function (/*err*/) { }, function (/*err*/) {
return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) {
return mkdirpAsync(path.dirname(args.domainKeyPath)).then(function () { keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
return fs.writeFileAsync(args.domainKeyPath, keypair.privateKeyPem, 'ascii').then(function () { keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair);
return keypair; return backend.setPrivatePem(args, keypair);
});
});
}); });
}); });
@ -304,12 +116,13 @@ function getCertificateAsync(args, defaults, handlers) {
// (args is per-request, defaults is per instance) // (args is per-request, defaults is per instance)
// //
, setChallenge: function (domain, key, value, done) { , setChallenge: function (domain, key, value, done) {
var copy = merge(defaults, { domains: [domain] }); var copy = handlers.merge({ domains: [domain] }, defaults);
tplCopy(copy); handlers.tplCopy(copy);
args.domains = [domain]; args.domains = [domain];
args.webrootPath = args.webrootPath; //args.domains = args.domains || [domain];
if (4 === handlers.setChallenge.length) { 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); handlers.setChallenge(copy, key, value, done);
} }
else if (5 === handlers.setChallenge.length) { else if (5 === handlers.setChallenge.length) {
@ -320,8 +133,8 @@ function getCertificateAsync(args, defaults, handlers) {
} }
} }
, removeChallenge: function (domain, key, done) { , removeChallenge: function (domain, key, done) {
var copy = merge(defaults, { domains: [domain] }); var copy = handlers.merge({ domains: [domain] }, defaults);
tplCopy(copy); handlers.tplCopy(copy);
if (3 === handlers.removeChallenge.length) { if (3 === handlers.removeChallenge.length) {
handlers.removeChallenge(copy, key, done); handlers.removeChallenge(copy, key, done);
@ -337,7 +150,7 @@ function getCertificateAsync(args, defaults, handlers) {
}).then(function (results) { }).then(function (results) {
// { cert, chain, fullchain, privkey } // { cert, chain, fullchain, privkey }
args.pems = results; args.pems = results;
return writeCertificateAsync(args, defaults, handlers); return backend.setRegistration(args, defaults, handlers);
}); });
} }
@ -347,7 +160,7 @@ function getOrCreateDomainCertificate(args, defaults, handlers) {
return getCertificateAsync(args, defaults, handlers); return getCertificateAsync(args, defaults, handlers);
} }
return fetchFromConfigLiveDir(args).then(function (certs) { return backend.getRegistration(args).then(function (certs) {
var halfLife = (certs.expiresAt - certs.issuedAt) / 2; var halfLife = (certs.expiresAt - certs.issuedAt) / 2;
if (!certs || (Date.now() - certs.issuedAt) > halfLife) { if (!certs || (Date.now() - certs.issuedAt) > halfLife) {
@ -373,21 +186,7 @@ function getOrCreateAcmeAccount(args, defaults, handlers) {
} }
} }
var pyconf = PromiseA.promisifyAll(require('pyconf')); return backend.getAccountId(args).then(function (accountId) {
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);
}
return PromiseA.reject(err);
}).then(function (accountId) {
// Note: the ACME urls are always fetched fresh on purpose // Note: the ACME urls are always fetched fresh on purpose
return getAcmeUrls(args).then(function (urls) { return getAcmeUrls(args).then(function (urls) {
@ -413,42 +212,17 @@ function getOrCreateAcmeAccount(args, defaults, handlers) {
log('[le/core.js] created account'); log('[le/core.js] created account');
return 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 = { var wrapped = {
registerAsync: function (args) { registerAsync: function (args) {
var copy; var copy = handlers.merge(args, defaults);
// TODO move these defaults elsewhere? handlers.tplCopy(copy);
//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);
return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) {
copy.account = account; copy.account = account;
return getOrCreateRenewal(copy).then(function (pyobj) { return backend.getOrCreateRenewal(copy).then(function (pyobj) {
copy.pyobj = pyobj; copy.pyobj = pyobj;
return getOrCreateDomainCertificate(copy, defaults, handlers); return getOrCreateDomainCertificate(copy, defaults, handlers);
@ -459,55 +233,19 @@ module.exports.create = function (defaults, handlers) {
return PromiseA.reject(err); return PromiseA.reject(err);
}); });
} }
, fetchAsync: function (args) { , getOrCreateAccount: function (args) {
var copy = merge(args, defaults); // TODO
tplCopy(copy); keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
keypair.publicKeyPem = RSA.exportPublicPem(keypair);
return fetchFromConfigLiveDir(copy, defaults); return createAccount(args, handlers);
} }
, configureAsync: function (hargs) { , configureAsync: function (hargs) {
hargs.renewalPath = hargs.renewalPath || ':config/renewal/:hostname.conf';
var copy = merge(hargs, defaults); var copy = merge(hargs, defaults);
tplCopy(copy); tplCopy(copy);
return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) {
copy.account = account; copy.account = account;
return getOrCreateRenewal(copy); return backend.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);
}));
}); });
} }
}; };

View File

@ -3,11 +3,11 @@
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
module.exports.agreeToTerms = function (args, agree) { module.exports.agreeToTerms = function (args, agreeCb) {
agree(null, args.agreeTos); 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 hostname = args.domains[0];
var mkdirp = require('mkdirp'); 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]; //var hostname = args.domains[0];
//console.log("getting the challenge", args, key); //console.log("getting the challenge", args, key);
fs.readFile(path.join(args.webrootPath, key), 'utf8', done); 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]; //var hostname = args.domains[0];
fs.unlink(path.join(args.webrootPath, key), done); fs.unlink(path.join(args.webrootPath, key), done);

517
lib/pycompat.js Normal file
View File

@ -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);
});
}
};
};

View File

@ -30,8 +30,6 @@
}, },
"homepage": "https://github.com/Daplie/node-letsencrypt#readme", "homepage": "https://github.com/Daplie/node-letsencrypt#readme",
"devDependencies": { "devDependencies": {
"express": "^4.13.3",
"localhost.daplie.com-certificates": "^1.1.2"
}, },
"optionalDependencies": {}, "optionalDependencies": {},
"dependencies": { "dependencies": {