so close...

This commit is contained in:
AJ ONeal 2015-12-13 01:04:12 +00:00
parent c5f77c9339
commit d832a607e7
3 changed files with 143 additions and 82 deletions

View File

@ -40,6 +40,7 @@ le.register({
domains: ['example.com', 'www.example.com'] domains: ['example.com', 'www.example.com']
, email: 'user@example.com' , email: 'user@example.com'
, agreeTos: true , agreeTos: true
, webrootPath: '/srv/www/example.com/public'
}, function (err, certs) { }, function (err, certs) {
// do stuff // do stuff
}); });
@ -111,6 +112,9 @@ Typically the backend wrapper will already merge any necessary backend-specific
} }
``` ```
Note: `webrootPath` can be set as a default, semi-locally with `webrootPathTpl`, or per
regesitration as `webrootPath` (which overwrites `defaults.webrootPath`).
#### handlers *optional* #### handlers *optional*
`h.setChallenge(hostnames, name, value, cb)`: `h.setChallenge(hostnames, name, value, cb)`:
@ -171,6 +175,7 @@ Example:
le.register({ le.register({
domains: ['example.com', 'www.example.com'] domains: ['example.com', 'www.example.com']
, email: 'user@example.com' , email: 'user@example.com'
, webrootPath: '/srv/www/example.com/public'
, agreeTos: true , agreeTos: true
}, function (err, certs) { }, function (err, certs) {
// err is some error // err is some error
@ -233,7 +238,9 @@ This is what `args` looks like:
, agreeTos: true , agreeTos: true
, configDir: '/etc/letsencrypt' , configDir: '/etc/letsencrypt'
, fullchainTpl: '/live/:hostname/fullchain.pem' // :hostname will be replaced with the domainname , fullchainTpl: '/live/:hostname/fullchain.pem' // :hostname will be replaced with the domainname
, privkeyTpl: '/live/:hostname/privkey.pem' // :hostname , privkeyTpl: '/live/:hostname/privkey.pem'
, webrootPathTpl: '/srv/www/:hostname/public'
, webrootPath: '/srv/www/example.com/public' // templated from webrootPathTpl
} }
``` ```

View File

@ -31,7 +31,19 @@ var bkDefaults = {
, server: LEP.stagingServer , server: LEP.stagingServer
, text: true , text: true
}; };
var le = require('../').create(lep, bkDefaults, { }); var le = require('../').create(lep, bkDefaults, {
/*
setChallenge: function () {
// the python backend needs fs.watch implemented
// before this would work (and even then it would be difficult)
, getChallenge: function () {
//
}
}
, sniRegisterCallback: function () {
}
*/
});
var localCerts = require('localhost.daplie.com-certificates'); var localCerts = require('localhost.daplie.com-certificates');
var express = require('express'); var express = require('express');
@ -59,11 +71,14 @@ le.register({
agreeTos: 'agree' === conf.agree agreeTos: 'agree' === conf.agree
, domains: conf.domains.split(',') , domains: conf.domains.split(',')
, email: conf.email , email: conf.email
}).then(function () {
console.log('success');
}, function (err) { }, function (err) {
console.error(err.stack); if (err) {
}).then(function () { console.error('[Error]: node-letsencrypt/examples/standalone');
console.error(err.stack);
} else {
console.log('success');
}
server.close(); server.close();
tlsServer.close(); tlsServer.close();
}); });

191
index.js
View File

@ -1,12 +1,52 @@
'use strict'; 'use strict';
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var tls = require('tls');
module.exports.create = function (letsencrypt, defaults, options) { 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; }
letsencrypt = PromiseA.promisifyAll(letsencrypt); letsencrypt = PromiseA.promisifyAll(letsencrypt);
var tls = require('tls');
var fs = PromiseA.promisifyAll(require('fs')); var fs = PromiseA.promisifyAll(require('fs'));
var utils = require('./utils'); var utils = require('./utils');
// TODO move to backend-python.js
var registerAsync = PromiseA.promisify(function (args) { var registerAsync = PromiseA.promisify(function (args) {
return letsencrypt.registerAsync('certonly', args); return letsencrypt.registerAsync('certonly', args);
}); });
@ -20,14 +60,19 @@ module.exports.create = function (letsencrypt, defaults, options) {
, fs.readFileAsync(crtpath, 'ascii') , fs.readFileAsync(crtpath, 'ascii')
// stat the file, not the link // stat the file, not the link
, fs.statAsync(crtpath, 'ascii') , fs.statAsync(crtpath, 'ascii')
]); ]).then(function (arr) {
return {
key: arr[0] // privkey.pem
, cert: arr[1] // fullchain.pem
// TODO parse centificate
, renewedAt: arr[2].mtime.valueOf()
};
});
}); });
defaults.webroot = true;
//var attempts = {}; // should exist in master process only //var attempts = {}; // should exist in master process only
var ipc = {}; // in-process cache var ipc = {}; // in-process cache
var count = 0;
var now;
var le; var le;
// TODO check certs on initial load // TODO check certs on initial load
@ -35,35 +80,27 @@ module.exports.create = function (letsencrypt, defaults, options) {
// TODO check certs with setInterval? // TODO check certs with setInterval?
//options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000); //options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000);
defaults.webroot = true;
function merge(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;
}
function isCurrent(cache) { function isCurrent(cache) {
return cache; return cache;
} }
function sniCallback(hostname, cb) { function sniCallback(hostname, cb) {
var args = merge({}); var args = LE.merge(defaults, {});
args.domains = [hostname]; args.domains = [hostname];
le.fetch(args, function (err, cache) { le.fetch(args, function (err, cache) {
if (err) { if (err) {
cb(err); cb(err);
return; return;
} }
function respond(c2) { // vazhdo is Albanian for 'continue'
function vazhdo(err, c2) {
if (err) {
cb(err);
return;
}
cache = c2 || cache; cache = c2 || cache;
if (!cache.context) { if (!cache.context) {
@ -78,18 +115,25 @@ module.exports.create = function (letsencrypt, defaults, options) {
} }
if (isCurrent(cache)) { if (isCurrent(cache)) {
respond(); vazhdo();
return; return;
} }
defaults.needsRegistration(hostname, respond); var args = LE.merge(defaults, { domains: [hostname] });
handlers.sniRegisterCallback(args, cache, vazhdo);
}); });
} }
le = { le = {
validate: function () { validate: function (hostnames, cb) {
// TODO check dns, etc // TODO check dns, etc
return PromiseA.resolve(); 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);
} }
, middleware: function () { , middleware: function () {
//console.log('[DEBUG] webrootPath', defaults.webrootPath); //console.log('[DEBUG] webrootPath', defaults.webrootPath);
@ -107,62 +151,53 @@ module.exports.create = function (letsencrypt, defaults, options) {
} }
, SNICallback: sniCallback , SNICallback: sniCallback
, sniCallback: sniCallback , sniCallback: sniCallback
, cacheCerts: function (args, certs) { , register: function (args, cb) {
var hostname = args.domains[0]; var copy = LE.merge(defaults, args);
// assume 90 day renewals based on stat time, for now var err;
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])) { if (!utils.isValidDomain(args.domains[0])) {
return PromiseA.reject({ err = new Error("invalid domain");
message: "invalid domain" err.code = "INVALID_DOMAIN";
, code: "INVALID_DOMAIN" cb(err);
}); return;
} }
return le.validate(args.domains).then(function () { return le.validate(args.domains, function (err) {
if (err) {
cb(err);
return;
}
return registerAsync(copy).then(function () { return registerAsync(copy).then(function () {
return fetchAsync(args); // calls fetch because fetch calls cacheCertInfo
}); return le.fetch(args, cb);
}, cb);
}); });
} }
, fetch: function (args, cb) { , fetch: function (args, cb) {
var hostname = args.domains[0]; var hostname = args.domains[0];
// TODO don't call now() every time because this is hot code
var now = Date.now();
count += 1; // TODO handle www and no-www together somehow?
if (count >= 1000) {
now = Date.now();
count = 0;
}
var cached = ipc[hostname]; var cached = ipc[hostname];
// TODO handle www and no-www together
if (cached && ((now - cached.updated) < options.cacheContextsFor)) { if (cached) {
cb(null, cached.context); cb(null, cached.context);
return;
if ((now - cached.loadedAt) < (cached.memorizeFor)) {
// not stale yet
return;
}
} }
return fetchAsync(args).then(function (cached) { return fetchAsync(args).then(function (certInfo) {
cb(null, cached.context); if (certInfo) {
certInfo = LE.cacheCertInfo(args, certInfo, ipc, handlers);
cb(null, certInfo.context);
} else {
cb(null, null);
}
}, cb); }, cb);
} }
, fetchOrRegister: function (args, cb) { , fetchOrRegister: function (args, cb) {
@ -181,22 +216,26 @@ module.exports.create = function (letsencrypt, defaults, options) {
// TODO validate domains empirically before trying le // TODO validate domains empirically before trying le
return registerAsync(args/*, opts*/).then(function () { return registerAsync(args/*, opts*/).then(function () {
// wait at least n minutes // wait at least n minutes
return fetchAsync(args).then(function (cached) { le.fetch(args, function (err, cache) {
// success if (cache) {
cb(null, cached.context); cb(null, cache.context);
}, function (err) { return;
}
// still couldn't read the certs after success... that's weird // still couldn't read the certs after success... that's weird
cb(err); cb(err, null);
}); });
}, function (err) { }, function (err) {
console.error("[Error] Let's Encrypt failed:"); console.error("[Error] Let's Encrypt failed:");
console.error(err.stack || new Error(err.message || err.toString())); console.error(err.stack || new Error(err.message || err.toString()).stack);
// wasn't successful with lets encrypt, don't try again for n minutes // wasn't successful with lets encrypt, don't try again for n minutes
ipc[hostname] = { ipc[hostname] = {
context: null context: null
, updated: Date.now() , renewedAt: Date.now()
, duration: (5 * 60 * 1000)
}; };
cb(null, ipc[hostname]); cb(null, ipc[hostname]);
}); });
}); });