959 lines
26 KiB
JavaScript
959 lines
26 KiB
JavaScript
'use strict';
|
|
/*global Promise*/
|
|
require('./compat.js');
|
|
|
|
var util = require('util');
|
|
function promisifyAll(obj) {
|
|
var aobj = {};
|
|
Object.keys(obj).forEach(function(key) {
|
|
if ('function' === typeof obj[key]) {
|
|
aobj[key] = obj[key];
|
|
aobj[key + 'Async'] = util.promisify(obj[key]);
|
|
}
|
|
});
|
|
return aobj;
|
|
}
|
|
|
|
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 utils = require('./utils');
|
|
var RSA = 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 Promise.resolve(gl._ipc.acmeUrls);
|
|
}
|
|
|
|
// TODO acme-v2/nocompat
|
|
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);
|
|
if (!args.account) {
|
|
args.account = {};
|
|
}
|
|
if ('object' === typeof args.account && !args.account.id) {
|
|
args.account.id = args.accountId || args.email || '';
|
|
}
|
|
|
|
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 Promise.reject(err);
|
|
}
|
|
|
|
return utils.testEmail(args.email).then(function() {
|
|
if (
|
|
args.account &&
|
|
args.account.privkey &&
|
|
(args.account.privkey.jwk || args.account.privkey.pem)
|
|
) {
|
|
// TODO import jwk or pem and return it here
|
|
console.warn(
|
|
'TODO: implement accounts.checkKeypairAsync skipping'
|
|
);
|
|
}
|
|
var accountKeypair;
|
|
var newAccountKeypair = true;
|
|
var promise = gl.store.accounts
|
|
.checkKeypairAsync(args)
|
|
.then(function(keypair) {
|
|
if (keypair) {
|
|
// TODO keypairs
|
|
newAccountKeypair = false;
|
|
accountKeypair = RSA.import(keypair);
|
|
return;
|
|
}
|
|
|
|
if (args.accountKeypair) {
|
|
// TODO keypairs
|
|
accountKeypair = RSA.import(
|
|
args.accountKeypair
|
|
);
|
|
return;
|
|
}
|
|
|
|
var keypairOpts = {
|
|
bitlen: args.rsaKeySize,
|
|
exp: 65537,
|
|
public: true,
|
|
pem: true
|
|
};
|
|
// TODO keypairs
|
|
return (args.generateKeypair ||
|
|
RSA.generateKeypairAsync)(keypairOpts).then(
|
|
function(keypair) {
|
|
keypair.privateKeyPem = RSA.exportPrivatePem(
|
|
keypair
|
|
);
|
|
keypair.publicKeyPem = RSA.exportPublicPem(
|
|
keypair
|
|
);
|
|
keypair.privateKeyJwk = RSA.exportPrivateJwk(
|
|
keypair
|
|
);
|
|
accountKeypair = keypair;
|
|
}
|
|
);
|
|
})
|
|
.then(function() {
|
|
return accountKeypair;
|
|
});
|
|
|
|
return promise.then(function(keypair) {
|
|
// Note: the ACME urls are always fetched fresh on purpose
|
|
// TODO acme-v2/nocompat
|
|
return core.getAcmeUrlsAsync(args).then(function(urls) {
|
|
args._acmeUrls = urls;
|
|
|
|
// TODO acme-v2/nocompat
|
|
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,
|
|
kid:
|
|
receipt &&
|
|
receipt.key &&
|
|
(receipt.key.kid || receipt.kid),
|
|
email: args.email,
|
|
newRegUrl: args._acmeUrls.newReg,
|
|
newAuthzUrl: args._acmeUrls.newAuthz
|
|
};
|
|
|
|
var accountKeypairPromise;
|
|
args.keypair = keypair;
|
|
args.receipt = receipt;
|
|
if (newAccountKeypair) {
|
|
accountKeypairPromise = gl.store.accounts.setKeypairAsync(
|
|
args,
|
|
keypair
|
|
);
|
|
}
|
|
return Promise.resolve(
|
|
accountKeypairPromise
|
|
).then(function() {
|
|
// TODO move templating of arguments to right here?
|
|
if (!gl.store.accounts.setAsync) {
|
|
return Promise.resolve({
|
|
keypair: keypair
|
|
});
|
|
}
|
|
return gl.store.accounts
|
|
.setAsync(args, reg)
|
|
.then(function(account) {
|
|
if (
|
|
account &&
|
|
'object' !== typeof account
|
|
) {
|
|
throw new Error(
|
|
"store.accounts.setAsync should either return 'null' or an object with at least a string 'id'"
|
|
);
|
|
}
|
|
if (!account) {
|
|
account = {};
|
|
}
|
|
account.keypair = keypair;
|
|
return account;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
// Accounts
|
|
// (only used for keypair)
|
|
getAsync: function(args) {
|
|
var accountPromise = null;
|
|
if (gl.store.accounts.checkAsync) {
|
|
accountPromise = core.accounts.checkAsync(args);
|
|
}
|
|
return Promise.resolve(accountPromise).then(function(account) {
|
|
if (!account) {
|
|
return core.accounts.registerAsync(args);
|
|
}
|
|
if (account.keypair) {
|
|
return account;
|
|
}
|
|
|
|
if (!args.account) {
|
|
args.account = {};
|
|
}
|
|
if ('object' === typeof args.account && !args.account.id) {
|
|
args.account.id = args.accountId || args.email || '';
|
|
}
|
|
var copy = utils.merge(args, gl);
|
|
args = utils.tplCopy(copy);
|
|
return gl.store.accounts
|
|
.checkKeypairAsync(args)
|
|
.then(function(keypair) {
|
|
if (keypair) {
|
|
return { keypair: keypair };
|
|
}
|
|
return core.accounts.registerAsync(args);
|
|
});
|
|
});
|
|
},
|
|
|
|
// Accounts
|
|
checkAsync: function(args) {
|
|
var requiredArgs = ['accountId', 'email', 'domains', 'domain'];
|
|
if (
|
|
!(args.account && (args.account.id || args.account.kid)) &&
|
|
!requiredArgs.some(function(key) {
|
|
return -1 !== Object.keys(args).indexOf(key);
|
|
})
|
|
) {
|
|
return Promise.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);
|
|
if (!args.account) {
|
|
args.account = {};
|
|
}
|
|
if ('object' === typeof args.account && !args.account.id) {
|
|
args.account.id = args.accountId || args.email || '';
|
|
}
|
|
|
|
// we can re-register the same account until we're blue in the face and it's all the same
|
|
// of course, we can also skip the lookup if we do store the account, but whatever
|
|
if (!gl.store.accounts.checkAsync) {
|
|
return Promise.resolve(null);
|
|
}
|
|
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);
|
|
if (!copy.subject) {
|
|
copy.subject = copy.domains[0];
|
|
}
|
|
if (!copy.domain) {
|
|
copy.domain = copy.domains[0];
|
|
}
|
|
args = utils.tplCopy(copy);
|
|
|
|
if (!Array.isArray(args.domains)) {
|
|
return Promise.reject(
|
|
new Error('args.domains should be an array of domains')
|
|
);
|
|
}
|
|
//if (-1 === args.domains.indexOf(args.subject)) // TODO relax the constraint once acme-v2 handles subject?
|
|
if (args.subject !== args.domains[0]) {
|
|
console.warn(
|
|
"The certificate's subject (primary domain) should be first in the list of opts.domains"
|
|
);
|
|
console.warn(
|
|
'\topts.subject: (set by you approveDomains(), falling back to opts.domain) ' +
|
|
args.subject
|
|
);
|
|
console.warn(
|
|
'\topts.domain: (set by SNICallback()) ' + args.domain
|
|
);
|
|
console.warn(
|
|
'\topts.domains: (set by you in approveDomains()) ' +
|
|
args.domains.join(',')
|
|
);
|
|
console.warn(
|
|
'Updating your code will prevent weird, random, hard-to-repro bugs during renewals'
|
|
);
|
|
console.warn(
|
|
'(also this will be required in the next major version of greenlock)'
|
|
);
|
|
//return Promise.reject(new Error('certificate subject (primary domain) must be the first in opts.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.subject +
|
|
') ' +
|
|
args.domains.join(',') +
|
|
"'"
|
|
);
|
|
err.code = 'INVALID_DOMAIN';
|
|
return Promise.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;
|
|
|
|
if (
|
|
args.certificate &&
|
|
args.certificate.privkey &&
|
|
(args.certificate.privkey.jwk ||
|
|
args.certificate.privkey.pem)
|
|
) {
|
|
// TODO import jwk or pem and return it here
|
|
console.warn(
|
|
'TODO: implement certificates.checkKeypairAsync skipping'
|
|
);
|
|
}
|
|
var domainKeypair;
|
|
var newDomainKeypair = true;
|
|
// This has been done in the getAsync already, so we skip it here
|
|
// if approveDomains doesn't set subject, we set it here
|
|
//args.subject = args.subject || args.domains[0];
|
|
var promise = gl.store.certificates
|
|
.checkKeypairAsync(args)
|
|
.then(function(keypair) {
|
|
if (keypair) {
|
|
domainKeypair = RSA.import(keypair);
|
|
newDomainKeypair = false;
|
|
return;
|
|
}
|
|
|
|
if (args.domainKeypair) {
|
|
domainKeypair = RSA.import(args.domainKeypair);
|
|
return;
|
|
}
|
|
|
|
var keypairOpts = {
|
|
bitlen: args.rsaKeySize,
|
|
exp: 65537,
|
|
public: true,
|
|
pem: true
|
|
};
|
|
return (args.generateKeypair ||
|
|
RSA.generateKeypairAsync)(keypairOpts).then(
|
|
function(keypair) {
|
|
keypair.privateKeyPem = RSA.exportPrivatePem(
|
|
keypair
|
|
);
|
|
keypair.publicKeyPem = RSA.exportPublicPem(
|
|
keypair
|
|
);
|
|
keypair.privateKeyJwk = RSA.exportPrivateJwk(
|
|
keypair
|
|
);
|
|
domainKeypair = keypair;
|
|
}
|
|
);
|
|
})
|
|
.then(function() {
|
|
return domainKeypair;
|
|
});
|
|
|
|
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,
|
|
subject: args.subject, // TODO handle this in acme-v2
|
|
domains: args.domains,
|
|
challengeTypes: Object.keys(
|
|
args.challenges
|
|
)
|
|
};
|
|
|
|
//
|
|
// 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(
|
|
challenge,
|
|
done
|
|
) {
|
|
log(
|
|
args.debug,
|
|
"setChallenge called for '" +
|
|
challenge.altname +
|
|
"'"
|
|
);
|
|
// NOTE: First arg takes precedence
|
|
var copy = utils.merge(
|
|
{ domains: [challenge.altname] },
|
|
args
|
|
);
|
|
copy = utils.merge(copy, gl);
|
|
utils.tplCopy(copy);
|
|
copy.challenge = challenge;
|
|
|
|
if (
|
|
1 ===
|
|
copy.challenges[challenge.type].set
|
|
.length
|
|
) {
|
|
copy.challenges[challenge.type]
|
|
.set(copy)
|
|
.then(function(result) {
|
|
done(null, result);
|
|
})
|
|
.catch(done);
|
|
} else if (
|
|
2 ===
|
|
copy.challenges[challenge.type].set
|
|
.length
|
|
) {
|
|
copy.challenges[challenge.type].set(
|
|
copy,
|
|
done
|
|
);
|
|
} else {
|
|
Object.keys(challenge).forEach(
|
|
function(key) {
|
|
done[key] = challenge[key];
|
|
}
|
|
);
|
|
// regression bugfix for le-challenge-cloudflare
|
|
// (_acme-challege => _greenlock-dryrun-XXXX)
|
|
copy.acmePrefix =
|
|
(
|
|
challenge.dnsHost || ''
|
|
).replace(/\.*/, '') ||
|
|
copy.acmePrefix;
|
|
copy.challenges[challenge.type].set(
|
|
copy,
|
|
challenge.altname,
|
|
challenge.token,
|
|
challenge.keyAuthorization,
|
|
done
|
|
);
|
|
}
|
|
};
|
|
certReq.removeChallenge = function(
|
|
challenge,
|
|
done
|
|
) {
|
|
log(
|
|
args.debug,
|
|
"removeChallenge called for '" +
|
|
challenge.altname +
|
|
"'"
|
|
);
|
|
var copy = utils.merge(
|
|
{ domains: [challenge.altname] },
|
|
args
|
|
);
|
|
copy = utils.merge(copy, gl);
|
|
utils.tplCopy(copy);
|
|
copy.challenge = challenge;
|
|
|
|
if (
|
|
1 ===
|
|
copy.challenges[challenge.type]
|
|
.remove.length
|
|
) {
|
|
copy.challenges[challenge.type]
|
|
.remove(copy)
|
|
.then(function(result) {
|
|
done(null, result);
|
|
})
|
|
.catch(done);
|
|
} else if (
|
|
2 ===
|
|
copy.challenges[challenge.type]
|
|
.remove.length
|
|
) {
|
|
copy.challenges[
|
|
challenge.type
|
|
].remove(copy, done);
|
|
} else {
|
|
Object.keys(challenge).forEach(
|
|
function(key) {
|
|
done[key] = challenge[key];
|
|
}
|
|
);
|
|
copy.challenges[
|
|
challenge.type
|
|
].remove(
|
|
copy,
|
|
challenge.altname,
|
|
challenge.token,
|
|
done
|
|
);
|
|
}
|
|
};
|
|
certReq.init = function(deps) {
|
|
var copy = utils.merge(deps, args);
|
|
copy = utils.merge(copy, gl);
|
|
utils.tplCopy(copy);
|
|
|
|
Object.keys(copy.challenges).forEach(
|
|
function(key) {
|
|
if (
|
|
'function' ===
|
|
typeof copy.challenges[key]
|
|
.init
|
|
) {
|
|
copy.challenges[key].init(
|
|
copy
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
return null;
|
|
};
|
|
certReq.getZones = function(challenge) {
|
|
var copy = utils.merge(
|
|
{
|
|
dnsHosts: args.domains.map(
|
|
function(x) {
|
|
return 'xxxx.' + x;
|
|
}
|
|
)
|
|
},
|
|
args
|
|
);
|
|
copy = utils.merge(copy, gl);
|
|
utils.tplCopy(copy);
|
|
copy.challenge = challenge;
|
|
|
|
if (
|
|
!copy.challenges[challenge.type] ||
|
|
'function' !==
|
|
typeof copy.challenges[
|
|
challenge.type
|
|
].zones
|
|
) {
|
|
// may not be available, that's fine.
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
return copy.challenges[
|
|
challenge.type
|
|
].zones(copy);
|
|
};
|
|
|
|
log(
|
|
args.debug,
|
|
'calling greenlock.acme.getCertificateAsync',
|
|
certReq.subject,
|
|
certReq.domains
|
|
);
|
|
|
|
// TODO acme-v2/nocompat
|
|
return gl.acme
|
|
.getCertificateAsync(certReq)
|
|
.then(utils.attachCertInfo);
|
|
});
|
|
})
|
|
.then(function(results) {
|
|
//var requested = {};
|
|
//var issued = {};
|
|
// { cert, chain, privkey /*TODO, subject, altnames, issuedAt, expiresAt */ }
|
|
|
|
// args.certs.privkey = RSA.exportPrivatePem(options.domainKeypair);
|
|
args.certs = results;
|
|
// args.pems is deprecated
|
|
args.pems = results;
|
|
// This has been done in the getAsync already, so we skip it here
|
|
// if approveDomains doesn't set subject, we set it here
|
|
//args.subject = args.subject || args.domains[0];
|
|
var promise;
|
|
if (newDomainKeypair) {
|
|
args.keypair = domainKeypair;
|
|
promise = gl.store.certificates.setKeypairAsync(
|
|
args,
|
|
domainKeypair
|
|
);
|
|
}
|
|
return Promise.resolve(promise).then(function() {
|
|
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 Promise.reject(err);
|
|
}
|
|
|
|
// Either the cert has entered its renewal period
|
|
// or we're forcing a refresh via 'dupliate: true'
|
|
log(args.debug, 'Renewing!');
|
|
|
|
if (!args.domains || !args.domains.length) {
|
|
args.domains =
|
|
args.servernames ||
|
|
[certs.subject].concat(certs.altnames);
|
|
}
|
|
|
|
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);
|
|
// if approveDomains doesn't set subject, we set it here
|
|
if (!(copy.domains && copy.domains.length)) {
|
|
copy.domains = [copy.subject || copy.domain].filter(
|
|
Boolean
|
|
);
|
|
}
|
|
if (!copy.subject) {
|
|
copy.subject = copy.domains[0];
|
|
}
|
|
if (!copy.domain) {
|
|
copy.domain = copy.domains[0];
|
|
}
|
|
args = utils.tplCopy(copy);
|
|
|
|
// returns pems
|
|
return gl.store.certificates
|
|
.checkAsync(args)
|
|
.then(function(cert) {
|
|
if (!cert) {
|
|
log(
|
|
args.debug,
|
|
'checkAsync failed to find certificates'
|
|
);
|
|
return null;
|
|
}
|
|
|
|
cert = utils.attachCertInfo(cert);
|
|
if (utils.certHasDomain(cert, args.domain)) {
|
|
log(
|
|
args.debug,
|
|
'checkAsync found existing certificates'
|
|
);
|
|
|
|
if (cert.privkey) {
|
|
return cert;
|
|
} else {
|
|
return gl.store.certificates
|
|
.checkKeypairAsync(args)
|
|
.then(function(keypair) {
|
|
cert.privkey =
|
|
keypair.privateKeyPem ||
|
|
RSA.exportPrivatePem(keypair);
|
|
return cert;
|
|
});
|
|
}
|
|
}
|
|
log(
|
|
args.debug,
|
|
'checkAsync found mismatched / incomplete certificates'
|
|
);
|
|
});
|
|
},
|
|
// Certificates
|
|
getAsync: function(args) {
|
|
var copy = utils.merge(args, gl);
|
|
// if approveDomains doesn't set subject, we set it here
|
|
if (!(copy.domains && copy.domains.length)) {
|
|
copy.domains = [copy.subject || copy.domain].filter(
|
|
Boolean
|
|
);
|
|
}
|
|
if (!copy.subject) {
|
|
copy.subject = copy.domains[0];
|
|
}
|
|
if (!copy.domain) {
|
|
copy.domain = copy.domains[0];
|
|
}
|
|
args = utils.tplCopy(copy);
|
|
|
|
if (
|
|
args.certificate &&
|
|
args.certificate.privkey &&
|
|
args.certificate.cert &&
|
|
args.certificate.chain
|
|
) {
|
|
// TODO skip fetching a certificate if it's fetched during approveDomains
|
|
console.warn(
|
|
'TODO: implement certificates.checkAsync skipping'
|
|
);
|
|
}
|
|
return core.certificates
|
|
.checkAsync(args)
|
|
.then(function(certs) {
|
|
if (certs) {
|
|
certs = utils.attachCertInfo(certs);
|
|
}
|
|
if (
|
|
!certs ||
|
|
!utils.certHasDomain(certs, args.domain)
|
|
) {
|
|
// There is no cert available
|
|
if (
|
|
false !== args.securityUpdates &&
|
|
!args._communityMemberAdded
|
|
) {
|
|
// We will notify all greenlock users of mandatory and security updates
|
|
// We'll keep track of versions and os so we can make sure things work well
|
|
// { name, version, email, domains, action, communityMember, telemetry }
|
|
require('./community').add({
|
|
name: args._communityPackage,
|
|
version: args._communityPackageVersion,
|
|
email: args.email,
|
|
domains: args.domains || args.servernames,
|
|
action: 'reg',
|
|
communityMember: args.communityMember,
|
|
telemetry: args.telemetry
|
|
});
|
|
args._communityMemberAdded = true;
|
|
}
|
|
return core.certificates.registerAsync(args);
|
|
}
|
|
|
|
if (core.certificates._isRenewable(args, certs)) {
|
|
// it's time to renew the available cert
|
|
if (
|
|
false !== args.securityUpdates &&
|
|
!args._communityMemberAdded
|
|
) {
|
|
// We will notify all greenlock users of mandatory and security updates
|
|
// We'll keep track of versions and os so we can make sure things work well
|
|
// { name, version, email, domains, action, communityMember, telemetry }
|
|
require('./community').add({
|
|
name: args._communityPackage,
|
|
version: args._communityPackageVersion,
|
|
email: args.email,
|
|
domains: args.domains || args.servernames,
|
|
action: 'renew',
|
|
communityMember: args.communityMember,
|
|
telemetry: args.telemetry
|
|
});
|
|
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;
|
|
};
|