Browse Source

partial refactor

greenlock
AJ ONeal 8 years ago
parent
commit
fd02f44c13
  1. 7
      examples/README.md
  2. 64
      examples/simple.js
  3. 71
      index.js
  4. 195
      lib/accounts.js
  5. 79
      lib/common.js
  6. 628
      lib/core.js
  7. 10
      lib/default-handlers.js
  8. 517
      lib/pycompat.js
  9. 2
      package.json

7
examples/README.md

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

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

71
index.js

@ -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.merge = require('./lib/common').merge;
LE.configDir = leCore.configDir;
LE.logsDir = leCore.logsDir;
LE.workDir = leCore.workDir;
LE.acmeChallengPrefix = leCore.acmeChallengPrefix;
LE.knownEndpoints = leCore.knownEndpoints;
LE.privkeyPath = ':config/live/:hostname/privkey.pem';
LE.fullchainPath = ':config/live/:hostname/fullchain.pem';
LE.certPath = ':config/live/:hostname/cert.pem';
LE.chainPath = ':config/live/:hostname/chain.pem';
LE.renewalPath = ':config/renewal/:hostname.conf';
LE.accountsDir = ':config/accounts/:server';
LE.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));

195
lib/accounts.js

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

79
lib/common.js

@ -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 = {};
Object.keys(defaults).forEach(function (key) { allDefaults.forEach(function (defaults) {
copy[key] = defaults[key]; Object.keys(defaults).forEach(function (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;
});
};

628
lib/core.js

@ -1,454 +1,228 @@
'use strict'; 'use strict';
var PromiseA = require('bluebird');
var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA);
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
var path = require('path');
var fs = PromiseA.promisifyAll(require('fs'));
var sfs = require('safe-replace');
var LE = require('../'); var LE = require('../');
var LeCore = PromiseA.promisifyAll(require('letiny-core'));
var Accounts = require('./accounts');
var merge = require('./common').merge;
var tplCopy = require('./common').tplCopy;
var fetchFromConfigLiveDir = require('./common').fetchFromDisk;
var ipc = {}; // in-process cache var ipc = {}; // in-process cache
function getAcmeUrls(args) { module.exports.create = function (defaults, handlers, backend) {
var now = Date.now(); defaults.server = defaults.server || LE.liveServer;
handlers.merge = require('./common').merge;
handlers.tplCopy = require('./common').tplCopy;
var PromiseA = require('bluebird');
var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA);
var LeCore = PromiseA.promisifyAll(require('letiny-core'));
var crypto = require('crypto');
function createAccount(args, handlers) {
// 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
// TODO check response header on request for cache time , debug: defaults.debug || args.debug || handlers.debug
if ((now - ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { }).then(function (body) {
return PromiseA.resolve(ipc.acmeUrls); // TODO XXX use sha256 (the python client uses md5)
} // TODO ssh fingerprint (noted on rsa-compat issues page, I believe)
keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex');
keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex');
return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { var accountId = keypair.publicKeyMd5;
ipc.acmeUrlsUpdatedAt = Date.now(); var regr = { body: body };
ipc.acmeUrls = data; var account = {};
return ipc.acmeUrls; args.accountId = accountId;
});
}
function readRenewalConfig(args) { account.keypair = keypair;
var pyconf = PromiseA.promisifyAll(require('pyconf')); account.regr = regr;
account.accountId = accountId;
account.id = accountId;
return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) { args.account = account;
return pyobj;
}, function () {
return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) {
return pyobj;
});
});
}
function writeRenewalConfig(args) { return backend.setAccountAsync(args, account).then(function () {
function log() { return account;
if (args.debug) { });
console.log.apply(console, arguments); });
} });
} }
var pyobj = args.pyobj; function getAcmeUrls(args) {
pyobj.checkpoints = parseInt(pyobj.checkpoints, 10) || 0; var now = Date.now();
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 // TODO check response header on request for cache time
// :hostname = :webroot_path if ((now - ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) {
args.domains.forEach(function (hostname) { return PromiseA.resolve(ipc.acmeUrls);
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 return LeCore.getAcmeUrlsAsync(args.server).then(function (data) {
// args.configDir = args.configDir || pyobj.configDir; ipc.acmeUrlsUpdatedAt = Date.now();
ipc.acmeUrls = data;
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); return ipc.acmeUrls;
}); });
}
function writeCertificateAsync(args, defaults, handlers) {
function log() {
if (args.debug) {
console.log.apply(console, arguments);
}
} }
log("[le/core.js] got certificate!"); function getCertificateAsync(args, defaults, handlers) {
function log() {
var obj = args.pyobj; if (args.debug || defaults.debug) {
var result = args.pems; console.log.apply(console, arguments);
}
result.fullchain = result.cert + '\n' + (result.chain || result.ca);
obj.checkpoints = parseInt(obj.checkpoints, 10) || 0;
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
var certPath = args.certPath || obj.cert || path.join(liveDir, 'cert.pem');
var fullchainPath = args.fullchainPath || obj.fullchain || path.join(liveDir, 'fullchain.pem');
var chainPath = args.chainPath || obj.chain || path.join(liveDir, 'chain.pem');
var privkeyPath = args.privkeyPath || obj.privkey
//|| args.domainPrivateKeyPath || args.domainKeyPath || obj.keyPath
|| path.join(liveDir, 'privkey.pem');
log('[le/core.js] privkeyPath', privkeyPath);
var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]);
var checkpoints = obj.checkpoints.toString();
var certArchive = path.join(archiveDir, 'cert' + checkpoints + '.pem');
var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoints + '.pem');
var chainArchive = path.join(archiveDir, 'chain'+ checkpoints + '.pem');
var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoints + '.pem');
return mkdirpAsync(archiveDir).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certArchive, result.cert, 'ascii')
, sfs.writeFileAsync(chainArchive, (result.chain || result.ca), 'ascii')
, sfs.writeFileAsync(fullchainArchive, result.fullchain, 'ascii')
, sfs.writeFileAsync(
privkeyArchive
// TODO nix args.key, args.domainPrivateKeyPem ??
, (result.privkey || result.key) || RSA.exportPrivatePem(args.domainKeypair)
, 'ascii'
)
]);
}).then(function () {
return mkdirpAsync(liveDir);
}).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certPath, result.cert, 'ascii')
, sfs.writeFileAsync(chainPath, (result.chain || result.ca), 'ascii')
, sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii')
, sfs.writeFileAsync(
privkeyPath
// TODO nix args.key, args.domainPrivateKeyPem ??
, (result.privkey || result.key) || RSA.exportPrivatePem(args.domainKeypair)
, 'ascii'
)
]);
}).then(function () {
obj.checkpoints += 1;
args.checkpoints += 1;
return writeRenewalConfig(args);
}).then(function () {
var getCertInfo = require('./cert-info').getCertInfo;
// XXX Note: Parsing the certificate info comes at a great cost (~500kb)
var certInfo = getCertInfo(result.cert);
return {
certPath: certPath
, chainPath: chainPath
, fullchainPath: fullchainPath
, privkeyPath: privkeyPath
// TODO nix keypair
, keypair: args.domainKeypair
// TODO nix args.key, args.domainPrivateKeyPem ??
// some ambiguity here...
, privkey: (result.privkey || result.key) || RSA.exportPrivatePem(args.domainKeypair)
, fullchain: result.fullchain || (result.cert + '\n' + result.chain)
, chain: (result.chain || result.ca)
// especially this one... might be cert only, might be fullchain
, cert: result.cert
, issuedAt: Date(certInfo.notBefore.value).valueOf() // Date.now()
, expiresAt: Date(certInfo.notAfter.value).valueOf()
, lifetime: defaults.lifetime || handlers.lifetime
};
});
}
function getCertificateAsync(args, defaults, handlers) {
function log() {
if (args.debug || defaults.debug) {
console.log.apply(console, arguments);
} }
}
var account = args.account; var account = args.account;
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) {
return RSA.import({ privateKeyPem: pem });
promise = fs.readFileAsync(args.domainKeyPath, 'ascii').then(function (pem) { }, function (/*err*/) {
return RSA.import({ privateKeyPem: pem }); return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) {
}, function (/*err*/) { keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) { keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair);
return mkdirpAsync(path.dirname(args.domainKeyPath)).then(function () { return backend.setPrivatePem(args, keypair);
return fs.writeFileAsync(args.domainKeyPath, keypair.privateKeyPem, 'ascii').then(function () {
return keypair;
});
}); });
}); });
});
return promise.then(function (domainKeypair) {
log("[le/core.js] get certificate");
args.domainKeypair = domainKeypair;
//args.registration = domainKey;
return LeCore.getCertificateAsync({
debug: args.debug
, newAuthzUrl: args._acmeUrls.newAuthz
, newCertUrl: args._acmeUrls.newCert
, accountKeypair: RSA.import(account.keypair)
, domainKeypair: domainKeypair
, domains: args.domains
//
// IMPORTANT
//
// setChallenge and removeChallenge are handed defaults
// instead of args because getChallenge does not have
// access to args
// (args is per-request, defaults is per instance)
//
, setChallenge: function (domain, key, value, done) {
var copy = merge(defaults, { domains: [domain] });
tplCopy(copy);
args.domains = [domain];
args.webrootPath = args.webrootPath;
if (4 === handlers.setChallenge.length) {
handlers.setChallenge(copy, key, value, done);
}
else if (5 === handlers.setChallenge.length) {
handlers.setChallenge(copy, domain, key, value, done);
}
else {
done(new Error("handlers.setChallenge receives the wrong number of arguments"));
}
}
, removeChallenge: function (domain, key, done) {
var copy = merge(defaults, { domains: [domain] });
tplCopy(copy);
if (3 === handlers.removeChallenge.length) { return promise.then(function (domainKeypair) {
handlers.removeChallenge(copy, key, done); log("[le/core.js] get certificate");
}
else if (4 === handlers.removeChallenge.length) { args.domainKeypair = domainKeypair;
handlers.removeChallenge(copy, domain, key, done); //args.registration = domainKey;
return LeCore.getCertificateAsync({
debug: args.debug
, newAuthzUrl: args._acmeUrls.newAuthz
, newCertUrl: args._acmeUrls.newCert
, accountKeypair: RSA.import(account.keypair)
, domainKeypair: domainKeypair
, domains: args.domains
//
// IMPORTANT
//
// setChallenge and removeChallenge are handed defaults
// instead of args because getChallenge does not have
// access to args
// (args is per-request, defaults is per instance)
//
, setChallenge: function (domain, key, value, done) {
var copy = handlers.merge({ domains: [domain] }, defaults);
handlers.tplCopy(copy);
args.domains = [domain];
//args.domains = args.domains || [domain];
if (4 === handlers.setChallenge.length) {
console.warn('[WARNING] deprecated use. Define setChallenge as function (opts, domain, key, val, cb) { }');
handlers.setChallenge(copy, key, value, done);
}
else if (5 === handlers.setChallenge.length) {
handlers.setChallenge(copy, domain, key, value, done);
}
else {
done(new Error("handlers.setChallenge receives the wrong number of arguments"));
}
} }
else { , removeChallenge: function (domain, key, done) {
done(new Error("handlers.removeChallenge receives the wrong number of arguments")); var copy = handlers.merge({ domains: [domain] }, defaults);
handlers.tplCopy(copy);
if (3 === handlers.removeChallenge.length) {
handlers.removeChallenge(copy, key, done);
}
else if (4 === handlers.removeChallenge.length) {
handlers.removeChallenge(copy, domain, key, done);
}
else {
done(new Error("handlers.removeChallenge receives the wrong number of arguments"));
}
} }
} });
}).then(function (results) {
// { cert, chain, fullchain, privkey }
args.pems = results;
return backend.setRegistration(args, defaults, handlers);
}); });
}).then(function (results) {
// { cert, chain, fullchain, privkey }
args.pems = results;
return writeCertificateAsync(args, defaults, handlers);
});
}
function getOrCreateDomainCertificate(args, defaults, handlers) {
if (args.duplicate) {
// we're forcing a refresh via 'dupliate: true'
return getCertificateAsync(args, defaults, handlers);
} }
return fetchFromConfigLiveDir(args).then(function (certs) { function getOrCreateDomainCertificate(args, defaults, handlers) {
var halfLife = (certs.expiresAt - certs.issuedAt) / 2; if (args.duplicate) {
// we're forcing a refresh via 'dupliate: true'
if (!certs || (Date.now() - certs.issuedAt) > halfLife) {
// There is no cert available
// Or the cert is more than half-expired
return getCertificateAsync(args, defaults, handlers); return getCertificateAsync(args, defaults, handlers);
} }
return PromiseA.reject(new Error( return backend.getRegistration(args).then(function (certs) {
"[ERROR] Certificate issued at '" var halfLife = (certs.expiresAt - certs.issuedAt) / 2;
+ new Date(certs.issuedAt).toISOString() + "' and expires at '"
+ new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '"
+ new Date(certs.issuedA + halfLife).toISOString() + "'. Set { duplicate: true } to force."
));
});
}
// returns 'account' from lib/accounts { meta, regr, keypair, accountId (id) }
function getOrCreateAcmeAccount(args, defaults, handlers) {
function log() {
if (args.debug) {
console.log.apply(console, arguments);
}
}
var pyconf = PromiseA.promisifyAll(require('pyconf')); if (!certs || (Date.now() - certs.issuedAt) > halfLife) {
// There is no cert available
// Or the cert is more than half-expired
return getCertificateAsync(args, defaults, handlers);
}
return pyconf.readFileAsync(args.renewalPath).then(function (renewal) { return PromiseA.reject(new Error(
var accountId = renewal.account; "[ERROR] Certificate issued at '"
renewal = renewal.account; + new Date(certs.issuedAt).toISOString() + "' and expires at '"
+ new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '"
+ new Date(certs.issuedA + halfLife).toISOString() + "'. Set { duplicate: true } to force."
));
});
}
return accountId; // returns 'account' from lib/accounts { meta, regr, keypair, accountId (id) }
}, function (err) { function getOrCreateAcmeAccount(args, defaults, handlers) {
if ("ENOENT" === err.code) { function log() {
log("[le/core.js] try email"); if (args.debug) {
return Accounts.getAccountIdByEmail(args, handlers); console.log.apply(console, arguments);
}
} }
return PromiseA.reject(err); return backend.getAccountId(args).then(function (accountId) {
}).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) {
args._acmeUrls = urls; args._acmeUrls = urls;
if (accountId) { if (accountId) {
log('[le/core.js] use account'); log('[le/core.js] use account');
args.accountId = accountId; args.accountId = accountId;
return Accounts.getAccount(args, handlers); return Accounts.getAccount(args, handlers);
} else { } else {
log('[le/core.js] create account'); log('[le/core.js] create account');
return Accounts.createAccount(args, handlers); return Accounts.createAccount(args, handlers);
}
});
}).then(function (account) {
/*
if (renewal.account !== account) {
// the account has become corrupt, re-register
return;
} }
*/
log('[le/core.js] created account');
return account;
}); });
}).then(function (account) { }
/*
if (renewal.account !== account) {
// the account has become corrupt, re-register
return;
}
*/
log('[le/core.js] created account');
return account;
});
/*
return fs.readdirAsync(accountsDir, function (nodes) {
return PromiseA.all(nodes.map(function (node) {
var reMd5 = /[a-f0-9]{32}/i;
if (reMd5.test(node)) {
}
}));
});
*/
}
module.exports.create = function (defaults, handlers) {
defaults.server = defaults.server || LE.liveServer;
var wrapped = { 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);
}));
}); });
} }
}; };

10
lib/default-handlers.js

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

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

2
package.json

@ -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": {

Loading…
Cancel
Save