| 
									
										
										
										
											2019-10-26 23:52:19 -06:00
										 |  |  | "use strict"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var sni = module.exports; | 
					
						
							|  |  |  | var tls = require("tls"); | 
					
						
							|  |  |  | var servernameRe = /^[a-z0-9\.\-]+$/i; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // a nice, round, irrational number - about every 6¼ hours
 | 
					
						
							|  |  |  | var refreshOffset = Math.round(Math.PI * 2 * (60 * 60 * 1000)); | 
					
						
							|  |  |  | // and another, about 15 minutes
 | 
					
						
							|  |  |  | var refreshStagger = Math.round(Math.PI * 5 * (60 * 1000)); | 
					
						
							|  |  |  | // and another, about 30 seconds
 | 
					
						
							|  |  |  | var smallStagger = Math.round(Math.PI * (30 * 1000)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-28 03:43:42 -06:00
										 |  |  | //secureOpts.SNICallback = sni.create(greenlock, secureOpts);
 | 
					
						
							|  |  |  | sni.create = function(greenlock, secureOpts) { | 
					
						
							| 
									
										
										
										
											2019-11-01 15:14:07 -06:00
										 |  |  |     var _cache = {}; | 
					
						
							|  |  |  |     var defaultServername = greenlock.servername || ""; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (secureOpts.cert) { | 
					
						
							|  |  |  |         // Note: it's fine if greenlock.servername is undefined,
 | 
					
						
							|  |  |  |         // but if the caller wants this to auto-renew, they should define it
 | 
					
						
							|  |  |  |         _cache[defaultServername] = { | 
					
						
							|  |  |  |             refreshAt: 0, | 
					
						
							|  |  |  |             secureContext: tls.createSecureContext(secureOpts) | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return getSecureContext; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function notify(ev, args) { | 
					
						
							|  |  |  |         try { | 
					
						
							|  |  |  |             // TODO _notify() or notify()?
 | 
					
						
							|  |  |  |             (greenlock.notify || greenlock._notify)(ev, args); | 
					
						
							|  |  |  |         } catch (e) { | 
					
						
							|  |  |  |             console.error(e); | 
					
						
							|  |  |  |             console.error(ev, args); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function getSecureContext(servername, cb) { | 
					
						
							|  |  |  |         //console.log("debug sni", servername);
 | 
					
						
							|  |  |  |         if ("string" !== typeof servername) { | 
					
						
							|  |  |  |             // this will never happen... right? but stranger things have...
 | 
					
						
							|  |  |  |             console.error("[sanity fail] non-string servername:", servername); | 
					
						
							|  |  |  |             cb(new Error("invalid servername"), null); | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var secureContext = getCachedContext(servername); | 
					
						
							|  |  |  |         if (secureContext) { | 
					
						
							|  |  |  |             //console.log("debug sni got cached context", servername, getCachedMeta(servername));
 | 
					
						
							|  |  |  |             cb(null, secureContext); | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         getFreshContext(servername) | 
					
						
							|  |  |  |             .then(function(secureContext) { | 
					
						
							|  |  |  |                 if (secureContext) { | 
					
						
							|  |  |  |                     //console.log("debug sni got fresh context", servername, getCachedMeta(servername));
 | 
					
						
							|  |  |  |                     cb(null, secureContext); | 
					
						
							|  |  |  |                     return; | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2019-11-02 20:54:09 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-01 15:14:07 -06:00
										 |  |  |                 // Note: this does not replace tlsSocket.setSecureContext()
 | 
					
						
							|  |  |  |                 // as it only works when SNI has been sent
 | 
					
						
							|  |  |  |                 //console.log("debug sni got default context", servername, getCachedMeta(servername));
 | 
					
						
							| 
									
										
										
										
											2019-11-02 20:54:09 -06:00
										 |  |  |                 if (!/PROD/.test(process.env.ENV) || /DEV|STAG/.test(process.env.ENV)) { | 
					
						
							|  |  |  |                     // Change this once
 | 
					
						
							|  |  |  |                     // A) the 'notify' message passing is verified fixed in cluster mode
 | 
					
						
							|  |  |  |                     // B) we have a good way to let people know their server isn't configured
 | 
					
						
							|  |  |  |                     console.debug("debug: ignoring servername " + JSON.stringify(servername)); | 
					
						
							|  |  |  |                     console.debug("       (it's probably either missing from your config, or a bot)"); | 
					
						
							|  |  |  |                     notify("servername_unknown", { | 
					
						
							|  |  |  |                         servername: servername | 
					
						
							|  |  |  |                     }); | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2019-11-01 15:14:07 -06:00
										 |  |  |                 cb(null, getDefaultContext()); | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  |             .catch(function(err) { | 
					
						
							|  |  |  |                 if (!err.context) { | 
					
						
							|  |  |  |                     err.context = "sni_callback"; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 notify("error", err); | 
					
						
							|  |  |  |                 //console.log("debug sni error", servername, err);
 | 
					
						
							|  |  |  |                 cb(err); | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function getCachedMeta(servername) { | 
					
						
							|  |  |  |         var meta = _cache[servername]; | 
					
						
							|  |  |  |         if (!meta) { | 
					
						
							|  |  |  |             if (!_cache[wildname(servername)]) { | 
					
						
							|  |  |  |                 return null; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return meta; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function getCachedContext(servername) { | 
					
						
							|  |  |  |         var meta = getCachedMeta(servername); | 
					
						
							|  |  |  |         if (!meta) { | 
					
						
							|  |  |  |             return null; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // always renew in background
 | 
					
						
							|  |  |  |         if (!meta.refreshAt || Date.now() >= meta.refreshAt) { | 
					
						
							|  |  |  |             getFreshContext(servername).catch(function(e) { | 
					
						
							|  |  |  |                 if (!e.context) { | 
					
						
							|  |  |  |                     e.context = "sni_background_refresh"; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 notify("error", e); | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // under normal circumstances this would never be expired
 | 
					
						
							|  |  |  |         // and, if it is expired, something is so wrong it's probably
 | 
					
						
							|  |  |  |         // not worth wating for the renewal - it has probably failed
 | 
					
						
							|  |  |  |         return meta.secureContext; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function getFreshContext(servername) { | 
					
						
							|  |  |  |         var meta = getCachedMeta(servername); | 
					
						
							|  |  |  |         if (!meta && !validServername(servername)) { | 
					
						
							| 
									
										
										
										
											2019-11-02 20:54:09 -06:00
										 |  |  |             if ((servername && !/PROD/.test(process.env.ENV)) || /DEV|STAG/.test(process.env.ENV)) { | 
					
						
							|  |  |  |                 // Change this once
 | 
					
						
							|  |  |  |                 // A) the 'notify' message passing is verified fixed in cluster mode
 | 
					
						
							|  |  |  |                 // B) we have a good way to let people know their server isn't configured
 | 
					
						
							|  |  |  |                 console.debug("debug: invalid servername " + JSON.stringify(servername)); | 
					
						
							|  |  |  |                 console.debug("       (it's probably just a bot trolling for vulnerable servers)"); | 
					
						
							|  |  |  |                 notify("servername_invalid", { | 
					
						
							|  |  |  |                     servername: servername | 
					
						
							|  |  |  |                 }); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2019-11-01 15:14:07 -06:00
										 |  |  |             return Promise.resolve(null); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (meta) { | 
					
						
							|  |  |  |             // prevent stampedes
 | 
					
						
							|  |  |  |             meta.refreshAt = Date.now() + randomRefreshOffset(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // TODO don't get unknown certs at all, rely on auto-updates from greenlock
 | 
					
						
							|  |  |  |         // Note: greenlock.get() will return an existing fresh cert or issue a new one
 | 
					
						
							|  |  |  |         return greenlock.get({ servername: servername }).then(function(result) { | 
					
						
							|  |  |  |             var meta = getCachedMeta(servername); | 
					
						
							|  |  |  |             if (!meta) { | 
					
						
							|  |  |  |                 meta = _cache[servername] = { secureContext: { _valid: false } }; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             // prevent from being punked by bot trolls
 | 
					
						
							|  |  |  |             meta.refreshAt = Date.now() + smallStagger; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // nothing to do
 | 
					
						
							|  |  |  |             if (!result) { | 
					
						
							|  |  |  |                 return null; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // we only care about the first one
 | 
					
						
							|  |  |  |             var pems = result.pems; | 
					
						
							|  |  |  |             var site = result.site; | 
					
						
							|  |  |  |             if (!pems || !pems.cert) { | 
					
						
							|  |  |  |                 // nothing to do
 | 
					
						
							|  |  |  |                 // (and the error should have been reported already)
 | 
					
						
							|  |  |  |                 return null; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             meta = { | 
					
						
							|  |  |  |                 refreshAt: Date.now() + randomRefreshOffset(), | 
					
						
							|  |  |  |                 secureContext: tls.createSecureContext({ | 
					
						
							|  |  |  |                     // TODO support passphrase-protected privkeys
 | 
					
						
							|  |  |  |                     key: pems.privkey, | 
					
						
							|  |  |  |                     cert: pems.cert + "\n" + pems.chain + "\n" | 
					
						
							|  |  |  |                 }) | 
					
						
							|  |  |  |             }; | 
					
						
							|  |  |  |             meta.secureContext._valid = true; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // copy this same object into every place
 | 
					
						
							|  |  |  |             (result.altnames || site.altnames || [result.subject || site.subject]).forEach(function(altname) { | 
					
						
							|  |  |  |                 _cache[altname] = meta; | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return meta.secureContext; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function getDefaultContext() { | 
					
						
							|  |  |  |         return getCachedContext(defaultServername); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-10-26 23:52:19 -06:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // whenever we need to know when to refresh next
 | 
					
						
							|  |  |  | function randomRefreshOffset() { | 
					
						
							| 
									
										
										
										
											2019-11-01 15:14:07 -06:00
										 |  |  |     var stagger = Math.round(refreshStagger / 2) - Math.round(Math.random() * refreshStagger); | 
					
						
							|  |  |  |     return refreshOffset + stagger; | 
					
						
							| 
									
										
										
										
											2019-10-26 23:52:19 -06:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function validServername(servername) { | 
					
						
							| 
									
										
										
										
											2019-11-01 15:14:07 -06:00
										 |  |  |     // format and (lightly) sanitize sni so that users can be naive
 | 
					
						
							|  |  |  |     // and not have to worry about SQL injection or fs discovery
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     servername = (servername || "").toLowerCase(); | 
					
						
							|  |  |  |     // hostname labels allow a-z, 0-9, -, and are separated by dots
 | 
					
						
							|  |  |  |     // _ is sometimes allowed, but not as a "hostname", and not by Let's Encrypt ACME
 | 
					
						
							|  |  |  |     // REGEX // https://www.codeproject.com/Questions/1063023/alphanumeric-validation-javascript-without-regex
 | 
					
						
							|  |  |  |     return servernameRe.test(servername) && -1 === servername.indexOf(".."); | 
					
						
							| 
									
										
										
										
											2019-10-26 23:52:19 -06:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function wildname(servername) { | 
					
						
							| 
									
										
										
										
											2019-11-01 15:14:07 -06:00
										 |  |  |     return ( | 
					
						
							|  |  |  |         "*." + | 
					
						
							|  |  |  |         servername | 
					
						
							|  |  |  |             .split(".") | 
					
						
							|  |  |  |             .slice(1) | 
					
						
							|  |  |  |             .join(".") | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2019-10-26 23:52:19 -06:00
										 |  |  | } |