greenlock.js/index.js

284 wiersze
9.3 KiB
JavaScript
Czysty Zwykły widok Historia

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-13 05:03:48 +00:00
var crypto = require('crypto');
2015-12-13 01:04:12 +00:00
var tls = require('tls');
2015-12-17 04:59:47 +00:00
var leCore = require('letiny-core');
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-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
LE.merge = function merge(defaults, args) {
var copy = {};
Object.keys(defaults).forEach(function (key) {
copy[key] = defaults[key];
});
Object.keys(args).forEach(function (key) {
copy[key] = args[key];
});
return copy;
};
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;
}
if (!backend) { backend = require('./lib/letiny-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);
};
}
if (!handlers.getChallenge) {
2015-12-16 12:57:53 +00:00
if (!defaults.manual && !defaults.webrootPath) {
// 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, {});
var getChallenge = require('./lib/default-handlers').getChallenge;
defaultos.domains = [hostname];
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 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}}
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-13 05:03:48 +00:00
backend = PromiseA.promisifyAll(backend);
2015-12-13 01:04:12 +00:00
2015-12-15 11:38:21 +00:00
var utils = require('./utils');
2015-12-11 14:22:46 +00:00
//var attempts = {}; // should exist in master process only
var ipc = {}; // in-process cache
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
, 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-13 01:04:12 +00:00
console.warn("[SECURITY WARNING]: node-letsencrypt: validate(hostnames, cb) NOT IMPLEMENTED");
cb(null, true);
2015-12-12 14:20:12 +00:00
}
, middleware: function () {
2015-12-16 10:07:00 +00:00
var prefix = leCore.acmeChallengePrefix;
2015-12-12 14:20:12 +00:00
return function (req, res, next) {
2015-12-12 15:05:45 +00:00
if (0 !== req.url.indexOf(prefix)) {
2015-12-16 12:57:53 +00:00
//console.log('[LE middleware]: pass');
2015-12-12 14:20:12 +00:00
next();
return;
}
//args.domains = [req.hostname];
2015-12-16 12:57:53 +00:00
//console.log('[LE middleware]:', req.hostname, req.url, req.url.slice(prefix.length));
function done(err, token) {
if (err) {
res.send("Error: These aren't the tokens you're looking for. Move along.");
return;
}
res.send(token);
}
if (3 === handlers.getChallenge.length) {
handlers.getChallenge(req.hostname, req.url.slice(prefix.length), done);
}
else if (4 === handlers.getChallenge.length) {
handlers.getChallenge(defaults, req.hostname, req.url.slice(prefix.length), done);
}
else {
console.error("handlers.getChallenge [2] receives the wrong number of arguments");
done(new Error("handlers.getChallenge [2] receives the wrong number of arguments"));
}
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
2015-12-13 01:04:12 +00:00
return le.validate(args.domains, function (err) {
if (err) {
cb(err);
return;
}
2015-12-16 12:57:53 +00:00
//console.log("[NLE]: begin registration");
return backend.registerAsync(copy).then(function (pems) {
2015-12-16 12:57:53 +00:00
//console.log("[NLE]: end registration");
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) {
if (args.debug) {
console.log('[LE] debug is on');
2015-12-13 05:03:48 +00:00
}
if (true || args.debug) {
console.log('[LE] raw fetch certs', certInfo);
}
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);
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-13 05:03:48 +00:00
le._fetchHelper(args, cb);
2015-12-12 14:20:12 +00:00
}
, renew: function (args, cb) {
args.duplicate = false;
le.register(args, cb);
}
2015-12-13 05:03:48 +00:00
, register: function (args, cb) {
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-12 14:20:12 +00:00
var hostname = args.domains[0];
if (err) { cb(err); return; }
else if (hit) { cb(null, hit); return; }
2015-12-11 14:22:46 +00:00
return le._registerHelper(args, function (err, pems) {
2015-12-13 05:03:48 +00:00
if (err) {
cb(err);
return;
}
le._fetchHelper(args, function (err, cache) {
2015-12-13 01:04:12 +00:00
if (cache) {
cb(null, cache);
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
2015-12-13 01:04:12 +00:00
cb(err, null);
2015-12-11 14:22:46 +00:00
});
2015-12-12 14:20:12 +00:00
}, function (err) {
console.error("[Error] Let's Encrypt failed:");
2015-12-13 01:04:12 +00:00
console.error(err.stack || new Error(err.message || err.toString()).stack);
2015-12-12 14:20:12 +00:00
2015-12-13 05:03:48 +00:00
// wasn't successful with lets encrypt, don't automatically try again for 12 hours
// TODO what's the better way to handle this?
// failure callback?
2015-12-12 14:20:12 +00:00
ipc[hostname] = {
2015-12-13 05:03:48 +00:00
context: null // TODO default context
, issuedAt: Date.now()
, lifetime: (12 * 60 * 60 * 1000)
// , expiresAt: generated in next step
2015-12-12 14:20:12 +00:00
};
2015-12-13 01:04:12 +00:00
2015-12-13 05:03:48 +00:00
cb(err, ipc[hostname]);
2015-12-11 14:22:46 +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
};