This commit is contained in:
AJ ONeal 2015-12-12 14:20:12 +00:00
parent 145dbad411
commit 88406b9c0b
4 changed files with 186 additions and 158 deletions

View File

@ -1,7 +1,6 @@
'use strict'; 'use strict';
var homedir = require('homedir'); var leBinPath = require('homedir')() + '/.local/share/letsencrypt/bin/letsencrypt';
var leBinPath = homedir() + '/.local/share/letsencrypt/bin/letsencrypt';
var lep = require('letsencrypt-python').create(leBinPath); var lep = require('letsencrypt-python').create(leBinPath);
var conf = { var conf = {
domains: process.argv[2] domains: process.argv[2]
@ -21,13 +20,13 @@ var bkDefaults = {
, workDir: '/var/lib/letsencrypt' , workDir: '/var/lib/letsencrypt'
, text: true , text: true
}; };
var le = require('letsencrypt').create(lep, bkDefaults); var le = require('../').create(lep, bkDefaults);
var localCerts = require('localhost.daplie.com-certificates'); var localCerts = require('localhost.daplie.com-certificates');
var express = require('express'); var express = require('express');
var app = express(); var app = express();
app.use(le.middleware); app.use(le.middleware());
var server = require('http').createServer(); var server = require('http').createServer();
server.on('request', app); server.on('request', app);
@ -38,14 +37,14 @@ server.listen(80, function () {
var tlsServer = require('https').createServer({ var tlsServer = require('https').createServer({
key: localCerts.key key: localCerts.key
, cert: localCerts.cert , cert: localCerts.cert
, SNICallback: le.SNICallback , SNICallback: le.sniCallback
}); });
tlsServer.on('request', app); tlsServer.on('request', app);
tlsServer.listen(443, function () { tlsServer.listen(443, function () {
console.log('Listening http', tlsServer.address()); console.log('Listening http', tlsServer.address());
}); });
le.register('certonly', { le.register({
agreeTos: 'agree' === conf.agree agreeTos: 'agree' === conf.agree
, domains: conf.domains.split(',') , domains: conf.domains.split(',')
, email: conf.email , email: conf.email

240
index.js
View File

@ -1,120 +1,186 @@
'use strict'; 'use strict';
module.exports.create = function (lebinpath, defaults, options) { module.exports.create = function (letsencrypt, defaults, options) {
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var tls = require('tls'); var tls = require('tls');
var fs = PromiseA.promisifyAll(require('fs')); var fs = PromiseA.promisifyAll(require('fs'));
var letsencrypt = PromiseA.promisifyAll(require('./le-exec-wrapper')); 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')
]);
});
//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 count = 0;
//var certTpl = "/live/:hostname/cert.pem"; var now;
var certTpl = "/live/:hostname/fullchain.pem"; var le;
var privTpl = "/live/:hostname/privkey.pem";
options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000); options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000);
defaults.webroot = true; defaults.webroot = true;
defaults.webrootPath = '/srv/www/acme-challenge';
return letsencrypt.optsAsync(lebinpath).then(function (keys) { function merge(args) {
var now; var copy = {};
var le; Object.keys(defaults).forEach(function (key) {
copy[key] = defaults[key];
});
Object.keys(args).forEach(function (key) {
copy[key] = args[key];
});
}
le = { function sniCallback(hostname, cb) {
validate: function () { var args = merge({});
args.domains = [hostname];
le.fetch(args, function (err, cache) {
if (err) {
cb(err);
return;
} }
, argnames: keys
, readCerts: function (hostname) {
var crtpath = defaults.configDir + certTpl.replace(/:hostname/, hostname);
var privpath = defaults.configDir + privTpl.replace(/:hostname/, hostname);
return PromiseA.all([ if (!cache.context) {
fs.readFileAsync(privpath, 'ascii') cache.context = tls.createSecureContext({
, fs.readFileAsync(crtpath, 'ascii') key: cache.key // privkey.pem
// stat the file, not the link , cert: cache.cert // fullchain.pem
, fs.statAsync(crtpath, 'ascii') //, ciphers // node's defaults are great
]).then(function (arr) {
return arr;
}); });
} }
, cacheCerts: function (hostname, certs) {
// 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]; cb(null, cache.context);
} });
, readAndCacheCerts: function (hostname) { }
return le.readCerts(hostname).then(function (certs) {
return le.cacheCerts(hostname, certs);
});
}
, get: function (hostname, args, opts, cb) {
count += 1;
if (count >= 1000) { le = {
now = Date.now(); validate: function () {
count = 0; }
} , middleware: function () {
var serveStatic = require('serve-static')(defaults.webrootPath);
var prefix = '/.well-known/acme-challenge/';
var cached = ipc[hostname]; return function (req, res, next) {
// TODO handle www and no-www together if (0 === req.url.indexOf(prefix)) {
if (cached && ((now - cached.updated) < options.cacheContextsFor)) { next();
cb(null, cached.context);
return; return;
} }
return le.readCerts(hostname).then(function (cached) { var pathname = req.url;
cb(null, cached.context); req.url = req.url.substr(prefix.length - 1);
}, function (/*err*/) { serveStatic(req, res, function (err) {
var copy = {}; req.url = pathname;
var arr; next(err);
});
};
}
, 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()
};
// TODO validate domains and such return ipc[hostname];
Object.keys(defaults).forEach(function (key) { }
copy[key] = defaults[key]; , readAndCacheCerts: function (args) {
}); return fetchAsync(args).then(function (certs) {
Object.keys(args).forEach(function (key) { return le.cacheCerts(args, certs);
copy[key] = args[key]; });
}); }
, register: function (args) {
// TODO validate domains and such
arr = letsencrypt.objToArr(keys, copy); var copy = merge(args);
// TODO validate domains empirically before trying le
return letsencrypt.execAsync(lebinpath, arr, opts).then(function () {
// wait at least n minutes
return le.readCerts(hostname).then(function (cached) {
// success
cb(null, cached.context);
}, function (err) {
// still couldn't read the certs after success... that's weird
cb(err);
});
}, 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 if (!utils.isValidDomain(args.domains[0])) {
ipc[hostname] = { return PromiseA.reject({
context: null message: "invalid domain"
, updated: Date.now() , code: "INVALID_DOMAIN"
};
cb(null, ipc[hostname]);
});
}); });
} }
};
return le; return le.validate(args.domains).then(function () {
}); return registerAsync(copy).then(function () {
return fetchAsync(args);
});
});
}
, fetch: function (args, cb) {
var hostname = args.domains[0];
count += 1;
if (count >= 1000) {
now = Date.now();
count = 0;
}
var cached = ipc[hostname];
// TODO handle www and no-www together
if (cached && ((now - cached.updated) < options.cacheContextsFor)) {
cb(null, cached.context);
return;
}
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);
return;
}
// 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);
}, function (err) {
// still couldn't read the certs after success... that's weird
cb(err);
});
}, 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]);
});
});
}
};
return le;
}; };

View File

@ -2,7 +2,7 @@
"name": "letsencrypt", "name": "letsencrypt",
"version": "1.0.0", "version": "1.0.0",
"description": "Let's Encrypt for node.js on npm", "description": "Let's Encrypt for node.js on npm",
"main": "le.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },

View File

@ -1,74 +1,37 @@
'use strict'; 'use strict';
var letsencrypt = require('letsencrypt'); var config = require('./config');
var networkInterfaces = require('os').networkInterfaces(); var Letsencrypt = require('../');
var ipify = require('ipify'); var leBinPath = '/home/user/.local/share/letsencrypt/bin/letsencrypt';
var LEP = require('letsencrypt-python');
var lep = LEP.create(leBinPath);
function getSecureContext(le, hostname, cb) { require('./serve-acme-challenges').create({
hostname = hostname.replace(/^www\./, ''); configDir: config.configDir
});
function needsRegistration(hostnames, cb) { //var networkInterfaces = require('os').networkInterfaces();
// //var ipify = require('ipify');
// 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
//
le.validate(hostnames, {
networkInterfaces: networkInterfaces
, ipify: ipify
}, function (err) {
if (err) {
cb(null, null);
return;
}
// these hostnames need to be registered var le = Letsencrypt.create(
// lep
cb(null, {
email: 'john.doe@gmail.com'
, agreeTos: true
, domains: ['www.' + hostname, hostname]
});
});
}
// secure contexts will be cached
// renewals will be checked in the background
le.get(hostname, needsRegistration, function (secureContext) {
// this will fallback to the localCerts if the domain cannot be registered
if (!secureContext) {
var localCerts = require('localhost.daplie.com-certificates');
secureContext = localCerts;
}
cb(null, secureContext);
}, function (err) {
cb(err);
});
}
letsencrypt.create(
'/home/user/.local/share/letsencrypt/bin/letsencrypt'
// set some defaults // set some defaults
, { configDir: '/etc/letsencrypt' , { configDir: config.configDir
, workDir: '/var/lib/letsencrypt' , workDir: config.workDir
, logsDir: '/var/log/letsencrypt' , logsDir: config.logsDir
, standalone: true
//, webroot: true , webroot: true
//, webrootPath: '/srv/www/acme-challenges/' , webrootPath: config.webrootPath
, server: LEP.stagingServer
} }
, { cacheContextsFor: 1 * 60 * 60 * 1000 // 1 hour , { cacheContextsFor: 1 * 60 * 60 * 1000 // 1 hour
, cacheRenewChecksFor: 3 * 24 * 60 * 60 * 1000 // 3 days , cacheRenewChecksFor: 3 * 24 * 60 * 60 * 1000 // 3 days
} }
).then(function (le) { );
getSecureContext(le, 'example.com', function (secureContext) {
console.log(secureContext); le.register({
}); agreeTos: true
, domains: ['lds.io']
, email: 'coolaj86@gmail.com'
}); });