🔐 Free SSL, Free Wildcard SSL, and Fully Automated HTTPS for node.js, issued by Let's Encrypt v2 via ACME. Issues and PRs on Github.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

429 lines
16 KiB

'use strict';
function _log(debug) {
if (debug) {
var args = Array.prototype.slice.call(arguments);
args.shift();
args.unshift("[greenlock/lib/core.js]");
console.log.apply(console, args);
}
}
module.exports.create = function (gl) {
var PromiseA = require('bluebird');
var utils = require('./utils');
var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA);
var log = gl.log || _log; // allow custom log
var pendingRegistrations = {};
var core = {
//
// Helpers
//
getAcmeUrlsAsync: function (args) {
var now = Date.now();
// TODO check response header on request for cache time
if ((now - gl._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) {
return PromiseA.resolve(gl._ipc.acmeUrls);
}
return gl.acme.getAcmeUrlsAsync(args.server).then(function (data) {
gl._ipc.acmeUrlsUpdatedAt = Date.now();
gl._ipc.acmeUrls = data;
return gl._ipc.acmeUrls;
});
}
//
// The Main Enchilada
//
//
// Accounts
//
, accounts: {
// Accounts
registerAsync: function (args) {
var err;
var copy = utils.merge(args, gl);
var disagreeTos;
args = utils.tplCopy(copy);
disagreeTos = (!args.agreeTos && 'undefined' !== typeof args.agreeTos);
if (!args.email || disagreeTos || (parseInt(args.rsaKeySize, 10) < 2048)) {
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 () {
var keypairOpts = { public: true, pem: true };
var promise = gl.store.accounts.checkKeypairAsync(args).then(function (keypair) {
if (keypair) {
return RSA.import(keypair);
}
if (args.accountKeypair) {
return gl.store.accounts.setKeypairAsync(args, RSA.import(args.accountKeypair));
}
return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) {
keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
keypair.publicKeyPem = RSA.exportPublicPem(keypair);
keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair);
return gl.store.accounts.setKeypairAsync(args, keypair);
});
});
return promise.then(function (keypair) {
// 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 gl.acme.registerNewAccountAsync({
email: args.email
, newRegUrl: args._acmeUrls.newReg
, newAuthzUrl: args._acmeUrls.newAuthz
, agreeToTerms: function (tosUrl, agreeCb) {
if (true === args.agreeTos || tosUrl === args.agreeTos || tosUrl === gl.agreeToTerms) {
agreeCb(null, tosUrl);
return;
}
// args.email = email; // already there
// args.domains = domains // already there
args.tosUrl = tosUrl;
gl.agreeToTerms(args, agreeCb);
}
, accountKeypair: keypair
, debug: gl.debug || args.debug
}).then(function (receipt) {
var reg = {
keypair: keypair
, receipt: receipt
, email: args.email
, newRegUrl: args._acmeUrls.newReg
, newAuthzUrl: args._acmeUrls.newAuthz
};
// TODO move templating of arguments to right here?
return gl.store.accounts.setAsync(args, reg).then(function (account) {
// should now have account.id and account.accountId
args.account = account;
args.accountId = account.id;
return account;
});
});
});
});
});
}
// Accounts
, getAsync: function (args) {
return core.accounts.checkAsync(args).then(function (account) {
if (account) {
return account;
} else {
return core.accounts.registerAsync(args);
}
});
}
// Accounts
, checkAsync: function (args) {
var requiredArgs = ['accountId', 'email', 'domains', 'domain'];
if (!requiredArgs.some(function (key) { return -1 !== Object.keys(args).indexOf(key); })) {
return PromiseA.reject(new Error(
"In order to register or retrieve an account one of '" + requiredArgs.join("', '") + "' must be present"
));
}
var copy = utils.merge(args, gl);
args = utils.tplCopy(copy);
return gl.store.accounts.checkAsync(args).then(function (account) {
if (!account) {
return null;
}
args.account = account;
args.accountId = account.id;
return account;
});
}
}
, certificates: {
// Certificates
registerAsync: function (args) {
var err;
var challengeDefaults = gl['_challengeOpts_' + (args.challengeType || gl.challengeType)] || {};
var copy = utils.merge(args, challengeDefaults || {});
copy = utils.merge(copy, gl);
args = utils.tplCopy(copy);
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);
}
// 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) {
// TODO renewal cb
// accountId and or email
return core.accounts.getAsync(args).then(function (account) {
args.account = account;
var promise = gl.store.certificates.checkKeypairAsync(args).then(function (keypair) {
if (keypair) {
return RSA.import(keypair);
}
if (args.domainKeypair) {
return gl.store.certificates.setKeypairAsync(args, RSA.import(args.domainKeypair));
}
var keypairOpts = { public: true, pem: true };
return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) {
keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
keypair.publicKeyPem = RSA.exportPublicPem(keypair);
keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair);
return gl.store.certificates.setKeypairAsync(args, keypair);
});
});
return promise.then(function (domainKeypair) {
args.domainKeypair = domainKeypair;
//args.registration = domainKey;
// 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;
var certReq = {
debug: args.debug || gl.debug
, newAuthzUrl: args._acmeUrls.newAuthz
, newCertUrl: args._acmeUrls.newCert
, accountKeypair: RSA.import(account.keypair)
, domainKeypair: domainKeypair
, domains: args.domains
, challengeType: args.challengeType
};
//
// 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 + "'");
var copy = utils.merge({ domains: [domain] }, args);
copy = utils.merge(copy, gl);
utils.tplCopy(copy);
// TODO need to save challengeType
gl.challenges[args.challengeType].set(copy, domain, key, value, done);
};
certReq.removeChallenge = function (domain, key, done) {
log(args.debug, "removeChallenge called for '" + domain + "'");
var copy = utils.merge({ domains: [domain] }, gl);
utils.tplCopy(copy);
gl.challenges[args.challengeType].remove(copy, domain, key, done);
};
log(args.debug, 'calling greenlock.acme.getCertificateAsync', certReq.domains);
return gl.acme.getCertificateAsync(certReq).then(utils.attachCertInfo);
});
}).then(function (results) {
// { cert, chain, privkey /*TODO, subject, altnames, issuedAt, expiresAt */ }
args.certs = results;
// args.pems is deprecated
args.pems = results;
return gl.store.certificates.setAsync(args).then(function () {
return results;
});
});
});
}
// Certificates
, renewAsync: function (args, certs) {
var renewableAt = core.certificates._getRenewableAt(args, certs);
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) {
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."
);
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
// 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;
}
}
return core.certificates.registerAsync(args);
}
// 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) {
return certs.expiresAt - (args.renewWithin || gl.renewWithin);
}
, checkAsync: function (args) {
var copy = utils.merge(args, gl);
utils.tplCopy(copy);
// returns pems
return gl.store.certificates.checkAsync(copy).then(function (cert) {
if (cert) {
log(args.debug, 'checkAsync found existing certificates');
return utils.attachCertInfo(cert);
}
log(args.debug, 'checkAsync failed to find certificates');
return null;
});
}
// Certificates
, getAsync: function (args) {
var copy = utils.merge(args, gl);
args = utils.tplCopy(copy);
return core.certificates.checkAsync(args).then(function (certs) {
if (!certs) {
// There is no cert available
if (args.communityMember && !args._communityMemberAdded) {
try {
require('./community').add(args._communityPackage + ' reg', args.email, args.domains);
} catch(e) { /* ignore */ }
args._communityMemberAdded = true;
}
return core.certificates.registerAsync(args);
}
if (core.certificates._isRenewable(args, certs)) {
// it's time to renew the available cert
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;
}
}
// 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;
}).then(function (results) {
// returns pems
return results;
});
}
}
};
return core;
};