so close...
This commit is contained in:
parent
c5f77c9339
commit
d832a607e7
|
@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
if (err) {
|
||||||
|
console.error('[Error]: node-letsencrypt/examples/standalone');
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
}).then(function () {
|
} else {
|
||||||
|
console.log('success');
|
||||||
|
}
|
||||||
|
|
||||||
server.close();
|
server.close();
|
||||||
tlsServer.close();
|
tlsServer.close();
|
||||||
});
|
});
|
||||||
|
|
189
index.js
189
index.js
|
@ -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, function (err) {
|
||||||
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return le.validate(args.domains).then(function () {
|
|
||||||
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);
|
||||||
|
|
||||||
|
if ((now - cached.loadedAt) < (cached.memorizeFor)) {
|
||||||
|
// not stale yet
|
||||||
return;
|
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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue