332 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			332 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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 ||
 | |
| 		gnlck._defaults.subscriberEmail;
 | |
| 
 | |
| 	var id = args.altnames.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 ||
 | |
| 		gnlck._defaults.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 ||
 | |
| 				gnlck._defaults.renewStagger ||
 | |
| 				0
 | |
| 		);
 | |
| 	} catch (e) {
 | |
| 		renewStagger = U._parseDuration(gnlck._defaults.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 ||
 | |
| 			gnlck._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'
 | |
| 		);
 | |
| 	}
 | |
| 	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;
 | |
| };
 |