mirror of
				https://github.com/therootcompany/greenlock-express.js.git
				synced 2024-11-16 17:28:59 +00:00 
			
		
		
		
	untested separation of concerns
This commit is contained in:
		
							parent
							
								
									8d27e09217
								
							
						
					
					
						commit
						d1375aceb0
					
				
							
								
								
									
										135
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								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('<!-- Hello Mr Developer! Please use HTTPS instead -->'); | ||||
| })); | ||||
| 
 | ||||
| 
 | ||||
| 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('<!-- Hello Mr Developer! Please use HTTPS instead -->'); | ||||
| })); | ||||
| 
 | ||||
| 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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 }; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user