diff --git a/README.md b/README.md index 922feb1..ee1efd9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ Free SSL and managed or automatic HTTPS for node.js with Express, Connect, and other middleware systems. - ## Install ``` @@ -95,7 +94,7 @@ node -e 'require("letsencrypt-express").testing().create( require("express")().u 'use strict'; // Note: using staging server url, remove .testing() for production -var lex = require('letsencrypt-express').testing(); +var LEX = require('letsencrypt-express').testing(); var express = require('express'); var app = express(); @@ -103,22 +102,22 @@ app.use('/', function (req, res) { res.send({ success: true }); }); -lex.create({ +LEX.create({ configDir: './letsencrypt.config' // ~/letsencrypt, /etc/letsencrypt, whatever you want - + , onRequest: app // your express app (or plain node http app) , letsencrypt: null // you can provide you own instance of letsencrypt // if you need to configure it (with an agreeToTerms // callback, for example) - + , approveRegistration: function (hostname, cb) { // PRODUCTION MODE needs this function, but only if you want // automatic registration (usually not necessary) // renewals for registered domains will still be automatic cb(null, { domains: [hostname] , email: 'user@example.com' - , agreeTos: true // you + , agreeTos: true // you }); } }).listen([80], [443, 5001], function () { @@ -164,6 +163,42 @@ console.log(results.plainServers); console.log(results.tlsServers); ``` +### Use with raw http / https modules + +Let's say you want to redirect all http to https. + +``` +var http = require('http'); +var https = require('https'); +var LEX = require('letsencrypt-express'); +var LE = require('letsencrypt'); + +var lex = LEX.create({ + configDir: __dirname + '/letsencrypt.config' +, approveRegistration: function (hostname, cb) { + cb(null, { + domains: [hostname] + , email: 'user@example.com' + , agreeTos: true + }); + } +}); + +http.createServer(LEX.createAcmeResponder(lex, function redirectHttps(req, res) { + res.setHeader('Location', 'https://' + req.headers.host + req.url); + res.end(''); +})); + + +var app = require('express')(); + +app.use('/', function (req, res) { + res.end('Hello!'); +}); + +https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app)); +``` + ### WebSockets with Let's Encrypt Note: you don't need to create websockets for the plain ports. @@ -180,7 +215,7 @@ function onConnection(ws) { var location = url.parse(ws.upgradeReq.url, true); // you might use location.query.access_token to authenticate or share sessions // or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312) - + ws.on('message', function incoming(message) { console.log('received: %s', message); }); @@ -229,7 +264,20 @@ LEX.createSniCallback(opts) // this will call letsencrypt.renew and letsencr // uses `opts.webrootPath` to read from the filesystem -LEX.getChallenge(opts, hostname, key cb) +LEX.getChallenge(opts, hostname, key cb) + +LEX.createAcmeResponder(opts, fn) // this will return the necessary request handler for /.well-known/acme-challenges + // which then calls `fn` (such as express app) to complete the request + // + // opts lex instance created with LEX.create(opts) + // more generally, any object with a compatible `getChallenge` will work: + // `lex.getChallenge(opts, domain, key, function (err, val) {})` + // + // fn function (req, res) { + // console.log(req.method, req.url); + // + // res.end('Hello!'); + // } ``` ## Options @@ -286,6 +334,77 @@ server: url // url use letsencrypt.productionServerUr // default production ``` +### Fullest Example Ever + +Here's absolutely every option and function exposed + +``` +var http = require('http'); +var https = require('https'); +var LEX = require('letsencrypt-express'); +var LE = require('letsencrypt'); +var lex; + +lex = LEX.create({ + webrootPath: '/tmp/.well-known/acme-challenge' + +, lifetime: 90 * 24 * 60 * 60 * 1000 // expect certificates to last 90 days +, failedWait: 5 * 60 * 1000 // if registering fails wait 5 minutes before trying again +, renewWithin: 3 * 24 * 60 * 60 * 1000 // renew at least 3 days before expiration +, memorizeFor: 1 * 24 * 60 * 60 * 1000 // keep certificates in memory for 1 day + +, approveRegistration: function (hostname, cb) { + cb(null, { + domains: [hostname] + , email: 'user@example.com' + , agreeTos: true + }); + } + +, handleRenewFailure: function (err, hostname, certInfo) { + console.error("ERROR: Failed to renew domain '", hostname, "':"); + if (err) { + console.error(err.stack || err); + } + if (certInfo) { + console.error(certInfo); + } + } + +, letsencrypt: LE.create( + // options + { configDir: './letsencrypt.config' + + , server: LE.productionServerUrl + , privkeyPath: LE.privkeyPath + , fullchainPath: LE.fullchainPath + , certPath: LE.certPath + , chainPath: LE.chainPath + , renewalPath: LE.renewalPath + , accountsDir: LE.accountsDir + + , debug: false + } + + // handlers + , { setChallenge: LEX.setChallenge + , removeChallenge: LEX.removeChallenge + } + ) + +, debug: false +}); + +http.createServer(LEX.createAcmeResponder(lex, function (req, res) { + res.setHeader('Location', 'https://' + req.headers.host + req.url); + res.end(''); +})); + +https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, function (req, res) { + res.end('Hello!'); +})); +``` + ## Heroku? This doesn't work on heroku because heroku uses a proxy with built-in https diff --git a/bin/lex.js b/bin/lex.js index 888ba3b..0821a5e 100755 --- a/bin/lex.js +++ b/bin/lex.js @@ -151,7 +151,7 @@ cli.main(function(_, options) { function startServers() { // Note: using staging server url, remove .testing() for production var LE = require('letsencrypt'); - var challengeStore = require('../lib/challenge-handlers'); + var LEX = require('../'); var le = LE.create({ configDir: configDir , manual: true @@ -163,10 +163,9 @@ cli.main(function(_, options) { , renewalPath: LE.renewalPath , accountsDir: LE.accountsDir }, { - setChallenge: challengeStore.set - , removeChallenge: challengeStore.remove + setChallenge: LEX.setChallenge + , removeChallenge: LEX.removeChallenge }); - var lex = require('../'); var app = express(); var vhosts = {}; @@ -192,7 +191,7 @@ cli.main(function(_, options) { }); app.use('/', express.static(path.join(__dirname, '..', 'lib', 'public'))); - lex.create({ + LEX.create({ onRequest: app , configDir: configDir , letsencrypt: le diff --git a/lib/sni-callback.js b/lib/sni-callback.js index 7aea857..4b40c14 100644 --- a/lib/sni-callback.js +++ b/lib/sni-callback.js @@ -4,12 +4,23 @@ var crypto = require('crypto'); var tls = require('tls'); module.exports.create = function (opts) { + var ipc = {}; // in-process cache + + // function (/*err, hostname, certInfo*/) {} + function handleRenewFailure(err, hostname, certInfo) { + console.error("ERROR: Failed to renew domain '", hostname, "':"); + if (err) { + console.error(err.stack || err); + } + if (certInfo) { + console.error(certInfo); + } + } + + if (!opts) { throw new Error("requires opts to be an object"); } if (opts.debug) { console.debug("[LEX] creating sniCallback", JSON.stringify(opts, null, ' ')); } - var ipc = {}; // in-process cache - - if (!opts) { throw new Error("requires opts to be an object"); } if (!opts.letsencrypt) { throw new Error("requires opts.letsencrypt to be a letsencrypt instance"); } if (!opts.lifetime) { opts.lifetime = 90 * 24 * 60 * 60 * 1000; } @@ -19,7 +30,7 @@ module.exports.create = function (opts) { if (!opts.approveRegistration) { opts.approveRegistration = function (hostname, cb) { cb(null, null); }; } //opts.approveRegistration = function (hostname, cb) { cb(null, null); }; - if (!opts.handleRenewFailure) { opts.handleRenewFailure = function (/*err, hostname, certInfo*/) {}; } + if (!opts.handleRenewFailure) { opts.handleRenewFailure = handleRenewFailure; } function assignBestByDates(now, certInfo) { certInfo = certInfo || { loadedAt: now, expiresAt: 0, issuedAt: 0, lifetime: 0 }; diff --git a/lib/standalone.js b/lib/standalone.js index 1fa2373..20ddc88 100644 --- a/lib/standalone.js +++ b/lib/standalone.js @@ -5,6 +5,39 @@ var challengeStore = require('./challenge-handlers'); var createSniCallback = require('./sni-callback').create; var LE = require('letsencrypt'); +function createAcmeResponder(obj, onRequest) { + + function httpAcmeResponder(req, res) { + if (LEX.debug) { + console.debug('[LEX] ', req.method, req.headers.host, req.url); + } + var acmeChallengePrefix = '/.well-known/acme-challenge/'; + + if (0 !== req.url.indexOf(acmeChallengePrefix)) { + onRequest(req, res); + return; + } + + var key = req.url.slice(acmeChallengePrefix.length); + + obj.getChallenge({ + debug: LEX.debug || obj.debug + }, req.headers.host, key, function (err, val) { + if (LEX.debug) { + console.debug('[LEX] GET challenge, response:'); + console.debug('challenge:', key); + console.debug('response:', val); + if (err) { + console.debug(err.stack); + } + } + res.end(val || '_'); + }); + } + + return httpAcmeResponder; +} + function lexHelper(obj, app) { var defaultPems = require('localhost.daplie.com-certificates'); @@ -32,19 +65,14 @@ function lexHelper(obj, app) { if (!obj.getChallenge) { if (false !== obj.getChallenge) { - obj.getChallenge = challengeStore.get; + obj.getChallenge = LEX.getChallenge; } if (!obj.webrootPath) { obj.webrootPath = path.join(require('os').tmpdir(), 'acme-challenge'); } } - if (!obj.onRequest && false !== obj.onRequest) { - console.warn("You should either do args.onRequest = app or server.on('request', app)," - + " otherwise only acme-challenge requests will be handled (and the rest will hang)"); - console.warn("You can silence this warning by setting args.onRequest = false"); - } - + // BEGIN LetsEncrypt options if (!obj.configDir) { obj.configDir = path.join(require('homedir')(), '/letsencrypt/etc'); } @@ -60,17 +88,21 @@ function lexHelper(obj, app) { if (!obj.chainPath) { obj.chainPath = ':config/live/:hostname/chain.pem'; } - if (!obj.server) { obj.server = LEX.defaultServerUrl; } + // END LetsEncrypt options + + obj.getChallenge = obj.getChallenge || LEX.getChallenge; + obj.setChallenge = obj.setChallenge || LEX.setChallenge; + obj.removeChallenge = obj.removeChallenge || LEX.removeChallenge; if (!obj.letsencrypt) { //LE.merge(obj, ); // { configDir, webrootPath, server } obj.letsencrypt = LE.create(obj, { - setChallenge: challengeStore.set - , removeChallenge: challengeStore.remove + setChallenge: obj.setChallenge + , removeChallenge: obj.removeChallenge }); } @@ -132,39 +164,6 @@ function lexHelper(obj, app) { httpsOptions.SNICallback = createSniCallback(obj); } - function createAcmeResponder(onRequest) { - - function httpAcmeResponder(req, res) { - if (LEX.debug) { - console.debug('[LEX] ', req.method, req.headers.host, req.url); - } - var acmeChallengePrefix = '/.well-known/acme-challenge/'; - - if (0 !== req.url.indexOf(acmeChallengePrefix)) { - onRequest(req, res); - return; - } - - var key = req.url.slice(acmeChallengePrefix.length); - - obj.getChallenge(obj, req.headers.host, key, function (err, val) { - if (LEX.debug) { - console.debug('[LEX] GET challenge, response:'); - console.debug('challenge:', key); - console.debug('response:', val); - if (err) { - console.debug(err.stack); - } - } - res.end(val || '_'); - }); - } - - return httpAcmeResponder; - } - - obj.httpAcmeResponder = createAcmeResponder(obj.onHttpRequest||obj.onRequest); - obj.httpsAcmeResponder = createAcmeResponder(obj.onHttpsRequest||obj.onRequest); obj.httpsOptions = httpsOptions; return obj; @@ -175,6 +174,14 @@ function LEX(obj, app) { var http = require('http'); function listen(plainPorts, tlsPorts, onListening) { + if (!(obj.onRequest || (obj.onHttpRequest && obj.onHttpsRequest)) && false !== obj.onRequest) { + console.warn("You should either do args.onRequest = app or server.on('request', app)," + + " otherwise only acme-challenge requests will be handled (and the rest will hang)"); + console.warn("You can silence this warning by setting args.onRequest = false"); + } + obj.httpAcmeResponder = createAcmeResponder(obj, obj.onHttpRequest || obj.onRequest); + obj.httpsAcmeResponder = createAcmeResponder(obj, obj.onHttpsRequest || obj.onRequest); + if (plainPorts && (!Array.isArray(plainPorts) || !Array.isArray(tlsPorts))) { throw new Error(".listen() must be used with plain and tls port arrays, like this: `.listen([80], [443, 5001], function () {})`"); } @@ -293,6 +300,8 @@ LEX.middleware = function (defaults) { LEX.stagingServerUrl = LE.stagingServerUrl; LEX.productionServerUrl = LE.productionServerUrl || LE.liveServerUrl; LEX.defaultServerUrl = LEX.productionServerUrl; +LEX.createAcmeResponder = createAcmeResponder; +LEX.normalizeOptions = lexHelper; LEX.testing = function () { LEX.debug = true; LEX.defaultServerUrl = LEX.stagingServerUrl;