From 88406b9c0b62c0d473048505972c17f39a0b077f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 12 Dec 2015 14:20:12 +0000 Subject: [PATCH] updates --- bin/standalone.js | 11 +- index.js | 242 ++++++++++++++++++++++++++--------------- package.json | 2 +- tests/le-standalone.js | 89 +++++---------- 4 files changed, 186 insertions(+), 158 deletions(-) diff --git a/bin/standalone.js b/bin/standalone.js index 764e352..af6c094 100644 --- a/bin/standalone.js +++ b/bin/standalone.js @@ -1,7 +1,6 @@ 'use strict'; -var homedir = require('homedir'); -var leBinPath = homedir() + '/.local/share/letsencrypt/bin/letsencrypt'; +var leBinPath = require('homedir')() + '/.local/share/letsencrypt/bin/letsencrypt'; var lep = require('letsencrypt-python').create(leBinPath); var conf = { domains: process.argv[2] @@ -21,13 +20,13 @@ var bkDefaults = { , workDir: '/var/lib/letsencrypt' , text: true }; -var le = require('letsencrypt').create(lep, bkDefaults); +var le = require('../').create(lep, bkDefaults); var localCerts = require('localhost.daplie.com-certificates'); var express = require('express'); var app = express(); -app.use(le.middleware); +app.use(le.middleware()); var server = require('http').createServer(); server.on('request', app); @@ -38,14 +37,14 @@ server.listen(80, function () { var tlsServer = require('https').createServer({ key: localCerts.key , cert: localCerts.cert -, SNICallback: le.SNICallback +, SNICallback: le.sniCallback }); tlsServer.on('request', app); tlsServer.listen(443, function () { console.log('Listening http', tlsServer.address()); }); -le.register('certonly', { +le.register({ agreeTos: 'agree' === conf.agree , domains: conf.domains.split(',') , email: conf.email diff --git a/index.js b/index.js index ea37ae9..4787645 100644 --- a/index.js +++ b/index.js @@ -1,120 +1,186 @@ 'use strict'; -module.exports.create = function (lebinpath, defaults, options) { +module.exports.create = function (letsencrypt, defaults, options) { var PromiseA = require('bluebird'); var tls = require('tls'); 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 ipc = {}; // in-process cache var count = 0; - //var certTpl = "/live/:hostname/cert.pem"; - var certTpl = "/live/:hostname/fullchain.pem"; - var privTpl = "/live/:hostname/privkey.pem"; + var now; + var le; options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000); defaults.webroot = true; - defaults.webrootPath = '/srv/www/acme-challenge'; - return letsencrypt.optsAsync(lebinpath).then(function (keys) { - var now; - var le; + 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]; + }); + } - le = { - validate: function () { + function sniCallback(hostname, cb) { + 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([ - fs.readFileAsync(privpath, 'ascii') - , fs.readFileAsync(crtpath, 'ascii') - // stat the file, not the link - , fs.statAsync(crtpath, 'ascii') - ]).then(function (arr) { - - - return arr; + if (!cache.context) { + cache.context = tls.createSecureContext({ + key: cache.key // privkey.pem + , cert: cache.cert // fullchain.pem + //, ciphers // node's defaults are great }); } - , 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() - }; + + cb(null, cache.context); + }); + } - return ipc[hostname]; - } - , readAndCacheCerts: function (hostname) { - return le.readCerts(hostname).then(function (certs) { - return le.cacheCerts(hostname, certs); - }); - } - , get: function (hostname, args, opts, cb) { - count += 1; + le = { + validate: function () { + } + , middleware: function () { + var serveStatic = require('serve-static')(defaults.webrootPath); + var prefix = '/.well-known/acme-challenge/'; - 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 function (req, res, next) { + if (0 === req.url.indexOf(prefix)) { + next(); return; } - return le.readCerts(hostname).then(function (cached) { - cb(null, cached.context); - }, function (/*err*/) { - var copy = {}; - var arr; + var pathname = req.url; + req.url = req.url.substr(prefix.length - 1); + serveStatic(req, res, function (err) { + req.url = pathname; + 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 - Object.keys(defaults).forEach(function (key) { - copy[key] = defaults[key]; - }); - Object.keys(args).forEach(function (key) { - copy[key] = args[key]; - }); + 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 - arr = letsencrypt.objToArr(keys, copy); - // 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())); + var copy = merge(args); - // wasn't successful with lets encrypt, don't try again for n minutes - ipc[hostname] = { - context: null - , updated: Date.now() - }; - cb(null, ipc[hostname]); - }); + if (!utils.isValidDomain(args.domains[0])) { + return PromiseA.reject({ + message: "invalid domain" + , code: "INVALID_DOMAIN" }); } - }; - 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; }; diff --git a/package.json b/package.json index 343685c..592dca3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "letsencrypt", "version": "1.0.0", "description": "Let's Encrypt for node.js on npm", - "main": "le.js", + "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/tests/le-standalone.js b/tests/le-standalone.js index 076452c..f1db4d6 100644 --- a/tests/le-standalone.js +++ b/tests/le-standalone.js @@ -1,74 +1,37 @@ 'use strict'; -var letsencrypt = require('letsencrypt'); -var networkInterfaces = require('os').networkInterfaces(); -var ipify = require('ipify'); +var config = require('./config'); +var Letsencrypt = require('../'); +var leBinPath = '/home/user/.local/share/letsencrypt/bin/letsencrypt'; +var LEP = require('letsencrypt-python'); +var lep = LEP.create(leBinPath); -function getSecureContext(le, hostname, cb) { - hostname = hostname.replace(/^www\./, ''); +require('./serve-acme-challenges').create({ + configDir: config.configDir +}); - function needsRegistration(hostnames, cb) { - // - // 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; - } +//var networkInterfaces = require('os').networkInterfaces(); +//var ipify = require('ipify'); - // these hostnames need to be registered - // - 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' +var le = Letsencrypt.create( + lep // set some defaults -, { configDir: '/etc/letsencrypt' - , workDir: '/var/lib/letsencrypt' - , logsDir: '/var/log/letsencrypt' - , standalone: true - //, webroot: true - //, webrootPath: '/srv/www/acme-challenges/' +, { configDir: config.configDir + , workDir: config.workDir + , logsDir: config.logsDir + + , webroot: true + , webrootPath: config.webrootPath + + , server: LEP.stagingServer } , { cacheContextsFor: 1 * 60 * 60 * 1000 // 1 hour , 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' });