greenlock.js-ARCHIVED/lib/core.js

427 lines
15 KiB
JavaScript
Raw Normal View History

2015-12-15 11:37:39 +00:00
'use strict';
function _log(debug) {
2016-08-09 19:02:10 +00:00
if (debug) {
var args = Array.prototype.slice.call(arguments);
args.shift();
args.unshift("[le/lib/core.js]");
console.log.apply(console, args);
}
}
2016-08-05 22:54:28 +00:00
module.exports.create = function (le) {
2016-08-04 22:49:35 +00:00
var PromiseA = require('bluebird');
2016-08-08 23:14:53 +00:00
var utils = require('./utils');
2016-08-04 22:49:35 +00:00
var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA);
var log = le.log || _log; // allow custom log
var pendingRegistrations = {};
2016-08-04 22:49:35 +00:00
2016-08-06 06:05:04 +00:00
var core = {
//
// Helpers
//
getAcmeUrlsAsync: function (args) {
var now = Date.now();
// TODO check response header on request for cache time
if ((now - le._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) {
return PromiseA.resolve(le._ipc.acmeUrls);
}
2015-12-15 11:37:39 +00:00
2016-08-08 15:21:33 +00:00
return le.acme.getAcmeUrlsAsync(args.server).then(function (data) {
2016-08-06 06:05:04 +00:00
le._ipc.acmeUrlsUpdatedAt = Date.now();
le._ipc.acmeUrls = data;
2015-12-15 11:37:39 +00:00
2016-08-06 06:05:04 +00:00
return le._ipc.acmeUrls;
});
}
2015-12-20 05:13:41 +00:00
2016-08-06 06:05:04 +00:00
//
// The Main Enchilada
//
//
// Accounts
//
, accounts: {
2016-08-08 22:11:25 +00:00
// Accounts
2016-08-06 06:05:04 +00:00
registerAsync: function (args) {
2016-08-08 19:17:09 +00:00
var err;
2016-08-08 22:11:25 +00:00
var copy = utils.merge(args, le);
2016-08-08 23:14:53 +00:00
var disagreeTos;
2016-08-08 22:11:25 +00:00
args = utils.tplCopy(copy);
2016-08-08 19:17:09 +00:00
2016-08-08 23:14:53 +00:00
disagreeTos = (!args.agreeTos && 'undefined' !== typeof args.agreeTos);
if (!args.email || disagreeTos || (parseInt(args.rsaKeySize, 10) < 2048)) {
2016-08-08 19:17:09 +00:00
err = new Error(
"In order to register an account both 'email' and 'agreeTos' must be present"
+ " and 'rsaKeySize' must be 2048 or greater."
);
err.code = 'E_ARGS';
return PromiseA.reject(err);
}
return utils.testEmail(args.email).then(function () {
2016-08-08 22:11:25 +00:00
var keypairOpts = { public: true, pem: true };
var promise = le.store.accounts.checkKeypairAsync(args).then(function (keypair) {
2016-08-09 19:02:10 +00:00
if (keypair) {
return RSA.import(keypair);
}
2016-08-08 23:43:31 +00:00
if (args.accountKeypair) {
return le.store.accounts.setKeypairAsync(args, RSA.import(args.accountKeypair));
}
2016-08-08 22:11:25 +00:00
return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) {
keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
2016-08-08 23:14:53 +00:00
keypair.publicKeyPem = RSA.exportPublicPem(keypair);
2016-08-08 22:11:25 +00:00
keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair);
return le.store.accounts.setKeypairAsync(args, keypair);
});
});
2016-08-08 19:17:09 +00:00
2016-08-08 22:11:25 +00:00
return promise.then(function (keypair) {
2016-08-08 19:17:09 +00:00
// Note: the ACME urls are always fetched fresh on purpose
// TODO is this the right place for this?
return core.getAcmeUrlsAsync(args).then(function (urls) {
args._acmeUrls = urls;
return le.acme.registerNewAccountAsync({
email: args.email
, newRegUrl: args._acmeUrls.newReg
, agreeToTerms: function (tosUrl, agreeCb) {
if (true === args.agreeTos || tosUrl === args.agreeTos || tosUrl === le.agreeToTerms) {
agreeCb(null, tosUrl);
return;
}
// args.email = email; // already there
// args.domains = domains // already there
args.tosUrl = tosUrl;
le.agreeToTerms(args, agreeCb);
}
, accountKeypair: keypair
, debug: le.debug || args.debug
2016-08-08 23:14:53 +00:00
}).then(function (receipt) {
var reg = {
keypair: keypair
, receipt: receipt
, email: args.email
};
// TODO move templating of arguments to right here?
return le.store.accounts.setAsync(args, reg).then(function (account) {
// should now have account.id and account.accountId
args.account = account;
args.accountId = account.id;
2016-08-08 19:17:09 +00:00
return account;
});
});
2016-08-06 06:05:04 +00:00
});
});
2016-08-04 22:49:35 +00:00
});
2016-08-06 06:05:04 +00:00
}
2016-08-08 19:17:09 +00:00
2016-08-08 22:11:25 +00:00
// Accounts
2016-08-06 06:05:04 +00:00
, getAsync: function (args) {
return core.accounts.checkAsync(args).then(function (account) {
2016-08-08 19:17:09 +00:00
if (account) {
return account;
} else {
return core.accounts.registerAsync(args);
}
2016-08-06 06:05:04 +00:00
});
}
2016-08-08 19:17:09 +00:00
2016-08-08 22:11:25 +00:00
// Accounts
2016-08-06 06:05:04 +00:00
, checkAsync: function (args) {
2016-08-08 19:17:09 +00:00
var requiredArgs = ['accountId', 'email', 'domains', 'domain'];
2016-08-08 22:11:25 +00:00
if (!requiredArgs.some(function (key) { return -1 !== Object.keys(args).indexOf(key); })) {
2016-08-08 19:17:09 +00:00
return PromiseA.reject(new Error(
"In order to register or retrieve an account one of '" + requiredArgs.join("', '") + "' must be present"
));
}
2015-12-20 02:01:31 +00:00
2016-08-08 19:17:09 +00:00
var copy = utils.merge(args, le);
args = utils.tplCopy(copy);
2015-12-20 03:31:05 +00:00
2016-08-08 19:17:09 +00:00
return le.store.accounts.checkAsync(args).then(function (account) {
2015-12-20 05:13:41 +00:00
2016-08-08 19:17:09 +00:00
if (!account) {
return null;
}
2015-12-20 05:13:41 +00:00
2016-08-08 19:17:09 +00:00
args.account = account;
args.accountId = account.id;
2016-08-05 08:14:40 +00:00
2016-08-08 19:17:09 +00:00
return account;
2016-08-06 06:05:04 +00:00
});
2016-08-04 22:49:35 +00:00
}
2016-08-04 18:26:49 +00:00
}
2016-08-06 06:05:04 +00:00
, certificates: {
2016-08-08 22:11:25 +00:00
// Certificates
2016-08-06 06:05:04 +00:00
registerAsync: function (args) {
2016-08-07 06:02:02 +00:00
var err;
2016-09-14 00:27:24 +00:00
var challengeDefaults = le['_challengeOpts_' + (args.challengeType || le.challengeType)] || {};
var copy = utils.merge(args, challengeDefaults || {});
copy = utils.merge(copy, le);
2016-08-07 06:02:02 +00:00
args = utils.tplCopy(copy);
2016-08-05 08:14:40 +00:00
2016-08-07 06:02:02 +00:00
if (!Array.isArray(args.domains)) {
return PromiseA.reject(new Error('args.domains should be an array of domains'));
}
if (!(args.domains.length && args.domains.every(utils.isValidDomain))) {
// NOTE: this library can't assume to handle the http loopback
// (or dns-01 validation may be used)
// so we do not check dns records or attempt a loopback here
err = new Error("invalid domain name(s): '" + args.domains + "'");
err.code = "INVALID_DOMAIN";
return PromiseA.reject(err);
}
2016-08-04 18:26:49 +00:00
// If a previous request to (re)register a certificate is already underway we need
// to return the same promise created before rather than registering things twice.
// I'm not 100% sure how to properly handle the case where someone registers domain
// lists with some but not all elements common, nor am I sure that's even a case that
// is allowed to happen anyway. But for now we act like the list is completely the
// same if any elements are the same.
var promise;
args.domains.some(function (name) {
if (pendingRegistrations.hasOwnProperty(name)) {
promise = pendingRegistrations[name];
return true;
}
});
if (promise) {
return promise;
}
promise = core.certificates._runRegistration(args);
// Now that the registration is actually underway we need to make sure any subsequent
// registration attempts return the same promise until it is completed (but not after
// it is completed).
args.domains.forEach(function (name) {
pendingRegistrations[name] = promise;
});
function clearPending() {
args.domains.forEach(function (name) {
delete pendingRegistrations[name];
});
}
promise.then(clearPending, clearPending);
return promise;
}
, _runRegistration: function (args) {
2016-08-09 19:02:10 +00:00
// TODO renewal cb
// accountId and or email
return core.accounts.getAsync(args).then(function (account) {
args.account = account;
2016-08-07 06:02:02 +00:00
var promise = le.store.certificates.checkKeypairAsync(args).then(function (keypair) {
2016-08-09 19:02:10 +00:00
if (keypair) {
return RSA.import(keypair);
}
2016-08-08 23:43:31 +00:00
if (args.domainKeypair) {
return le.store.certificates.setKeypairAsync(args, RSA.import(args.domainKeypair));
}
var keypairOpts = { public: true, pem: true };
2016-08-07 06:02:02 +00:00
return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) {
keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
2016-08-08 23:14:53 +00:00
keypair.publicKeyPem = RSA.exportPublicPem(keypair);
2016-08-07 06:02:02 +00:00
keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair);
return le.store.certificates.setKeypairAsync(args, keypair);
});
2016-08-06 06:05:04 +00:00
});
2016-08-04 18:26:49 +00:00
2016-08-07 06:02:02 +00:00
return promise.then(function (domainKeypair) {
args.domainKeypair = domainKeypair;
//args.registration = domainKey;
2016-08-08 19:17:09 +00:00
// Note: the ACME urls are always fetched fresh on purpose
// TODO is this the right place for this?
return core.getAcmeUrlsAsync(args).then(function (urls) {
args._acmeUrls = urls;
2016-08-09 19:02:10 +00:00
var certReq = {
2016-08-08 19:17:09 +00:00
debug: args.debug || le.debug
, newAuthzUrl: args._acmeUrls.newAuthz
, newCertUrl: args._acmeUrls.newCert
, accountKeypair: RSA.import(account.keypair)
, domainKeypair: domainKeypair
, domains: args.domains
, challengeType: args.challengeType
2016-08-09 19:02:10 +00:00
};
//
// 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)
//
// Each of these fires individually for each domain,
// even though the certificate on the whole may have many domains
//
certReq.setChallenge = function (domain, key, value, done) {
log(args.debug, "setChallenge called for '" + domain + "'");
2016-09-15 06:40:57 +00:00
var copy = utils.merge({ domains: [domain] }, args);
copy = utils.merge(copy, le);
2016-08-09 19:02:10 +00:00
utils.tplCopy(copy);
// TODO need to save challengeType
le.challenges[args.challengeType].set(copy, domain, key, value, done);
2016-08-09 19:02:10 +00:00
};
certReq.removeChallenge = function (domain, key, done) {
2016-09-15 06:40:57 +00:00
log(args.debug, "removeChallenge called for '" + domain + "'");
2016-08-09 19:02:10 +00:00
var copy = utils.merge({ domains: [domain] }, le);
utils.tplCopy(copy);
le.challenges[args.challengeType].remove(copy, domain, key, done);
2016-08-09 19:02:10 +00:00
};
log(args.debug, 'calling le.acme.getCertificateAsync', certReq.domains);
2016-08-09 19:10:44 +00:00
2016-08-09 19:02:10 +00:00
return le.acme.getCertificateAsync(certReq).then(utils.attachCertInfo);
2016-08-08 19:17:09 +00:00
});
2016-08-07 06:02:02 +00:00
}).then(function (results) {
2016-08-13 20:35:19 +00:00
// { cert, chain, privkey /*TODO, subject, altnames, issuedAt, expiresAt */ }
2016-08-06 06:05:04 +00:00
2016-08-13 20:35:19 +00:00
args.certs = results;
// args.pems is deprecated
2016-08-07 06:02:02 +00:00
args.pems = results;
return le.store.certificates.setAsync(args).then(function () {
return results;
});
});
2016-08-06 06:05:04 +00:00
});
2016-08-04 22:49:35 +00:00
}
2016-08-09 00:26:46 +00:00
// Certificates
, renewAsync: function (args, certs) {
var renewableAt = core.certificates._getRenewableAt(args, certs);
2016-08-09 19:51:42 +00:00
var err;
//var halfLife = (certs.expiresAt - certs.issuedAt) / 2;
//var renewable = (Date.now() - certs.issuedAt) > halfLife;
log(args.debug, "(Renew) Expires At", new Date(certs.expiresAt).toISOString());
log(args.debug, "(Renew) Renewable At", new Date(renewableAt).toISOString());
if (!args.duplicate && Date.now() < renewableAt) {
2016-08-09 19:51:42 +00:00
err = new Error(
"[ERROR] Certificate issued at '"
+ new Date(certs.issuedAt).toISOString() + "' and expires at '"
+ new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until '"
+ new Date(renewableAt).toISOString() + "'. Set { duplicate: true } to force."
2016-08-09 19:51:42 +00:00
);
err.code = 'E_NOT_RENEWABLE';
return PromiseA.reject(err);
}
// Either the cert has entered its renewal period
// or we're forcing a refresh via 'dupliate: true'
log(args.debug, "Renewing!");
// TODO fetch email address / accountId (accountBydomain) if not present
2016-08-09 19:02:10 +00:00
// store.config.getAsync(args.domains).then(function (config) { /*...*/ });
if (!args.domains || (args.domains.length || 0) <= 2) {
// this is a renewal, therefore we should renewal ALL of the domains
// associated with this certificate, unless args.domains is a list larger
// than example.com,www.example.com
// TODO check www. prefix
args.domains = certs.altnames;
if (Array.isArray(certs.domains) && certs.domains.length) {
args.domains = certs.domains;
}
}
2016-08-08 19:17:09 +00:00
return core.certificates.registerAsync(args);
}
2016-08-09 00:26:46 +00:00
// Certificates
, _isRenewable: function (args, certs) {
var renewableAt = core.certificates._getRenewableAt(args, certs);
log(args.debug, "Check Expires At", new Date(certs.expiresAt).toISOString());
log(args.debug, "Check Renewable At", new Date(renewableAt).toISOString());
if (args.duplicate || Date.now() >= renewableAt) {
log(args.debug, "certificates are renewable");
return true;
}
return false;
}
, _getRenewableAt: function (args, certs) {
2016-08-09 19:51:42 +00:00
return certs.expiresAt - (args.renewWithin || le.renewWithin);
}
2016-08-06 06:05:04 +00:00
, checkAsync: function (args) {
var copy = utils.merge(args, le);
utils.tplCopy(copy);
2015-12-15 15:21:27 +00:00
2016-08-07 06:02:02 +00:00
// returns pems
2016-08-09 00:26:46 +00:00
return le.store.certificates.checkAsync(copy).then(function (cert) {
if (cert) {
log(args.debug, 'checkAsync found existing certificates');
2016-08-09 00:26:46 +00:00
return utils.attachCertInfo(cert);
}
log(args.debug, 'checkAsync failed to find certificates');
2016-08-09 00:26:46 +00:00
return null;
});
2016-08-04 22:49:35 +00:00
}
2016-08-09 00:26:46 +00:00
// Certificates
2016-08-06 06:05:04 +00:00
, getAsync: function (args) {
var copy = utils.merge(args, le);
2016-08-07 06:02:02 +00:00
args = utils.tplCopy(copy);
2016-08-06 06:05:04 +00:00
return core.certificates.checkAsync(args).then(function (certs) {
2016-08-08 19:17:09 +00:00
if (!certs) {
// There is no cert available
2018-05-10 08:31:24 +00:00
if (args.communityMember && !args._communityMemberAdded) {
try {
require('./community').add(args._communityPackage + ' reg', args.email, args.domains);
} catch(e) { /* ignore */ }
args._communityMemberAdded = true;
}
2016-08-08 19:17:09 +00:00
return core.certificates.registerAsync(args);
}
if (core.certificates._isRenewable(args, certs)) {
// it's time to renew the available cert
2018-05-10 08:31:24 +00:00
if (args.communityMember && !args._communityMemberAdded) {
try {
require('./community').add(args._communityPackage + ' renew', args.email, args.domains);
} catch(e) { /* ignore */ }
args._communityMemberAdded = true;
}
certs.renewing = core.certificates.renewAsync(args, certs);
if (args.waitForRenewal) {
return certs.renewing;
}
2016-08-06 06:05:04 +00:00
}
2016-08-04 18:26:49 +00:00
// return existing unexpired (although potentially stale) certificates when available
// there will be an additional .renewing property if the certs are being asynchronously renewed
return certs;
2016-08-07 06:02:02 +00:00
}).then(function (results) {
// returns pems
return results;
2016-08-06 06:05:04 +00:00
});
}
}
2015-12-15 15:21:27 +00:00
2015-12-15 11:37:39 +00:00
};
2016-08-06 06:05:04 +00:00
return core;
2015-12-15 11:37:39 +00:00
};