2015-12-11 14:22:46 +00:00
|
|
|
'use strict';
|
|
|
|
|
2015-12-13 05:03:48 +00:00
|
|
|
// TODO handle www and no-www together somehow?
|
|
|
|
|
2015-12-12 15:05:45 +00:00
|
|
|
var PromiseA = require('bluebird');
|
2015-12-17 04:59:47 +00:00
|
|
|
var leCore = require('letiny-core');
|
2015-12-17 08:46:40 +00:00
|
|
|
var merge = require('./lib/common').merge;
|
2015-12-20 00:27:48 +00:00
|
|
|
var tplCopy = require('./lib/common').tplCopy;
|
2015-12-12 15:05:45 +00:00
|
|
|
|
2015-12-13 01:04:12 +00:00
|
|
|
var LE = module.exports;
|
2015-12-16 09:11:31 +00:00
|
|
|
LE.productionServerUrl = leCore.productionServerUrl;
|
2015-12-17 04:59:47 +00:00
|
|
|
LE.stagingServerUrl = leCore.stagingServerUrl;
|
2015-12-16 09:11:31 +00:00
|
|
|
LE.configDir = leCore.configDir;
|
2015-12-16 10:07:00 +00:00
|
|
|
LE.logsDir = leCore.logsDir;
|
|
|
|
LE.workDir = leCore.workDir;
|
2015-12-16 09:11:31 +00:00
|
|
|
LE.acmeChallengPrefix = leCore.acmeChallengPrefix;
|
|
|
|
LE.knownEndpoints = leCore.knownEndpoints;
|
2015-12-13 01:04:12 +00:00
|
|
|
|
2015-12-20 10:41:17 +00:00
|
|
|
LE.privkeyPath = ':config/live/:hostname/privkey.pem';
|
|
|
|
LE.fullchainPath = ':config/live/:hostname/fullchain.pem';
|
|
|
|
LE.certPath = ':config/live/:hostname/cert.pem';
|
|
|
|
LE.chainPath = ':config/live/:hostname/chain.pem';
|
2015-12-21 17:27:57 +00:00
|
|
|
LE.renewalPath = ':config/renewal/:hostname.conf';
|
|
|
|
LE.accountsDir = ':config/accounts/:server';
|
2016-02-13 02:33:50 +00:00
|
|
|
LE.defaults = {
|
|
|
|
privkeyPath: LE.privkeyPath
|
|
|
|
, fullchainPath: LE.fullchainPath
|
|
|
|
, certPath: LE.certPath
|
|
|
|
, chainPath: LE.chainPath
|
|
|
|
, renewalPath: LE.renewalPath
|
|
|
|
, accountsDir: LE.accountsDir
|
|
|
|
, server: LE.productionServerUrl
|
|
|
|
};
|
2015-12-20 10:41:17 +00:00
|
|
|
|
2015-12-16 09:11:31 +00:00
|
|
|
// backwards compat
|
2015-12-17 04:59:47 +00:00
|
|
|
LE.stagingServer = leCore.stagingServerUrl;
|
2015-12-16 09:11:31 +00:00
|
|
|
LE.liveServer = leCore.productionServerUrl;
|
|
|
|
LE.knownUrls = leCore.knownEndpoints;
|
2015-12-13 01:04:12 +00:00
|
|
|
|
2015-12-17 08:46:40 +00:00
|
|
|
LE.merge = require('./lib/common').merge;
|
|
|
|
LE.tplConfigDir = require('./lib/common').tplConfigDir;
|
2015-12-13 01:04:12 +00:00
|
|
|
|
2015-12-16 09:11:31 +00:00
|
|
|
// backend, defaults, handlers
|
|
|
|
LE.create = function (defaults, handlers, backend) {
|
|
|
|
var d, b, h;
|
|
|
|
// backwards compat for <= v1.0.2
|
|
|
|
if (defaults.registerAsync || defaults.create) {
|
|
|
|
b = defaults; d = handlers; h = backend;
|
|
|
|
defaults = d; handlers = h; backend = b;
|
|
|
|
}
|
2015-12-19 10:13:10 +00:00
|
|
|
if (!backend) { backend = require('./lib/core'); }
|
2015-12-13 01:04:12 +00:00
|
|
|
if (!handlers) { handlers = {}; }
|
2015-12-13 05:03:48 +00:00
|
|
|
if (!handlers.lifetime) { handlers.lifetime = 90 * 24 * 60 * 60 * 1000; }
|
|
|
|
if (!handlers.renewWithin) { handlers.renewWithin = 3 * 24 * 60 * 60 * 1000; }
|
2015-12-13 01:04:12 +00:00
|
|
|
if (!handlers.memorizeFor) { handlers.memorizeFor = 1 * 24 * 60 * 60 * 1000; }
|
2015-12-13 05:03:48 +00:00
|
|
|
if (!handlers.sniRegisterCallback) {
|
|
|
|
handlers.sniRegisterCallback = function (args, cache, cb) {
|
|
|
|
// TODO when we have ECDSA, just do this automatically
|
|
|
|
cb(null, null);
|
|
|
|
};
|
|
|
|
}
|
2015-12-15 15:40:44 +00:00
|
|
|
if (!handlers.getChallenge) {
|
2015-12-16 12:57:53 +00:00
|
|
|
if (!defaults.manual && !defaults.webrootPath) {
|
2015-12-15 15:40:44 +00:00
|
|
|
// GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}}
|
|
|
|
throw new Error("handlers.getChallenge or defaults.webrootPath must be set");
|
|
|
|
}
|
|
|
|
handlers.getChallenge = function (hostname, key, done) {
|
|
|
|
// TODO associate by hostname?
|
|
|
|
// hmm... I don't think there's a direct way to associate this with
|
|
|
|
// the request it came from... it's kinda stateless in that way
|
|
|
|
// but realistically there only needs to be one handler and one
|
|
|
|
// "directory" for this. It's not that big of a deal.
|
|
|
|
var defaultos = LE.merge(defaults, {});
|
2015-12-17 04:44:28 +00:00
|
|
|
var getChallenge = require('./lib/default-handlers').getChallenge;
|
2015-12-17 08:46:40 +00:00
|
|
|
var copy = merge(defaults, { domains: [hostname] });
|
|
|
|
|
2015-12-20 00:27:48 +00:00
|
|
|
tplCopy(copy);
|
2015-12-15 15:40:44 +00:00
|
|
|
defaultos.domains = [hostname];
|
2015-12-17 08:46:40 +00:00
|
|
|
|
2015-12-17 04:44:28 +00:00
|
|
|
if (3 === getChallenge.length) {
|
|
|
|
getChallenge(defaultos, key, done);
|
|
|
|
}
|
|
|
|
else if (4 === getChallenge.length) {
|
|
|
|
getChallenge(defaultos, hostname, key, done);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
done(new Error("handlers.getChallenge [1] receives the wrong number of arguments"));
|
|
|
|
}
|
2015-12-15 15:40:44 +00:00
|
|
|
};
|
|
|
|
}
|
2015-12-15 11:38:21 +00:00
|
|
|
if (!handlers.setChallenge) {
|
|
|
|
if (!defaults.webrootPath) {
|
|
|
|
// GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}}
|
|
|
|
throw new Error("handlers.setChallenge or defaults.webrootPath must be set");
|
|
|
|
}
|
2015-12-15 12:01:05 +00:00
|
|
|
handlers.setChallenge = require('./lib/default-handlers').setChallenge;
|
2015-12-15 11:38:21 +00:00
|
|
|
}
|
|
|
|
if (!handlers.removeChallenge) {
|
|
|
|
if (!defaults.webrootPath) {
|
|
|
|
// GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}}
|
2015-12-15 15:40:44 +00:00
|
|
|
throw new Error("handlers.removeChallenge or defaults.webrootPath must be set");
|
2015-12-15 11:38:21 +00:00
|
|
|
}
|
2015-12-15 12:01:46 +00:00
|
|
|
handlers.removeChallenge = require('./lib/default-handlers').removeChallenge;
|
2015-12-15 11:38:21 +00:00
|
|
|
}
|
|
|
|
if (!handlers.agreeToTerms) {
|
|
|
|
if (defaults.agreeTos) {
|
|
|
|
console.warn("[WARN] Agreeing to terms by default is risky business...");
|
|
|
|
}
|
2015-12-15 13:12:16 +00:00
|
|
|
handlers.agreeToTerms = require('./lib/default-handlers').agreeToTerms;
|
2015-12-15 11:38:21 +00:00
|
|
|
}
|
|
|
|
if ('function' === typeof backend.create) {
|
|
|
|
backend = backend.create(defaults, handlers);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// ignore
|
|
|
|
// this backend was created the v1.0.0 way
|
|
|
|
}
|
2015-12-17 08:46:40 +00:00
|
|
|
|
|
|
|
// replaces strings of workDir, certPath, etc
|
|
|
|
// if they have :config/etc/live or :conf/etc/archive
|
|
|
|
// to instead have the path of the configDir
|
|
|
|
LE.tplConfigDir(defaults.configDir, defaults);
|
|
|
|
|
2015-12-13 05:03:48 +00:00
|
|
|
backend = PromiseA.promisifyAll(backend);
|
2015-12-13 01:04:12 +00:00
|
|
|
|
2015-12-19 10:18:32 +00:00
|
|
|
var utils = require('./lib/common');
|
2015-12-11 14:22:46 +00:00
|
|
|
//var attempts = {}; // should exist in master process only
|
2015-12-12 14:20:12 +00:00
|
|
|
var le;
|
2015-12-11 14:22:46 +00:00
|
|
|
|
2015-12-12 15:05:45 +00:00
|
|
|
// TODO check certs on initial load
|
|
|
|
// TODO expect that certs expire every 90 days
|
|
|
|
// TODO check certs with setInterval?
|
|
|
|
//options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000);
|
2015-12-11 14:22:46 +00:00
|
|
|
|
2015-12-12 14:20:12 +00:00
|
|
|
le = {
|
2015-12-15 12:12:15 +00:00
|
|
|
backend: backend
|
2015-12-20 10:41:17 +00:00
|
|
|
, pyToJson: function (pyobj) {
|
|
|
|
if (!pyobj) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
var jsobj = {};
|
|
|
|
Object.keys(pyobj).forEach(function (key) {
|
|
|
|
jsobj[key] = pyobj[key];
|
|
|
|
});
|
|
|
|
jsobj.__lines = undefined;
|
|
|
|
jsobj.__keys = undefined;
|
|
|
|
|
|
|
|
return jsobj;
|
|
|
|
}
|
2015-12-15 12:12:15 +00:00
|
|
|
, validate: function (hostnames, cb) {
|
2015-12-12 15:05:45 +00:00
|
|
|
// TODO check dns, etc
|
2015-12-13 01:04:12 +00:00
|
|
|
if ((!hostnames.length && hostnames.every(le.isValidDomain))) {
|
|
|
|
cb(new Error("node-letsencrypt: invalid hostnames: " + hostnames.join(',')));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-12-13 06:00:30 +00:00
|
|
|
//
|
|
|
|
// IMPORTANT
|
|
|
|
//
|
|
|
|
// Before attempting a dynamic registration you need to validate that
|
|
|
|
//
|
|
|
|
// * these are hostnames that you expected to exist on the system
|
|
|
|
// * their A records currently point to this ip
|
|
|
|
// * this system's ip hasn't changed
|
|
|
|
//
|
|
|
|
// If you do not check these things, then someone could attack you
|
|
|
|
// and cause you, in return, to have your ip be rate-limit blocked
|
|
|
|
//
|
2015-12-20 05:13:41 +00:00
|
|
|
//console.warn("\n[TODO]: node-letsencrypt: `validate(hostnames, cb)` needs to be implemented");
|
|
|
|
//console.warn("(it'll work fine without it, but for security - and convenience - it should be implemented\n");
|
|
|
|
// UPDATE:
|
|
|
|
// it's actually probably better that we don't do this here and instead
|
|
|
|
// take care of it in the approveRegistrationCallback in letsencrypt-express
|
2015-12-13 01:04:12 +00:00
|
|
|
cb(null, true);
|
2015-12-12 14:20:12 +00:00
|
|
|
}
|
2015-12-13 05:03:48 +00:00
|
|
|
, _registerHelper: function (args, cb) {
|
2015-12-13 01:04:12 +00:00
|
|
|
var copy = LE.merge(defaults, args);
|
|
|
|
var err;
|
2015-12-12 14:20:12 +00:00
|
|
|
|
|
|
|
if (!utils.isValidDomain(args.domains[0])) {
|
2015-12-13 01:04:12 +00:00
|
|
|
err = new Error("invalid domain");
|
|
|
|
err.code = "INVALID_DOMAIN";
|
|
|
|
cb(err);
|
|
|
|
return;
|
2015-12-11 14:22:46 +00:00
|
|
|
}
|
2015-12-12 14:20:12 +00:00
|
|
|
|
2016-02-10 20:41:15 +00:00
|
|
|
le.validate(args.domains, function (err) {
|
2015-12-13 01:04:12 +00:00
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-02-10 20:41:15 +00:00
|
|
|
if (defaults.debug || args.debug) {
|
2015-12-20 00:27:48 +00:00
|
|
|
console.log("[NLE]: begin registration");
|
|
|
|
}
|
2016-02-10 20:41:15 +00:00
|
|
|
|
2015-12-17 05:44:41 +00:00
|
|
|
return backend.registerAsync(copy).then(function (pems) {
|
2016-02-10 20:41:15 +00:00
|
|
|
if (defaults.debug || args.debug) {
|
2015-12-20 00:27:48 +00:00
|
|
|
console.log("[NLE]: end registration");
|
|
|
|
}
|
2015-12-17 05:44:41 +00:00
|
|
|
cb(null, pems);
|
|
|
|
//return le.fetch(args, cb);
|
2015-12-13 01:04:12 +00:00
|
|
|
}, cb);
|
2015-12-12 14:20:12 +00:00
|
|
|
});
|
|
|
|
}
|
2015-12-13 05:03:48 +00:00
|
|
|
, _fetchHelper: function (args, cb) {
|
|
|
|
return backend.fetchAsync(args).then(function (certInfo) {
|
2015-12-17 05:44:41 +00:00
|
|
|
if (args.debug) {
|
2015-12-17 08:46:40 +00:00
|
|
|
console.log('[LE] raw fetch certs', certInfo && Object.keys(certInfo));
|
2015-12-17 05:44:41 +00:00
|
|
|
}
|
|
|
|
if (!certInfo) { cb(null, null); return; }
|
2015-12-13 05:03:48 +00:00
|
|
|
|
|
|
|
// key, cert, issuedAt, lifetime, expiresAt
|
|
|
|
if (!certInfo.expiresAt) {
|
|
|
|
certInfo.expiresAt = certInfo.issuedAt + (certInfo.lifetime || handlers.lifetime);
|
|
|
|
}
|
|
|
|
if (!certInfo.lifetime) {
|
|
|
|
certInfo.lifetime = (certInfo.lifetime || handlers.lifetime);
|
|
|
|
}
|
|
|
|
// a pretty good hard buffer
|
|
|
|
certInfo.expiresAt -= (1 * 24 * 60 * 60 * 100);
|
|
|
|
|
2015-12-17 05:44:41 +00:00
|
|
|
cb(null, certInfo);
|
2015-12-13 05:03:48 +00:00
|
|
|
}, cb);
|
|
|
|
}
|
2015-12-12 14:20:12 +00:00
|
|
|
, fetch: function (args, cb) {
|
2015-12-21 17:27:57 +00:00
|
|
|
if (defaults.debug || args.debug) {
|
|
|
|
console.log('[LE] fetch');
|
|
|
|
}
|
2015-12-13 05:03:48 +00:00
|
|
|
le._fetchHelper(args, cb);
|
2015-12-12 14:20:12 +00:00
|
|
|
}
|
2015-12-17 05:44:41 +00:00
|
|
|
, renew: function (args, cb) {
|
2015-12-21 17:27:57 +00:00
|
|
|
if (defaults.debug || args.debug) {
|
|
|
|
console.log('[LE] renew');
|
|
|
|
}
|
2015-12-17 05:44:41 +00:00
|
|
|
args.duplicate = false;
|
|
|
|
le.register(args, cb);
|
|
|
|
}
|
2015-12-20 10:41:17 +00:00
|
|
|
, getConfig: function (args, cb) {
|
2015-12-21 17:27:57 +00:00
|
|
|
if (defaults.debug || args.debug) {
|
|
|
|
console.log('[LE] getConfig');
|
|
|
|
}
|
2015-12-20 10:41:17 +00:00
|
|
|
backend.getConfigAsync(args).then(function (pyobj) {
|
|
|
|
cb(null, le.pyToJson(pyobj));
|
|
|
|
}, function (err) {
|
2016-02-10 20:41:15 +00:00
|
|
|
console.error("[letsencrypt/index.js] getConfig");
|
2015-12-20 10:41:17 +00:00
|
|
|
console.error(err.stack);
|
|
|
|
return cb(null, []);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
, getConfigs: function (args, cb) {
|
2015-12-21 17:27:57 +00:00
|
|
|
if (defaults.debug || args.debug) {
|
|
|
|
console.log('[LE] getConfigs');
|
|
|
|
}
|
2015-12-20 10:41:17 +00:00
|
|
|
backend.getConfigsAsync(args).then(function (configs) {
|
|
|
|
cb(null, configs.map(le.pyToJson));
|
|
|
|
}, function (err) {
|
|
|
|
if ('ENOENT' === err.code) {
|
|
|
|
cb(null, []);
|
|
|
|
} else {
|
2016-02-10 20:41:15 +00:00
|
|
|
console.error("[letsencrypt/index.js] getConfigs");
|
2015-12-20 10:41:17 +00:00
|
|
|
console.error(err.stack);
|
|
|
|
cb(err);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
, setConfig: function (args, cb) {
|
2015-12-21 17:27:57 +00:00
|
|
|
if (defaults.debug || args.debug) {
|
|
|
|
console.log('[LE] setConfig');
|
|
|
|
}
|
2015-12-20 10:41:17 +00:00
|
|
|
backend.configureAsync(args).then(function (pyobj) {
|
|
|
|
cb(null, le.pyToJson(pyobj));
|
|
|
|
});
|
|
|
|
}
|
2015-12-13 05:03:48 +00:00
|
|
|
, register: function (args, cb) {
|
2015-12-21 17:27:57 +00:00
|
|
|
if (defaults.debug || args.debug) {
|
|
|
|
console.log('[LE] register');
|
|
|
|
}
|
2015-12-16 12:57:53 +00:00
|
|
|
if (!Array.isArray(args.domains)) {
|
|
|
|
cb(new Error('args.domains should be an array of domains'));
|
|
|
|
return;
|
|
|
|
}
|
2015-12-13 05:03:48 +00:00
|
|
|
// this may be run in a cluster environment
|
|
|
|
// in that case it should NOT check the cache
|
|
|
|
// but ensure that it has the most fresh copy
|
|
|
|
// before attempting a renew
|
|
|
|
le._fetchHelper(args, function (err, hit) {
|
2015-12-17 08:46:40 +00:00
|
|
|
var now = Date.now();
|
2015-12-12 14:20:12 +00:00
|
|
|
|
2015-12-17 08:46:40 +00:00
|
|
|
if (err) {
|
|
|
|
// had a bad day
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else if (hit) {
|
2015-12-17 09:17:27 +00:00
|
|
|
if (!args.duplicate && (now - hit.issuedAt) < ((hit.lifetime || handlers.lifetime) * 0.65)) {
|
|
|
|
console.warn("\ntried to renew a certificate with over 1/3 of its lifetime left, ignoring");
|
|
|
|
console.warn("(use --duplicate or opts.duplicate to override\n");
|
2015-12-17 08:46:40 +00:00
|
|
|
cb(null, hit);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2015-12-11 14:22:46 +00:00
|
|
|
|
2016-02-10 20:41:15 +00:00
|
|
|
le._registerHelper(args, function (err/*, pems*/) {
|
2015-12-13 05:03:48 +00:00
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-12-17 08:46:40 +00:00
|
|
|
// Sanity Check
|
|
|
|
le._fetchHelper(args, function (err, pems) {
|
|
|
|
if (pems) {
|
|
|
|
cb(null, pems);
|
2015-12-13 01:04:12 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-12-12 14:20:12 +00:00
|
|
|
// still couldn't read the certs after success... that's weird
|
2016-02-10 20:41:15 +00:00
|
|
|
console.error("still couldn't read certs after success... that's weird");
|
2015-12-13 01:04:12 +00:00
|
|
|
cb(err, null);
|
2015-12-11 14:22:46 +00:00
|
|
|
});
|
2016-02-10 20:41:15 +00:00
|
|
|
});
|
2015-12-12 14:20:12 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
2015-12-11 14:22:46 +00:00
|
|
|
|
2015-12-12 14:20:12 +00:00
|
|
|
return le;
|
2015-12-11 14:22:46 +00:00
|
|
|
};
|