greenlock.js/index.js

247 рядки
6.8 KiB
JavaScript

2015-12-11 14:22:46 +00:00
'use strict';
2015-12-12 15:05:45 +00:00
var PromiseA = require('bluebird');
2015-12-13 01:04:12 +00:00
var tls = require('tls');
2015-12-12 15:05:45 +00:00
2015-12-13 01:04:12 +00:00
var LE = module.exports;
LE.cacheCertInfo = function (args, certInfo, ipc, handlers) {
// Randomize by +(0% to 25%) to prevent all caches expiring at once
var rnd = (require('crypto').randomBytes(1)[0] / 255);
var memorizeFor = Math.floor(handlers.memorizeFor + ((handlers.memorizeFor / 4) * rnd));
var hostname = args.domains[0];
certInfo.context = tls.createSecureContext({
key: certInfo.key
, cert: certInfo.cert
//, ciphers // node's defaults are great
});
certInfo.duration = certInfo.duration || handlers.duration;
certInfo.loadedAt = Date.now();
certInfo.memorizeFor = memorizeFor;
ipc[hostname] = certInfo;
return ipc[hostname];
};
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;
};
LE.create = function (letsencrypt, defaults, handlers) {
if (!handlers) { handlers = {}; }
if (!handlers.duration) { handlers.duration = 90 * 24 * 60 * 60 * 1000; }
if (!handlers.renewIn) { handlers.renewIn = 80 * 24 * 60 * 60 * 1000; }
if (!handlers.memorizeFor) { handlers.memorizeFor = 1 * 24 * 60 * 60 * 1000; }
2015-12-12 15:05:45 +00:00
letsencrypt = PromiseA.promisifyAll(letsencrypt);
2015-12-11 14:22:46 +00:00
var fs = PromiseA.promisifyAll(require('fs'));
2015-12-12 14:20:12 +00:00
var utils = require('./utils');
2015-12-13 01:04:12 +00:00
// TODO move to backend-python.js
2015-12-12 14:20:12 +00:00
var registerAsync = PromiseA.promisify(function (args) {
return letsencrypt.registerAsync('certonly', args);
});
var fetchAsync = PromiseA.promisify(function (args) {
var hostname = args.domains[0];
var crtpath = defaults.configDir + defaults.fullchainTpl.replace(/:hostname/, hostname);
var privpath = defaults.configDir + defaults.privkeyTpl.replace(/:hostname/, hostname);
return PromiseA.all([
fs.readFileAsync(privpath, 'ascii')
, fs.readFileAsync(crtpath, 'ascii')
// stat the file, not the link
, fs.statAsync(crtpath, 'ascii')
2015-12-13 01:04:12 +00:00
]).then(function (arr) {
return {
key: arr[0] // privkey.pem
, cert: arr[1] // fullchain.pem
// TODO parse centificate
, renewedAt: arr[2].mtime.valueOf()
};
});
2015-12-12 14:20:12 +00:00
});
2015-12-13 01:04:12 +00:00
defaults.webroot = true;
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 15:50:00 +00:00
function isCurrent(cache) {
return cache;
}
2015-12-12 14:20:12 +00:00
function sniCallback(hostname, cb) {
2015-12-13 01:04:12 +00:00
var args = LE.merge(defaults, {});
2015-12-12 14:20:12 +00:00
args.domains = [hostname];
2015-12-13 01:04:12 +00:00
2015-12-12 14:20:12 +00:00
le.fetch(args, function (err, cache) {
if (err) {
cb(err);
return;
2015-12-11 14:22:46 +00:00
}
2015-12-13 01:04:12 +00:00
// vazhdo is Albanian for 'continue'
function vazhdo(err, c2) {
if (err) {
cb(err);
return;
}
2015-12-12 15:50:00 +00:00
cache = c2 || cache;
if (!cache.context) {
cache.context = tls.createSecureContext({
key: cache.key // privkey.pem
, cert: cache.cert // fullchain.pem
//, ciphers // node's defaults are great
});
}
cb(null, cache.context);
2015-12-11 14:22:46 +00:00
}
2015-12-12 15:50:00 +00:00
if (isCurrent(cache)) {
2015-12-13 01:04:12 +00:00
vazhdo();
2015-12-12 15:50:00 +00:00
return;
}
2015-12-13 01:04:12 +00:00
var args = LE.merge(defaults, { domains: [hostname] });
handlers.sniRegisterCallback(args, cache, vazhdo);
2015-12-12 14:20:12 +00:00
});
}
le = {
2015-12-13 01:04:12 +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;
}
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-12 15:19:11 +00:00
//console.log('[DEBUG] webrootPath', defaults.webrootPath);
var serveStatic = require('serve-static')(defaults.webrootPath, { dotfiles: 'allow' });
2015-12-12 14:20:12 +00:00
var prefix = '/.well-known/acme-challenge/';
return function (req, res, next) {
2015-12-12 15:05:45 +00:00
if (0 !== req.url.indexOf(prefix)) {
2015-12-12 14:20:12 +00:00
next();
return;
}
2015-12-12 15:19:11 +00:00
serveStatic(req, res, next);
2015-12-12 14:20:12 +00:00
};
}
, SNICallback: sniCallback
, sniCallback: sniCallback
2015-12-13 01:04:12 +00:00
, register: function (args, cb) {
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-12 14:20:12 +00:00
return registerAsync(copy).then(function () {
2015-12-13 01:04:12 +00:00
// calls fetch because fetch calls cacheCertInfo
return le.fetch(args, cb);
}, cb);
2015-12-12 14:20:12 +00:00
});
}
, fetch: function (args, cb) {
var hostname = args.domains[0];
2015-12-13 01:04:12 +00:00
// TODO don't call now() every time because this is hot code
var now = Date.now();
2015-12-12 14:20:12 +00:00
2015-12-13 01:04:12 +00:00
// TODO handle www and no-www together somehow?
2015-12-12 14:20:12 +00:00
var cached = ipc[hostname];
2015-12-13 01:04:12 +00:00
if (cached) {
2015-12-12 14:20:12 +00:00
cb(null, cached.context);
2015-12-13 01:04:12 +00:00
if ((now - cached.loadedAt) < (cached.memorizeFor)) {
// not stale yet
return;
}
2015-12-12 14:20:12 +00:00
}
2015-12-11 14:22:46 +00:00
2015-12-13 01:04:12 +00:00
return fetchAsync(args).then(function (certInfo) {
if (certInfo) {
certInfo = LE.cacheCertInfo(args, certInfo, ipc, handlers);
cb(null, certInfo.context);
} else {
cb(null, null);
}
2015-12-12 14:20:12 +00:00
}, cb);
}
, fetchOrRegister: function (args, cb) {
le.fetch(args, function (err, hit) {
var hostname = args.domains[0];
if (err) {
cb(err);
return;
}
else if (hit) {
cb(null, hit);
2015-12-11 14:22:46 +00:00
return;
}
2015-12-12 14:20:12 +00:00
// TODO validate domains empirically before trying le
return registerAsync(args/*, opts*/).then(function () {
// wait at least n minutes
2015-12-13 01:04:12 +00:00
le.fetch(args, function (err, cache) {
if (cache) {
cb(null, cache.context);
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
// wasn't successful with lets encrypt, don't try again for n minutes
ipc[hostname] = {
context: null
2015-12-13 01:04:12 +00:00
, renewedAt: Date.now()
, duration: (5 * 60 * 1000)
2015-12-12 14:20:12 +00:00
};
2015-12-13 01:04:12 +00:00
2015-12-12 14:20:12 +00:00
cb(null, 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
};