325 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			325 lines
		
	
	
		
			9.6 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;
 | 
						|
 | 
						|
    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;
 | 
						|
};
 |