🔐 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.
 
 

279 lines
6.0 KiB

'use strict';
var C = module.exports;
var U = require('./utils.js');
var CSR = require('@root/csr');
var Enc = require('@root/encoding');
var pending = {};
var rawPending = {};
// Certificates
C._getOrOrder = function(greenlock, db, acme, challenges, account, args) {
var email = args.subscriberEmail || greenlock._defaults.subscriberEmail;
var id = args.altnames.join(' ');
if (pending[id]) {
return pending[id];
}
pending[id] = C._rawGetOrOrder(
greenlock,
db,
acme,
challenges,
account,
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(
greenlock,
db,
acme,
challenges,
account,
email,
args
) {
return C._check(db, args).then(function(pems) {
// No pems? get some!
if (!pems) {
return C._rawOrder(
greenlock,
db,
acme,
challenges,
account,
email,
args
).then(function(newPems) {
// do not wait on notify
greenlock.notify('cert_issue', {
options: args,
subject: args.subject,
altnames: args.altnames,
account: account,
email: email,
pems: newPems
});
return newPems;
});
}
// Nice and fresh? We're done!
if (!C._isStale(greenlock, 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;
}
// Getting stale? Let's renew to freshen up!
var p = C._rawOrder(
greenlock,
db,
acme,
challenges,
account,
email,
args
).then(function(renewedPems) {
// do not wait on notify
greenlock.notify('cert_renewal', {
options: args,
subject: args.subject,
altnames: args.altnames,
account: account,
email: email,
pems: renewedPems
});
return renewedPems;
});
// TODO what should this be?
if (args.waitForRenewal) {
return p;
}
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(greenlock, db, acme, challenges, account, email, args) {
var id = args.altnames
.slice(0)
.sort()
.join(' ');
if (rawPending[id]) {
return rawPending[id];
}
var keyType = args.serverKeyType || greenlock._defaults.serverKeyType;
var query = {
subject: args.subject,
certificate: args.certificate || {}
};
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,
domains: domains,
encoding: 'der'
})
.then(function(csrDer) {
// TODO let CSR support 'urlBase64' ?
return Enc.bufToUrlBase64(csrDer);
})
.then(function(csr) {
function notify() {
greenlock.notify('challenge_status', {
options: args,
subject: args.subject,
altnames: args.altnames,
account: account,
email: email
});
}
var certReq = {
debug: args.debug || greenlock._defaults.debug,
challenges: challenges,
account: account, // only used if accounts.key.kid exists
accountKeypair: account.keypair,
keypair: account.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;
}
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(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(db, args) {
var query = {
subject: args.subject,
// may contain certificate.id
certificate: args.certificate
};
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) {
pems.privkey = keypair.privateKeyPem;
return pems;
})
.catch(function() {
// TODO report error, but continue the process as with no cert
return null;
});
});
};
// Certificates
C._isStale = function(greenlock, args, pems) {
if (args.duplicate) {
return true;
}
var renewAt = C._renewableAt(greenlock, args, pems);
if (Date.now() >= renewAt) {
return true;
}
return false;
};
C._renewableAt = function(greenlock, args, pems) {
if (args.renewAt) {
return args.renewAt;
}
var renewOffset = args.renewOffset || greenlock._defaults.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'
);
}
if (renewOffset > 0) {
return pems.issuedAt + renewOffset;
}
return pems.expiresAt + renewOffset;
};