sni callback stuff
This commit is contained in:
		
							parent
							
								
									3132e7a592
								
							
						
					
					
						commit
						75d259dbb1
					
				
							
								
								
									
										23
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								README.md
									
									
									
									
									
								
							| @ -24,7 +24,7 @@ npm install --save letsencrypt-express | ||||
| 'use strict'; | ||||
| 
 | ||||
| // Note: using staging server url, remove .testing() for production | ||||
| var le = require('letsencrypt-express').testing(); | ||||
| var lex = require('letsencrypt-express').testing(); | ||||
| var express = require('express'); | ||||
| var app = express(); | ||||
| 
 | ||||
| @ -32,7 +32,7 @@ app.use('/', function (req, res) { | ||||
|   res.send({ success: true }); | ||||
| }); | ||||
| 
 | ||||
| le.create('/etc/letsencrypt', app).listen([80], [443, 5001], function () { | ||||
| lex.create('/etc/letsencrypt', app).listen([80], [443, 5001], function () { | ||||
|   console.log("ENCRYPT __ALL__ THE DOMAINS!"); | ||||
| }); | ||||
| ``` | ||||
| @ -42,7 +42,7 @@ le.create('/etc/letsencrypt', app).listen([80], [443, 5001], function () { | ||||
| ```javascript | ||||
| 'use strict'; | ||||
| 
 | ||||
| var le = require('letsencrypt-express'); | ||||
| var lex = require('letsencrypt-express'); | ||||
| var express = require('express'); | ||||
| var app = express(); | ||||
| 
 | ||||
| @ -50,7 +50,7 @@ app.use('/', function (req, res) { | ||||
|   res.send({ success: true }); | ||||
| }); | ||||
| 
 | ||||
| var results = le.create({ | ||||
| var results = lex.create({ | ||||
|   configDir: '/etc/letsencrypt' | ||||
| , onRequest: app | ||||
| , server: require('letsencrypt').productionServerUrl | ||||
| @ -84,6 +84,21 @@ results.tlsServers.forEach(function (server) { | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ## API | ||||
| 
 | ||||
| ``` | ||||
| LEX.create(options)             // checks options and sets up defaults. returns object with `listen` | ||||
|                                 // (it was really just done this way to appeal to what people are used to seeing) | ||||
| 
 | ||||
|   lex.listen(plain, tls, fn)    // actually creates the servers and causes them to listen | ||||
| 
 | ||||
| LEX.createSniCallback(le)       // receives an instance of letsencrypt, returns an SNICallback handler for https.createServer() | ||||
| 
 | ||||
| 
 | ||||
| LEX.getChallenge(opts, hostname, key cb)  // uses `opts.webrootPath` to read from the filesystem | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| ## Options | ||||
| 
 | ||||
| If any of these values are `undefined` or `null` the will assume use reasonable defaults. | ||||
|  | ||||
							
								
								
									
										128
									
								
								lib/sni-callback.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								lib/sni-callback.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,128 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var crypto = require('crypto'); | ||||
| var tls = require('tls'); | ||||
| 
 | ||||
| module.exports.create = function (memos) { | ||||
|   var ipc = {}; // in-process cache
 | ||||
| 
 | ||||
|   if (!memos) { throw new Error("requires opts to be an object"); } | ||||
|   if (!memos.letsencrypt) { throw new Error("requires opts.letsencrypt to be a letsencrypt instance"); } | ||||
| 
 | ||||
|   if (!memos.lifetime) { memos.lifetime = 90 * 24 * 60 * 60 * 1000; } | ||||
|   if (!memos.failedWait) { memos.failedWait = 5 * 60 * 1000; } | ||||
|   if (!memos.renewWithin) { memos.renewWithin = 3 * 24 * 60 * 60 * 1000; } | ||||
|   if (!memos.memorizeFor) { memos.memorizeFor = 1 * 24 * 60 * 60 * 1000; } | ||||
| 
 | ||||
|   if (!memos.handleRegistration) { memos.handleRegistration = function (args, cb) { cb(null, null); }; } | ||||
|   if (!memos.handleRenewFailure) { memos.handleRenewFailure = function () {}; } | ||||
| 
 | ||||
|   function assignBestByDates(now, certInfo) { | ||||
|     certInfo = certInfo || { loadedAt: now, expiresAt: 0, issuedAt: 0, lifetime: 0 }; | ||||
| 
 | ||||
|     var rnds = crypto.randomBytes(3)[0]; | ||||
|     var rnd1 = ((rnds[0] + 1) / 257); | ||||
|     var rnd2 = ((rnds[1] + 1) / 257); | ||||
|     var rnd3 = ((rnds[2] + 1) / 257); | ||||
| 
 | ||||
|     // Stagger randomly by plus 0% to 25% to prevent all caches expiring at once
 | ||||
|     var memorizeFor = Math.floor(memos.memorizeFor + ((memos.memorizeFor / 4) * rnd1)); | ||||
|     // Stagger randomly to renew between n and 2n days before renewal is due
 | ||||
|     // this *greatly* reduces the risk of multiple cluster processes renewing the same domain at once
 | ||||
|     var bestIfUsedBy = certInfo.expiresAt - (memos.renewWithin + Math.floor(memos.renewWithin * rnd2)); | ||||
|     // Stagger randomly by plus 0 to 5 min to reduce risk of multiple cluster processes
 | ||||
|     // renewing at once on boot when the certs have expired
 | ||||
|     var renewTimeout = Math.floor((5 * 60 * 1000) * rnd3); | ||||
| 
 | ||||
|     certInfo.loadedAt = now; | ||||
|     certInfo.memorizeFor = memorizeFor; | ||||
|     certInfo.bestIfUsedBy = bestIfUsedBy; | ||||
|     certInfo.renewTimeout = renewTimeout; | ||||
|   } | ||||
| 
 | ||||
|   function renewInBackground(now, hostname, certInfo) { | ||||
|     if ((now - certInfo.loadedAt) < memos.failedWait) { | ||||
|       // wait a few minutes
 | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (now > certInfo.bestIfUsedBy && !certInfo.timeout) { | ||||
|       // EXPIRING
 | ||||
|       if (now > certInfo.expiresAt) { | ||||
|         // EXPIRED
 | ||||
|         certInfo.renewTimeout = Math.floor(certInfo.renewTimeout / 2); | ||||
|       } | ||||
| 
 | ||||
|       certInfo.timeout = setTimeout(function () { | ||||
|         var opts = { domains: [ hostname ], duplicate: false }; | ||||
|         le.renew(opts, function (err, certInfo) { | ||||
|           if (err || !certInfo) { | ||||
|             memos.handleRenewFailure(err, certInfo, opts); | ||||
|           } | ||||
|           ipc[hostname] = assignBestByDates(now, certInfo); | ||||
|         }); | ||||
|       }, certInfo.renewTimeout); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function fetch(hostname, cb) { | ||||
|     le.fetch({ domains: [hostname] }, function (err, certInfo) { | ||||
|       var now = Date.now(); | ||||
| 
 | ||||
|       ipc[hostname] = assignBestByDates(now, certInfo); | ||||
|       if (!certInfo) { | ||||
|         // handles registration
 | ||||
|         memos.handleRegistration(hostname, cb); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // handles renewals
 | ||||
|       renewInBackground(now, hostname, certInfo); | ||||
| 
 | ||||
|       if (err) { | ||||
|         cb(err); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       try { | ||||
|         certInfo.tlsContext = tls.createSecureContext({ | ||||
|           key: certInfo.key                             // privkey.pem
 | ||||
|         , cert: certInfo.cert                           // fullchain.pem (cert.pem + '\n' + chain.pem)
 | ||||
|         }); | ||||
|       } catch(e) { | ||||
|         console.warn("[Sanity Check Fail]: a weird object was passed back through le.fetch to lex.fetch"); | ||||
|         cb(e); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       cb(null, certInfo.tlsContext); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   return function sniCallback(hostname, cb) { | ||||
|     var now = Date.now(); | ||||
|     var certInfo = ipc[hostname]; | ||||
| 
 | ||||
|     // TODO once ECDSA is available, wait for cert renewal if its due
 | ||||
|     if (!certInfo) { | ||||
|       fetch(hostname, cb); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (certInfo.context) { | ||||
|       cb(null, certInfo.context); | ||||
| 
 | ||||
|       if ((now - certInfo.loadedAt) < (certInfo.memorizeFor)) { | ||||
|         // these aren't stale, so don't fall through
 | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     else if ((now - certInfo.loadedAt) < memos.failedWait) { | ||||
|       // this was just fetched and failed, wait a few minutes
 | ||||
|       cb(null, null); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     fetch({ domains: [hostname] }, cb); | ||||
|   }; | ||||
| }; | ||||
| @ -2,6 +2,7 @@ | ||||
| 
 | ||||
| var path = require('path'); | ||||
| var challengeStore = require('./lib/challange-handlers'); | ||||
| var createSniCallback = require('./lib/sni-callback').create; | ||||
| var LE = require('letsencrypt'); | ||||
| 
 | ||||
| function LEX(obj, app) { | ||||
| @ -105,6 +106,7 @@ function LEX(obj, app) { | ||||
|     httpsOptions.SNICallback = obj.sniCallback; | ||||
|   } | ||||
|   else if (sniCallback) { | ||||
|     obj._sniCallback = createSniCallback(obj); | ||||
|     httpsOptions.SNICallback = function (domain, cb) { | ||||
|       sniCallback(domain, function (err, context) { | ||||
|         if (context) { | ||||
| @ -112,12 +114,12 @@ function LEX(obj, app) { | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         obj.letsencrypt.sniCallback(domain, cb); | ||||
|         obj._sniCallback(domain, cb); | ||||
|       }); | ||||
|     }; | ||||
|   } | ||||
|   else { | ||||
|     httpsOptions.SNICallback = obj.letsencrypt.sniCallback; | ||||
|     httpsOptions.SNICallback = createSniCallback(obj); | ||||
|   } | ||||
| 
 | ||||
|   function listen(plainPorts, tlsPorts, onListening) { | ||||
| @ -190,6 +192,7 @@ function LEX(obj, app) { | ||||
| } | ||||
| 
 | ||||
| module.exports = LEX; | ||||
| 
 | ||||
| LEX.create = LEX; | ||||
| LEX.setChallenge = challengeStore.set; | ||||
| LEX.getChallenge = challengeStore.get; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user