2015-12-11 14:22:46 +00:00
|
|
|
'use strict';
|
|
|
|
|
2015-12-12 15:05:45 +00:00
|
|
|
var PromiseA = require('bluebird');
|
|
|
|
|
2015-12-12 14:20:12 +00:00
|
|
|
module.exports.create = function (letsencrypt, defaults, options) {
|
2015-12-12 15:05:45 +00:00
|
|
|
letsencrypt = PromiseA.promisifyAll(letsencrypt);
|
2015-12-11 14:22:46 +00:00
|
|
|
var tls = require('tls');
|
|
|
|
var fs = PromiseA.promisifyAll(require('fs'));
|
2015-12-12 14:20:12 +00:00
|
|
|
var utils = require('./utils');
|
|
|
|
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-11 14:22:46 +00:00
|
|
|
|
|
|
|
//var attempts = {}; // should exist in master process only
|
|
|
|
var ipc = {}; // in-process cache
|
|
|
|
var count = 0;
|
|
|
|
|
2015-12-12 14:20:12 +00:00
|
|
|
var now;
|
|
|
|
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
|
|
|
|
|
|
|
defaults.webroot = true;
|
|
|
|
|
2015-12-12 14:20:12 +00:00
|
|
|
function merge(args) {
|
|
|
|
var copy = {};
|
2015-12-12 15:05:45 +00:00
|
|
|
|
2015-12-12 14:20:12 +00:00
|
|
|
Object.keys(defaults).forEach(function (key) {
|
|
|
|
copy[key] = defaults[key];
|
|
|
|
});
|
|
|
|
Object.keys(args).forEach(function (key) {
|
|
|
|
copy[key] = args[key];
|
|
|
|
});
|
2015-12-12 15:05:45 +00:00
|
|
|
|
|
|
|
return copy;
|
2015-12-12 14:20:12 +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) {
|
|
|
|
var args = merge({});
|
|
|
|
args.domains = [hostname];
|
|
|
|
le.fetch(args, function (err, cache) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
2015-12-11 14:22:46 +00:00
|
|
|
}
|
|
|
|
|
2015-12-12 15:50:00 +00:00
|
|
|
function respond(c2) {
|
|
|
|
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)) {
|
|
|
|
respond();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
defaults.needsRegistration(hostname, respond);
|
2015-12-12 14:20:12 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
le = {
|
|
|
|
validate: function () {
|
2015-12-12 15:05:45 +00:00
|
|
|
// TODO check dns, etc
|
|
|
|
return PromiseA.resolve();
|
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
|
|
|
|
, cacheCerts: function (args, certs) {
|
|
|
|
var hostname = args.domains[0];
|
|
|
|
// assume 90 day renewals based on stat time, for now
|
|
|
|
ipc[hostname] = {
|
|
|
|
context: tls.createSecureContext({
|
|
|
|
key: certs[0] // privkey.pem
|
|
|
|
, cert: certs[1] // fullchain.pem
|
|
|
|
//, ciphers // node's defaults are great
|
|
|
|
})
|
|
|
|
, updated: Date.now()
|
|
|
|
};
|
|
|
|
|
|
|
|
return ipc[hostname];
|
|
|
|
}
|
|
|
|
, readAndCacheCerts: function (args) {
|
|
|
|
return fetchAsync(args).then(function (certs) {
|
|
|
|
return le.cacheCerts(args, certs);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
, register: function (args) {
|
|
|
|
// TODO validate domains and such
|
|
|
|
|
|
|
|
var copy = merge(args);
|
|
|
|
|
|
|
|
if (!utils.isValidDomain(args.domains[0])) {
|
|
|
|
return PromiseA.reject({
|
|
|
|
message: "invalid domain"
|
|
|
|
, code: "INVALID_DOMAIN"
|
|
|
|
});
|
2015-12-11 14:22:46 +00:00
|
|
|
}
|
2015-12-12 14:20:12 +00:00
|
|
|
|
|
|
|
return le.validate(args.domains).then(function () {
|
|
|
|
return registerAsync(copy).then(function () {
|
|
|
|
return fetchAsync(args);
|
2015-12-11 14:22:46 +00:00
|
|
|
});
|
2015-12-12 14:20:12 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
, fetch: function (args, cb) {
|
|
|
|
var hostname = args.domains[0];
|
|
|
|
|
|
|
|
count += 1;
|
|
|
|
|
|
|
|
if (count >= 1000) {
|
|
|
|
now = Date.now();
|
|
|
|
count = 0;
|
2015-12-11 14:22:46 +00:00
|
|
|
}
|
|
|
|
|
2015-12-12 14:20:12 +00:00
|
|
|
var cached = ipc[hostname];
|
|
|
|
// TODO handle www and no-www together
|
|
|
|
if (cached && ((now - cached.updated) < options.cacheContextsFor)) {
|
|
|
|
cb(null, cached.context);
|
|
|
|
return;
|
|
|
|
}
|
2015-12-11 14:22:46 +00:00
|
|
|
|
2015-12-12 14:20:12 +00:00
|
|
|
return fetchAsync(args).then(function (cached) {
|
|
|
|
cb(null, cached.context);
|
|
|
|
}, 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
|
|
|
|
return fetchAsync(args).then(function (cached) {
|
|
|
|
// success
|
|
|
|
cb(null, cached.context);
|
2015-12-11 14:22:46 +00:00
|
|
|
}, function (err) {
|
2015-12-12 14:20:12 +00:00
|
|
|
// still couldn't read the certs after success... that's weird
|
|
|
|
cb(err);
|
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:");
|
|
|
|
console.error(err.stack || new Error(err.message || err.toString()));
|
|
|
|
|
|
|
|
// wasn't successful with lets encrypt, don't try again for n minutes
|
|
|
|
ipc[hostname] = {
|
|
|
|
context: null
|
|
|
|
, updated: Date.now()
|
|
|
|
};
|
|
|
|
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
|
|
|
};
|