'use strict'; var C = module.exports; var U = require('./utils.js'); var CSR = require('@root/csr'); var Enc = require('@root/encoding'); var Keypairs = require('@root/keypairs'); var pending = {}; var rawPending = {}; // What the abbreviations mean // // gnlkc => greenlock // mconf => manager config // db => greenlock store instance // acme => instance of ACME.js // chs => instances of challenges // acc => account // args => site / extra options // Certificates C._getOrOrder = function(gnlck, mconf, db, acme, chs, acc, args) { var email = args.subscriberEmail || mconf.subscriberEmail; var id = args.altnames .slice(0) .sort() .join(' '); if (pending[id]) { return pending[id]; } pending[id] = C._rawGetOrOrder( gnlck, mconf, db, acme, chs, acc, email, args ) .then(function(pems) { delete pending[id]; return pems; }) .catch(function(err) { delete pending[id]; throw err; }); return pending[id]; }; // Certificates C._rawGetOrOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) { return C._check(gnlck, mconf, db, args).then(function(pems) { // Nice and fresh? We're done! if (pems) { if (!C._isStale(gnlck, mconf, args, pems)) { // return existing unexpired (although potentially stale) certificates when available // there will be an additional .renewing property if the certs are being asynchronously renewed //pems._type = 'current'; return pems; } } // We're either starting fresh or freshening up... var p = C._rawOrder(gnlck, mconf, db, acme, chs, acc, email, args); var evname = pems ? 'cert_renewal' : 'cert_issue'; p.then(function(newPems) { // notify in the background var renewAt = C._renewWithStagger(gnlck, mconf, args, newPems); gnlck._notify(evname, { renewAt: renewAt, subject: args.subject, altnames: args.altnames }); gnlck._notify('_cert_issue', { renewAt: renewAt, subject: args.subject, altnames: args.altnames, pems: newPems }); }).catch(function(err) { if (!err.context) { err.context = evname; } err.subject = args.subject; err.altnames = args.altnames; gnlck._notify('error', err); }); // No choice but to hang tight and wait for it if ( !pems || pems.renewAt < Date.now() - 24 * 60 * 60 * 1000 || pems.expiresAt <= Date.now() + 24 * 60 * 60 * 1000 ) { return p; } // Wait it out // TODO should we call this waitForRenewal? if (args.waitForRenewal) { return p; } // Let the certs renew in the background return pems; }); }; // we have another promise here because it the optional renewal // may resolve in a different stack than the returned pems C._rawOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) { var id = args.altnames .slice(0) .sort() .join(' '); if (rawPending[id]) { return rawPending[id]; } var keyType = args.serverKeyType || mconf.serverKeyType; var query = { subject: args.subject, certificate: args.certificate || {}, directoryUrl: args.directoryUrl || mconf.directoryUrl || gnlck._defaults.directoryUrl }; rawPending[id] = U._getOrCreateKeypair(db, args.subject, query, keyType) .then(function(kresult) { var serverKeypair = kresult.keypair; var domains = args.altnames.slice(0); return CSR.csr({ jwk: serverKeypair.privateKeyJwk || serverKeypair.private, domains: domains, encoding: 'der' }) .then(function(csrDer) { // TODO let CSR support 'urlBase64' ? return Enc.bufToUrlBase64(csrDer); }) .then(function(csr) { function notify(ev, opts) { gnlck._notify(ev, opts); } var certReq = { debug: args.debug || gnlck._defaults.debug, challenges: chs, account: acc, // only used if accounts.key.kid exists accountKey: acc.keypair.privateKeyJwk || acc.keypair.private, keypair: acc.keypair, // TODO csr: csr, domains: domains, // because ACME.js v3 uses `domains` still, actually onChallengeStatus: notify, notify: notify // TODO // TODO handle this in acme-v2 //subject: args.subject, //altnames: args.altnames.slice(0), }; return acme.certificates .create(certReq) .then(U._attachCertInfo); }) .then(function(pems) { if (kresult.exists) { return pems; } query.keypair = serverKeypair; return db.setKeypair(query, serverKeypair).then(function() { return pems; }); }); }) .then(function(pems) { // TODO put this in the docs // { cert, chain, privkey, subject, altnames, issuedAt, expiresAt } // Note: the query has been updated query.pems = pems; return db.set(query); }) .then(function() { return C._check(gnlck, mconf, db, args); }) .then(function(bundle) { // TODO notify Manager delete rawPending[id]; return bundle; }) .catch(function(err) { // Todo notify manager delete rawPending[id]; throw err; }); return rawPending[id]; }; // returns pems, if they exist C._check = function(gnlck, mconf, db, args) { var query = { subject: args.subject, // may contain certificate.id certificate: args.certificate, directoryUrl: args.directoryUrl || mconf.directoryUrl || gnlck._defaults.directoryUrl }; return db.check(query).then(function(pems) { if (!pems) { return null; } pems = U._attachCertInfo(pems); // For eager management if (args.subject && !U._certHasDomain(pems, args.subject)) { // TODO report error, but continue the process as with no cert return null; } // For lazy SNI requests if (args.domain && !U._certHasDomain(pems, args.domain)) { // TODO report error, but continue the process as with no cert return null; } return U._getKeypair(db, args.subject, query) .then(function(keypair) { return Keypairs.export({ jwk: keypair.privateKeyJwk || keypair.private, encoding: 'pem' }).then(function(pem) { pems.privkey = pem; return pems; }); }) .catch(function() { // TODO report error, but continue the process as with no cert return null; }); }); }; // Certificates C._isStale = function(gnlck, mconf, args, pems) { if (args.duplicate) { return true; } var renewAt = C._renewableAt(gnlck, mconf, args, pems); if (Date.now() >= renewAt) { return true; } return false; }; C._renewWithStagger = function(gnlck, mconf, args, pems) { var renewOffset = C._renewOffset(gnlck, mconf, args, pems); var renewStagger; try { renewStagger = U._parseDuration( args.renewStagger || mconf.renewStagger || 0 ); } catch (e) { renewStagger = U._parseDuration( args.renewStagger || mconf.renewStagger ); } // TODO check this beforehand if (!args.force && renewStagger / renewOffset >= 0.5) { renewStagger = renewOffset * 0.1; } if (renewOffset > 0) { // stagger forward, away from issued at return Math.round( pems.issuedAt + renewOffset + Math.random() * renewStagger ); } // stagger backward, toward issued at return Math.round( pems.expiresAt + renewOffset - Math.random() * renewStagger ); }; C._renewOffset = function(gnlck, mconf, args /*, pems*/) { var renewOffset = U._parseDuration( args.renewOffset || mconf.renewOffset || 0 ); var week = 1000 * 60 * 60 * 24 * 6; if (!args.force && Math.abs(renewOffset) < week) { throw new Error( 'developer error: `renewOffset` should always be at least a week, use `force` to not safety-check renewOffset' ); } return renewOffset; }; C._renewableAt = function(gnlck, mconf, args, pems) { if (args.renewAt) { return args.renewAt; } var renewOffset = C._renewOffset(gnlck, mconf, args, pems); if (renewOffset > 0) { return pems.issuedAt + renewOffset; } return pems.expiresAt + renewOffset; };