mirror of
				https://github.com/therootcompany/greenlock-express.js.git
				synced 2024-11-16 17:28:59 +00:00 
			
		
		
		
	wip: API looks good, on to testing
This commit is contained in:
		
							parent
							
								
									9ab7844ea8
								
							
						
					
					
						commit
						0dd3641dc2
					
				
							
								
								
									
										30
									
								
								demo.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								demo.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | var Greenlock = require("./"); | ||||||
|  | var greenlockOptions = { | ||||||
|  | 	cluster: false, | ||||||
|  | 
 | ||||||
|  | 	maintainerEmail: "greenlock-test@rootprojects.org", | ||||||
|  | 	servername: "foo-gl.test.utahrust.com", | ||||||
|  | 	serverId: "bowie.local" | ||||||
|  | 
 | ||||||
|  | 	/* | ||||||
|  |   manager: { | ||||||
|  |     module: "greenlock-manager-sequelize", | ||||||
|  |     dbUrl: "postgres://foo@bar:baz/quux" | ||||||
|  |   } | ||||||
|  |   */ | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Greenlock.create(greenlockOptions) | ||||||
|  | 	.worker(function(glx) { | ||||||
|  | 		console.info(); | ||||||
|  | 		console.info("Hello from worker"); | ||||||
|  | 
 | ||||||
|  | 		glx.serveApp(function(req, res) { | ||||||
|  | 			res.end("Hello, Encrypted World!"); | ||||||
|  | 		}); | ||||||
|  | 	}) | ||||||
|  | 	.master(function() { | ||||||
|  | 		console.log("Hello from master"); | ||||||
|  | 	}); | ||||||
							
								
								
									
										29
									
								
								greenlock-express.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								greenlock-express.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | require("./lib/compat"); | ||||||
|  | 
 | ||||||
|  | // Greenlock Express
 | ||||||
|  | var GLE = module.exports; | ||||||
|  | 
 | ||||||
|  | // opts.approveDomains(options, certs, cb)
 | ||||||
|  | GLE.create = function(opts) { | ||||||
|  | 	if (!opts) { | ||||||
|  | 		opts = {}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// just for ironic humor
 | ||||||
|  | 	["cloudnative", "cloudscale", "webscale", "distributed", "blockchain"].forEach(function(k) { | ||||||
|  | 		if (opts[k]) { | ||||||
|  | 			opts.cluster = true; | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	// we want to be minimal, and only load the code that's necessary to load
 | ||||||
|  | 	if (opts.cluster) { | ||||||
|  | 		if (require("cluster").isMaster) { | ||||||
|  | 			return require("./master.js").create(opts); | ||||||
|  | 		} | ||||||
|  | 		return require("./worker.js").create(opts); | ||||||
|  | 	} | ||||||
|  | 	return require("./single.js").create(opts); | ||||||
|  | }; | ||||||
							
								
								
									
										102
									
								
								http-middleware.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								http-middleware.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,102 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | var HttpMiddleware = module.exports; | ||||||
|  | var servernameRe = /^[a-z0-9\.\-]+$/i; | ||||||
|  | var challengePrefix = "/.well-known/acme-challenge/"; | ||||||
|  | 
 | ||||||
|  | HttpMiddleware.create = function(gl, defaultApp) { | ||||||
|  | 	if (defaultApp && "function" !== typeof defaultApp) { | ||||||
|  | 		throw new Error("use greenlock.httpMiddleware() or greenlock.httpMiddleware(function (req, res) {})"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return function(req, res, next) { | ||||||
|  | 		var hostname = HttpMiddleware.sanitizeHostname(req); | ||||||
|  | 
 | ||||||
|  | 		req.on("error", function(err) { | ||||||
|  | 			explainError(gl, err, "http_01_middleware_socket", hostname); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (skipIfNeedBe(req, res, next, defaultApp, hostname)) { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var token = req.url.slice(challengePrefix.length); | ||||||
|  | 
 | ||||||
|  | 		gl.getAcmeHttp01ChallengeResponse({ type: "http-01", servername: hostname, token: token }) | ||||||
|  | 			.then(function(result) { | ||||||
|  | 				respondWithGrace(res, result, hostname, token); | ||||||
|  | 			}) | ||||||
|  | 			.catch(function(err) { | ||||||
|  | 				respondToError(gl, res, err, "http_01_middleware_challenge_response", hostname); | ||||||
|  | 			}); | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function skipIfNeedBe(req, res, next, defaultApp, hostname) { | ||||||
|  | 	if (!hostname || 0 !== req.url.indexOf(challengePrefix)) { | ||||||
|  | 		if ("function" === typeof defaultApp) { | ||||||
|  | 			defaultApp(req, res, next); | ||||||
|  | 		} else if ("function" === typeof next) { | ||||||
|  | 			next(); | ||||||
|  | 		} else { | ||||||
|  | 			res.statusCode = 500; | ||||||
|  | 			res.end("[500] Developer Error: app.use('/', greenlock.httpMiddleware()) or greenlock.httpMiddleware(app)"); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function respondWithGrace(res, result, hostname, token) { | ||||||
|  | 	var keyAuth = result.keyAuthorization; | ||||||
|  | 	if (keyAuth && "string" === typeof keyAuth) { | ||||||
|  | 		res.setHeader("Content-Type", "text/plain; charset=utf-8"); | ||||||
|  | 		res.end(keyAuth); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	res.statusCode = 404; | ||||||
|  | 	res.setHeader("Content-Type", "application/json; charset=utf-8"); | ||||||
|  | 	res.end(JSON.stringify({ error: { message: "domain '" + hostname + "' has no token '" + token + "'." } })); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function explainError(gl, err, ctx, hostname) { | ||||||
|  | 	if (!err.servername) { | ||||||
|  | 		err.servername = hostname; | ||||||
|  | 	} | ||||||
|  | 	if (!err.context) { | ||||||
|  | 		err.context = ctx; | ||||||
|  | 	} | ||||||
|  | 	(gl.notify || gl._notify)("error", err); | ||||||
|  | 	return err; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function respondToError(gl, res, err, ctx, hostname) { | ||||||
|  | 	err = explainError(gl, err, ctx, hostname); | ||||||
|  | 	res.statusCode = 500; | ||||||
|  | 	res.end("Internal Server Error: See logs for details."); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | HttpMiddleware.getHostname = function(req) { | ||||||
|  | 	return req.hostname || req.headers["x-forwarded-host"] || (req.headers.host || ""); | ||||||
|  | }; | ||||||
|  | HttpMiddleware.sanitizeHostname = function(req) { | ||||||
|  | 	// we can trust XFH because spoofing causes no ham in this limited use-case scenario
 | ||||||
|  | 	// (and only telebit would be legitimately setting XFH)
 | ||||||
|  | 	var servername = HttpMiddleware.getHostname(req) | ||||||
|  | 		.toLowerCase() | ||||||
|  | 		.replace(/:.*/, ""); | ||||||
|  | 	try { | ||||||
|  | 		req.hostname = servername; | ||||||
|  | 	} catch (e) { | ||||||
|  | 		// read-only express property
 | ||||||
|  | 	} | ||||||
|  | 	if (req.headers["x-forwarded-host"]) { | ||||||
|  | 		req.headers["x-forwarded-host"] = servername; | ||||||
|  | 	} | ||||||
|  | 	try { | ||||||
|  | 		req.headers.host = servername; | ||||||
|  | 	} catch (e) { | ||||||
|  | 		// TODO is this a possible error?
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return (servernameRe.test(servername) && -1 === servername.indexOf("..") && servername) || ""; | ||||||
|  | }; | ||||||
							
								
								
									
										133
									
								
								https-middleware.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								https-middleware.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,133 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | var SanitizeHost = module.exports; | ||||||
|  | var HttpMiddleware = require("./http-middleware.js"); | ||||||
|  | 
 | ||||||
|  | SanitizeHost.create = function(gl, app) { | ||||||
|  | 	return function(req, res, next) { | ||||||
|  | 		function realNext() { | ||||||
|  | 			if ("function" === typeof app) { | ||||||
|  | 				app(req, res); | ||||||
|  | 			} else if ("function" === typeof next) { | ||||||
|  | 				next(); | ||||||
|  | 			} else { | ||||||
|  | 				res.statusCode = 500; | ||||||
|  | 				res.end("Error: no middleware assigned"); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var hostname = HttpMiddleware.getHostname(req); | ||||||
|  | 		// Replace the hostname, and get the safe version
 | ||||||
|  | 		var safehost = HttpMiddleware.sanitizeHostname(req); | ||||||
|  | 
 | ||||||
|  | 		// if no hostname, move along
 | ||||||
|  | 		if (!hostname) { | ||||||
|  | 			realNext(); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// if there were unallowed characters, complain
 | ||||||
|  | 		if (safehost.length !== hostname.length) { | ||||||
|  | 			res.statusCode = 400; | ||||||
|  | 			res.end("Malformed HTTP Header: 'Host: " + hostname + "'"); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Note: This sanitize function is also called on plain sockets, which don't need Domain Fronting checks
 | ||||||
|  | 		if (req.socket.encrypted) { | ||||||
|  | 			if (req.socket && "string" === typeof req.socket.servername) { | ||||||
|  | 				// Workaround for https://github.com/nodejs/node/issues/22389
 | ||||||
|  | 				if (!SanitizeHost._checkServername(safehost, req.socket)) { | ||||||
|  | 					res.statusCode = 400; | ||||||
|  | 					res.setHeader("Content-Type", "text/html; charset=utf-8"); | ||||||
|  | 					res.end( | ||||||
|  | 						"<h1>Domain Fronting Error</h1>" + | ||||||
|  | 							"<p>This connection was secured using TLS/SSL for '" + | ||||||
|  | 							(req.socket.servername || "").toLowerCase() + | ||||||
|  | 							"'</p>" + | ||||||
|  | 							"<p>The HTTP request specified 'Host: " + | ||||||
|  | 							safehost + | ||||||
|  | 							"', which is (obviously) different.</p>" + | ||||||
|  | 							"<p>Because this looks like a domain fronting attack, the connection has been terminated.</p>" | ||||||
|  | 					); | ||||||
|  | 					return; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			/* | ||||||
|  |       else if (safehost && !gl._skip_fronting_check) { | ||||||
|  | 
 | ||||||
|  | 				// We used to print a log message here, but it turns out that it's
 | ||||||
|  | 				// really common for IoT devices to not use SNI (as well as many bots
 | ||||||
|  | 				// and such).
 | ||||||
|  | 				// It was common for the log message to pop up as the first request
 | ||||||
|  | 				// to the server, and that was confusing. So instead now we do nothing.
 | ||||||
|  | 
 | ||||||
|  | 				//console.warn("no string for req.socket.servername," + " skipping fronting check for '" + safehost + "'");
 | ||||||
|  | 				//gl._skip_fronting_check = true;
 | ||||||
|  | 			} | ||||||
|  |       */ | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// carry on
 | ||||||
|  | 		realNext(); | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var warnDomainFronting = true; | ||||||
|  | var warnUnexpectedError = true; | ||||||
|  | SanitizeHost._checkServername = function(safeHost, tlsSocket) { | ||||||
|  | 	var servername = (tlsSocket.servername || "").toLowerCase(); | ||||||
|  | 
 | ||||||
|  | 	// acceptable: older IoT devices may lack SNI support
 | ||||||
|  | 	if (!servername) { | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 	// acceptable: odd... but acceptable
 | ||||||
|  | 	if (!safeHost) { | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 	if (safeHost === servername) { | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if ("function" !== typeof tlsSocket.getCertificate) { | ||||||
|  | 		// domain fronting attacks allowed
 | ||||||
|  | 		if (warnDomainFronting) { | ||||||
|  | 			// https://github.com/nodejs/node/issues/24095
 | ||||||
|  | 			console.warn( | ||||||
|  | 				"Warning: node " + | ||||||
|  | 					process.version + | ||||||
|  | 					" is vulnerable to domain fronting attacks. Please use node v11.2.0 or greater." | ||||||
|  | 			); | ||||||
|  | 			warnDomainFronting = false; | ||||||
|  | 		} | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// connection established with servername and session is re-used for allowed name
 | ||||||
|  | 	// See https://github.com/nodejs/node/issues/24095
 | ||||||
|  | 	var cert = tlsSocket.getCertificate(); | ||||||
|  | 	try { | ||||||
|  | 		// TODO optimize / cache?
 | ||||||
|  | 		// *should* always have a string, right?
 | ||||||
|  | 		// *should* always be lowercase already, right?
 | ||||||
|  | 		if ( | ||||||
|  | 			(cert.subject.CN || "").toLowerCase() !== safeHost && | ||||||
|  | 			!(cert.subjectaltname || "").split(/,\s+/).some(function(name) { | ||||||
|  | 				// always prefixed with "DNS:"
 | ||||||
|  | 				return safeHost === name.slice(4).toLowerCase(); | ||||||
|  | 			}) | ||||||
|  | 		) { | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 	} catch (e) { | ||||||
|  | 		// not sure what else to do in this situation...
 | ||||||
|  | 		if (warnUnexpectedError) { | ||||||
|  | 			console.warn("Warning: encoutered error while performing domain fronting check: " + e.message); | ||||||
|  | 			warnUnexpectedError = false; | ||||||
|  | 		} | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return false; | ||||||
|  | }; | ||||||
							
								
								
									
										334
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										334
									
								
								index.js
									
									
									
									
									
								
							| @ -1,334 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| var PromiseA; |  | ||||||
| try { |  | ||||||
| 	PromiseA = require("bluebird"); |  | ||||||
| } catch (e) { |  | ||||||
| 	PromiseA = global.Promise; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // opts.approveDomains(options, certs, cb)
 |  | ||||||
| module.exports.create = function(opts) { |  | ||||||
| 	// accept all defaults for greenlock.challenges, greenlock.store, greenlock.middleware
 |  | ||||||
| 	if (!opts._communityPackage) { |  | ||||||
| 		opts._communityPackage = "greenlock-express.js"; |  | ||||||
| 		opts._communityPackageVersion = require("./package.json").version; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function explainError(e) { |  | ||||||
| 		console.error("Error:" + e.message); |  | ||||||
| 		if ("EACCES" === e.errno) { |  | ||||||
| 			console.error("You don't have prmission to access '" + e.address + ":" + e.port + "'."); |  | ||||||
| 			console.error('You probably need to use "sudo" or "sudo setcap \'cap_net_bind_service=+ep\' $(which node)"'); |  | ||||||
| 			return; |  | ||||||
| 		} |  | ||||||
| 		if ("EADDRINUSE" === e.errno) { |  | ||||||
| 			console.error("'" + e.address + ":" + e.port + "' is already being used by some other program."); |  | ||||||
| 			console.error("You probably need to stop that program or restart your computer."); |  | ||||||
| 			return; |  | ||||||
| 		} |  | ||||||
| 		console.error(e.code + ": '" + e.address + ":" + e.port + "'"); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function _createPlain(plainPort) { |  | ||||||
| 		if (!plainPort) { |  | ||||||
| 			plainPort = 80; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var parts = String(plainPort).split(":"); |  | ||||||
| 		var p = parts.pop(); |  | ||||||
| 		var addr = parts |  | ||||||
| 			.join(":") |  | ||||||
| 			.replace(/^\[/, "") |  | ||||||
| 			.replace(/\]$/, ""); |  | ||||||
| 		var args = []; |  | ||||||
| 		var httpType; |  | ||||||
| 		var server; |  | ||||||
| 		var validHttpPort = parseInt(p, 10) >= 0; |  | ||||||
| 
 |  | ||||||
| 		if (addr) { |  | ||||||
| 			args[1] = addr; |  | ||||||
| 		} |  | ||||||
| 		if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) { |  | ||||||
| 			console.warn("'" + p + "' doesn't seem to be a valid port number, socket path, or pipe"); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var mw = greenlock.middleware.sanitizeHost(greenlock.middleware(require("redirect-https")())); |  | ||||||
| 		server = require("http").createServer(function(req, res) { |  | ||||||
| 			req.on("error", function(err) { |  | ||||||
| 				console.error("Insecure Request Network Connection Error:"); |  | ||||||
| 				console.error(err); |  | ||||||
| 			}); |  | ||||||
| 			mw(req, res); |  | ||||||
| 		}); |  | ||||||
| 		httpType = "http"; |  | ||||||
| 
 |  | ||||||
| 		return { |  | ||||||
| 			server: server, |  | ||||||
| 			listen: function() { |  | ||||||
| 				return new PromiseA(function(resolve, reject) { |  | ||||||
| 					args[0] = p; |  | ||||||
| 					args.push(function() { |  | ||||||
| 						if (!greenlock.servername) { |  | ||||||
| 							if (Array.isArray(greenlock.approvedDomains) && greenlock.approvedDomains.length) { |  | ||||||
| 								greenlock.servername = greenlock.approvedDomains[0]; |  | ||||||
| 							} |  | ||||||
| 							if (Array.isArray(greenlock.approveDomains) && greenlock.approvedDomains.length) { |  | ||||||
| 								greenlock.servername = greenlock.approvedDomains[0]; |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						if (!greenlock.servername) { |  | ||||||
| 							resolve(null); |  | ||||||
| 							return; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						return greenlock |  | ||||||
| 							.check({ domains: [greenlock.servername] }) |  | ||||||
| 							.then(function(certs) { |  | ||||||
| 								if (certs) { |  | ||||||
| 									return { |  | ||||||
| 										key: Buffer.from(certs.privkey, "ascii"), |  | ||||||
| 										cert: Buffer.from(certs.cert + "\r\n" + certs.chain, "ascii") |  | ||||||
| 									}; |  | ||||||
| 								} |  | ||||||
| 								console.info( |  | ||||||
| 									"Fetching certificate for '%s' to use as default for HTTPS server...", |  | ||||||
| 									greenlock.servername |  | ||||||
| 								); |  | ||||||
| 								return new PromiseA(function(resolve, reject) { |  | ||||||
| 									// using SNICallback because all options will be set
 |  | ||||||
| 									greenlock.tlsOptions.SNICallback(greenlock.servername, function(err /*, secureContext*/) { |  | ||||||
| 										if (err) { |  | ||||||
| 											reject(err); |  | ||||||
| 											return; |  | ||||||
| 										} |  | ||||||
| 										return greenlock |  | ||||||
| 											.check({ domains: [greenlock.servername] }) |  | ||||||
| 											.then(function(certs) { |  | ||||||
| 												resolve({ |  | ||||||
| 													key: Buffer.from(certs.privkey, "ascii"), |  | ||||||
| 													cert: Buffer.from(certs.cert + "\r\n" + certs.chain, "ascii") |  | ||||||
| 												}); |  | ||||||
| 											}) |  | ||||||
| 											.catch(reject); |  | ||||||
| 									}); |  | ||||||
| 								}); |  | ||||||
| 							}) |  | ||||||
| 							.then(resolve) |  | ||||||
| 							.catch(reject); |  | ||||||
| 					}); |  | ||||||
| 					server.listen.apply(server, args).on("error", function(e) { |  | ||||||
| 						if (server.listenerCount("error") < 2) { |  | ||||||
| 							console.warn("Did not successfully create http server and bind to port '" + p + "':"); |  | ||||||
| 							explainError(e); |  | ||||||
| 							process.exit(41); |  | ||||||
| 						} |  | ||||||
| 					}); |  | ||||||
| 				}); |  | ||||||
| 			} |  | ||||||
| 		}; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function _create(port) { |  | ||||||
| 		if (!port) { |  | ||||||
| 			port = 443; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var parts = String(port).split(":"); |  | ||||||
| 		var p = parts.pop(); |  | ||||||
| 		var addr = parts |  | ||||||
| 			.join(":") |  | ||||||
| 			.replace(/^\[/, "") |  | ||||||
| 			.replace(/\]$/, ""); |  | ||||||
| 		var args = []; |  | ||||||
| 		var httpType; |  | ||||||
| 		var server; |  | ||||||
| 		var validHttpPort = parseInt(p, 10) >= 0; |  | ||||||
| 
 |  | ||||||
| 		if (addr) { |  | ||||||
| 			args[1] = addr; |  | ||||||
| 		} |  | ||||||
| 		if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) { |  | ||||||
| 			console.warn("'" + p + "' doesn't seem to be a valid port number, socket path, or pipe"); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var https; |  | ||||||
| 		try { |  | ||||||
| 			https = require("spdy"); |  | ||||||
| 			greenlock.tlsOptions.spdy = { protocols: ["h2", "http/1.1"], plain: false }; |  | ||||||
| 			httpType = "http2 (spdy/h2)"; |  | ||||||
| 		} catch (e) { |  | ||||||
| 			https = require("https"); |  | ||||||
| 			httpType = "https"; |  | ||||||
| 		} |  | ||||||
| 		var sniCallback = greenlock.tlsOptions.SNICallback; |  | ||||||
| 		greenlock.tlsOptions.SNICallback = function(domain, cb) { |  | ||||||
| 			sniCallback(domain, function(err, context) { |  | ||||||
| 				cb(err, context); |  | ||||||
| 
 |  | ||||||
| 				if (!context || server._hasDefaultSecureContext) { |  | ||||||
| 					return; |  | ||||||
| 				} |  | ||||||
| 				if (!domain) { |  | ||||||
| 					domain = greenlock.servername; |  | ||||||
| 				} |  | ||||||
| 				if (!domain) { |  | ||||||
| 					return; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				return greenlock |  | ||||||
| 					.check({ domains: [domain] }) |  | ||||||
| 					.then(function(certs) { |  | ||||||
| 						// ignore the case that check doesn't have all the right args here
 |  | ||||||
| 						// to get the same certs that it just got (eventually the right ones will come in)
 |  | ||||||
| 						if (!certs) { |  | ||||||
| 							return; |  | ||||||
| 						} |  | ||||||
| 						if (server.setSecureContext) { |  | ||||||
| 							// only available in node v11.0+
 |  | ||||||
| 							server.setSecureContext({ |  | ||||||
| 								key: Buffer.from(certs.privkey, "ascii"), |  | ||||||
| 								cert: Buffer.from(certs.cert + "\r\n" + certs.chain, "ascii") |  | ||||||
| 							}); |  | ||||||
| 							console.info("Using '%s' as default certificate", domain); |  | ||||||
| 						} else { |  | ||||||
| 							console.info("Setting default certificates dynamically requires node v11.0+. Skipping."); |  | ||||||
| 						} |  | ||||||
| 						server._hasDefaultSecureContext = true; |  | ||||||
| 					}) |  | ||||||
| 					.catch(function(/*e*/) { |  | ||||||
| 						// this may be that the test.example.com was requested, but it's listed
 |  | ||||||
| 						// on the cert for demo.example.com which is in its own directory, not the other
 |  | ||||||
| 						//console.warn("Unusual error: couldn't get newly authorized certificate:");
 |  | ||||||
| 						//console.warn(e.message);
 |  | ||||||
| 					}); |  | ||||||
| 			}); |  | ||||||
| 		}; |  | ||||||
| 		if (greenlock.tlsOptions.cert) { |  | ||||||
| 			server._hasDefaultSecureContext = true; |  | ||||||
| 			if (greenlock.tlsOptions.cert.toString("ascii").split("BEGIN").length < 3) { |  | ||||||
| 				console.warn( |  | ||||||
| 					"Invalid certificate file. 'tlsOptions.cert' should contain cert.pem (certificate file) *and* chain.pem (intermediate certificates) seperated by an extra newline (CRLF)" |  | ||||||
| 				); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var mw = greenlock.middleware.sanitizeHost(function(req, res) { |  | ||||||
| 			try { |  | ||||||
| 				greenlock.app(req, res); |  | ||||||
| 			} catch (e) { |  | ||||||
| 				console.error("[error] [greenlock.app] Your HTTP handler had an uncaught error:"); |  | ||||||
| 				console.error(e); |  | ||||||
| 				try { |  | ||||||
| 					res.statusCode = 500; |  | ||||||
| 					res.end("Internal Server Error: [Greenlock] HTTP exception logged for user-provided handler."); |  | ||||||
| 				} catch (e) { |  | ||||||
| 					// ignore
 |  | ||||||
| 					// (headers may have already been sent, etc)
 |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		}); |  | ||||||
| 		server = https.createServer(greenlock.tlsOptions, function(req, res) { |  | ||||||
| 			/* |  | ||||||
| 			// Don't do this yet
 |  | ||||||
| 			req.on("error", function(err) { |  | ||||||
| 				console.error("HTTPS Request Network Connection Error:"); |  | ||||||
| 				console.error(err); |  | ||||||
| 			}); |  | ||||||
| 			*/ |  | ||||||
| 			mw(req, res); |  | ||||||
| 		}); |  | ||||||
| 		server.type = httpType; |  | ||||||
| 
 |  | ||||||
| 		return { |  | ||||||
| 			server: server, |  | ||||||
| 			listen: function() { |  | ||||||
| 				return new PromiseA(function(resolve) { |  | ||||||
| 					args[0] = p; |  | ||||||
| 					args.push(function() { |  | ||||||
| 						resolve(/*server*/); |  | ||||||
| 					}); |  | ||||||
| 					server.listen.apply(server, args).on("error", function(e) { |  | ||||||
| 						if (server.listenerCount("error") < 2) { |  | ||||||
| 							console.warn("Did not successfully create http server and bind to port '" + p + "':"); |  | ||||||
| 							explainError(e); |  | ||||||
| 							process.exit(41); |  | ||||||
| 						} |  | ||||||
| 					}); |  | ||||||
| 				}); |  | ||||||
| 			} |  | ||||||
| 		}; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// NOTE: 'greenlock' is just 'opts' renamed
 |  | ||||||
| 	var greenlock = require("greenlock").create(opts); |  | ||||||
| 
 |  | ||||||
| 	if (!opts.app) { |  | ||||||
| 		opts.app = function(req, res) { |  | ||||||
| 			res.end("Hello, World!\nWith Love,\nGreenlock for Express.js"); |  | ||||||
| 		}; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	opts.listen = function(plainPort, port, fnPlain, fn) { |  | ||||||
| 		var server; |  | ||||||
| 		var plainServer; |  | ||||||
| 
 |  | ||||||
| 		// If there is only one handler for the `listening` (i.e. TCP bound) event
 |  | ||||||
| 		// then we want to use it as HTTPS (backwards compat)
 |  | ||||||
| 		if (!fn) { |  | ||||||
| 			fn = fnPlain; |  | ||||||
| 			fnPlain = null; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var obj1 = _createPlain(plainPort, true); |  | ||||||
| 		var obj2 = _create(port, false); |  | ||||||
| 
 |  | ||||||
| 		plainServer = obj1.server; |  | ||||||
| 		server = obj2.server; |  | ||||||
| 
 |  | ||||||
| 		server.then = obj1.listen().then(function(tlsOptions) { |  | ||||||
| 			if (tlsOptions) { |  | ||||||
| 				if (server.setSecureContext) { |  | ||||||
| 					// only available in node v11.0+
 |  | ||||||
| 					server.setSecureContext(tlsOptions); |  | ||||||
| 					console.info("Using '%s' as default certificate", greenlock.servername); |  | ||||||
| 				} else { |  | ||||||
| 					console.info("Setting default certificates dynamically requires node v11.0+. Skipping."); |  | ||||||
| 				} |  | ||||||
| 				server._hasDefaultSecureContext = true; |  | ||||||
| 			} |  | ||||||
| 			return obj2.listen().then(function() { |  | ||||||
| 				// Report plain http status
 |  | ||||||
| 				if ("function" === typeof fnPlain) { |  | ||||||
| 					fnPlain.apply(plainServer); |  | ||||||
| 				} else if (!fn && !plainServer.listenerCount("listening") && !server.listenerCount("listening")) { |  | ||||||
| 					console.info( |  | ||||||
| 						"[:" + |  | ||||||
| 							(plainServer.address().port || plainServer.address()) + |  | ||||||
| 							"] Handling ACME challenges and redirecting to " + |  | ||||||
| 							server.type |  | ||||||
| 					); |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				// Report h2/https status
 |  | ||||||
| 				if ("function" === typeof fn) { |  | ||||||
| 					fn.apply(server); |  | ||||||
| 				} else if (!server.listenerCount("listening")) { |  | ||||||
| 					console.info("[:" + (server.address().port || server.address()) + "] Serving " + server.type); |  | ||||||
| 				} |  | ||||||
| 			}); |  | ||||||
| 		}).then; |  | ||||||
| 
 |  | ||||||
| 		server.unencrypted = plainServer; |  | ||||||
| 		return server; |  | ||||||
| 	}; |  | ||||||
| 	opts.middleware.acme = function(opts) { |  | ||||||
| 		return greenlock.middleware.sanitizeHost(greenlock.middleware(require("redirect-https")(opts))); |  | ||||||
| 	}; |  | ||||||
| 	opts.middleware.secure = function(app) { |  | ||||||
| 		return greenlock.middleware.sanitizeHost(app); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	return greenlock; |  | ||||||
| }; |  | ||||||
							
								
								
									
										36
									
								
								main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								main.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | // this is the stuff that should run in the main foreground process,
 | ||||||
|  | // whether it's single or master
 | ||||||
|  | 
 | ||||||
|  | var major = process.versions.node.split(".")[0]; | ||||||
|  | var minor = process.versions.node.split(".")[1]; | ||||||
|  | var _hasSetSecureContext = false; | ||||||
|  | var shouldUpgrade = false; | ||||||
|  | 
 | ||||||
|  | // TODO can we trust earlier versions as well?
 | ||||||
|  | if (major >= 12) { | ||||||
|  | 	_hasSetSecureContext = !!require("http2").createSecureServer({}, function() {}).setSecureContext; | ||||||
|  | } else { | ||||||
|  | 	_hasSetSecureContext = !!require("https").createServer({}, function() {}).setSecureContext; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO document in issues
 | ||||||
|  | if (!_hasSetSecureContext) { | ||||||
|  | 	// TODO this isn't necessary if greenlock options are set with options.cert
 | ||||||
|  | 	console.warn("Warning: node " + process.version + " is missing tlsSocket.setSecureContext()."); | ||||||
|  | 	console.warn("         The default certificate may not be set."); | ||||||
|  | 	shouldUpgrade = true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | if (major < 11 || (11 === major && minor < 2)) { | ||||||
|  | 	// https://github.com/nodejs/node/issues/24095
 | ||||||
|  | 	console.warn("Warning: node " + process.version + " is missing tlsSocket.getCertificate()."); | ||||||
|  | 	console.warn("         This is necessary to guard against domain fronting attacks."); | ||||||
|  | 	shouldUpgrade = true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | if (shouldUpgrade) { | ||||||
|  | 	console.warn("Warning: Please upgrade to node v11.2.0 or greater."); | ||||||
|  |   console.warn(); | ||||||
|  | } | ||||||
							
								
								
									
										95
									
								
								master.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								master.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | require("./main.js"); | ||||||
|  | 
 | ||||||
|  | var Master = module.exports; | ||||||
|  | 
 | ||||||
|  | var cluster = require("cluster"); | ||||||
|  | var os = require("os"); | ||||||
|  | var Greenlock = require("@root/greenlock"); | ||||||
|  | var pkg = require("./package.json"); | ||||||
|  | 
 | ||||||
|  | Master.create = function(opts) { | ||||||
|  | 	var workers = []; | ||||||
|  | 	var resolveCb; | ||||||
|  | 	var readyCb; | ||||||
|  | 	var _kicked = false; | ||||||
|  | 
 | ||||||
|  | 	var packageAgent = pkg.name + "/" + pkg.version; | ||||||
|  | 	if ("string" === typeof opts.packageAgent) { | ||||||
|  | 		opts.packageAgent += " "; | ||||||
|  | 	} else { | ||||||
|  | 		opts.packageAgent = ""; | ||||||
|  | 	} | ||||||
|  | 	opts.packageAgent += packageAgent; | ||||||
|  | 	var greenlock = Greenlock.create(opts); | ||||||
|  | 
 | ||||||
|  | 	var ready = new Promise(function(resolve) { | ||||||
|  | 		resolveCb = resolve; | ||||||
|  | 	}).then(function(fn) { | ||||||
|  | 		readyCb = fn; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	function kickoff() { | ||||||
|  | 		if (_kicked) { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		_kicked = true; | ||||||
|  | 
 | ||||||
|  | 		console.log("TODO: start the workers and such..."); | ||||||
|  | 		// handle messages from workers
 | ||||||
|  | 		workers.push(null); | ||||||
|  | 		ready.then(function(fn) { | ||||||
|  | 			// not sure what this API should be yet
 | ||||||
|  | 			fn({ | ||||||
|  | 				//workers: workers.slice(0)
 | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var master = { | ||||||
|  | 		worker: function() { | ||||||
|  | 			kickoff(); | ||||||
|  | 			return master; | ||||||
|  | 		}, | ||||||
|  | 		master: function(fn) { | ||||||
|  | 			if (readyCb) { | ||||||
|  | 				throw new Error("can't call master twice"); | ||||||
|  | 			} | ||||||
|  | 			kickoff(); | ||||||
|  | 			resolveCb(fn); | ||||||
|  | 			return master; | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // opts.approveDomains(options, certs, cb)
 | ||||||
|  | GLE.create = function(opts) { | ||||||
|  | 	GLE._spawnWorkers(opts); | ||||||
|  | 
 | ||||||
|  | 	gl.tlsOptions = {}; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	return master; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function range(n) { | ||||||
|  | 	return new Array(n).join(",").split(","); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Master._spawnWorkers = function(opts) { | ||||||
|  | 	var numCpus = parseInt(process.env.NUMBER_OF_PROCESSORS, 10) || os.cpus().length; | ||||||
|  | 
 | ||||||
|  | 	var numWorkers = parseInt(opts.numWorkers, 10); | ||||||
|  | 	if (!numWorkers) { | ||||||
|  | 		if (numCpus <= 2) { | ||||||
|  | 			numWorkers = numCpus; | ||||||
|  | 		} else { | ||||||
|  | 			numWorkers = numCpus - 1; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return range(numWorkers).map(function() { | ||||||
|  | 		return cluster.fork(); | ||||||
|  | 	}); | ||||||
|  | }; | ||||||
							
								
								
									
										128
									
								
								servers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								servers.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,128 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | var Servers = module.exports; | ||||||
|  | 
 | ||||||
|  | var http = require("http"); | ||||||
|  | var HttpMiddleware = require("./http-middleware.js"); | ||||||
|  | var HttpsMiddleware = require("./https-middleware.js"); | ||||||
|  | var sni = require("./sni.js"); | ||||||
|  | 
 | ||||||
|  | Servers.create = function(greenlock, opts) { | ||||||
|  | 	var servers = {}; | ||||||
|  | 	var _httpServer; | ||||||
|  | 	var _httpsServer; | ||||||
|  | 
 | ||||||
|  | 	function startError(e) { | ||||||
|  | 		explainError(e); | ||||||
|  | 		process.exit(1); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	servers.httpServer = function(defaultApp) { | ||||||
|  | 		if (_httpServer) { | ||||||
|  | 			return _httpServer; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		_httpServer = http.createServer(HttpMiddleware.create(opts.greenlock, defaultApp)); | ||||||
|  | 		_httpServer.once("error", startError); | ||||||
|  | 
 | ||||||
|  | 		return _httpServer; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	servers.httpsServer = function(secureOpts, defaultApp) { | ||||||
|  | 		if (_httpsServer) { | ||||||
|  | 			return _httpsServer; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (!secureOpts) { | ||||||
|  | 			secureOpts = {}; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		_httpsServer = createSecureServer( | ||||||
|  | 			wrapDefaultSniCallback(opts, greenlock, secureOpts), | ||||||
|  | 			HttpsMiddleware.create(greenlock, defaultApp) | ||||||
|  | 		); | ||||||
|  | 		_httpsServer.once("error", startError); | ||||||
|  | 
 | ||||||
|  | 		return _httpsServer; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	servers.serveApp = function(app) { | ||||||
|  | 		return new Promise(function(resolve, reject) { | ||||||
|  | 			if ("function" !== typeof app) { | ||||||
|  | 				reject(new Error("glx.serveApp(app) expects a node/express app in the format `function (req, res) { ... }`")); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			var plainServer = servers.httpServer(require("redirect-https")()); | ||||||
|  | 			var plainAddr = "0.0.0.0"; | ||||||
|  | 			var plainPort = 80; | ||||||
|  | 			plainServer.listen(plainPort, plainAddr, function() { | ||||||
|  | 				console.info("Listening on", plainAddr + ":" + plainPort, "for ACME challenges, and redirecting to HTTPS"); | ||||||
|  | 
 | ||||||
|  | 				// TODO fetch greenlock.servername
 | ||||||
|  | 				var secureServer = servers.httpsServer(app); | ||||||
|  | 				var secureAddr = "0.0.0.0"; | ||||||
|  | 				var securePort = 443; | ||||||
|  | 				secureServer.listen(securePort, secureAddr, function() { | ||||||
|  | 					console.info("Listening on", secureAddr + ":" + securePort, "for secure traffic"); | ||||||
|  | 
 | ||||||
|  | 					plainServer.removeListener("error", startError); | ||||||
|  | 					secureServer.removeListener("error", startError); | ||||||
|  | 					resolve(); | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	}; | ||||||
|  | 	return servers; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function explainError(e) { | ||||||
|  | 	console.error(); | ||||||
|  | 	console.error("Error: " + e.message); | ||||||
|  | 	if ("EACCES" === e.errno) { | ||||||
|  | 		console.error("You don't have prmission to access '" + e.address + ":" + e.port + "'."); | ||||||
|  | 		console.error('You probably need to use "sudo" or "sudo setcap \'cap_net_bind_service=+ep\' $(which node)"'); | ||||||
|  | 	} else if ("EADDRINUSE" === e.errno) { | ||||||
|  | 		console.error("'" + e.address + ":" + e.port + "' is already being used by some other program."); | ||||||
|  | 		console.error("You probably need to stop that program or restart your computer."); | ||||||
|  | 	} else { | ||||||
|  | 		console.error(e.code + ": '" + e.address + ":" + e.port + "'"); | ||||||
|  | 	} | ||||||
|  | 	console.error(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function wrapDefaultSniCallback(opts, greenlock, secureOpts) { | ||||||
|  | 	// I'm not sure yet if the original SNICallback
 | ||||||
|  | 	// should be called before or after, so I'm just
 | ||||||
|  | 	// going to delay making that choice until I have the use case
 | ||||||
|  | 	/* | ||||||
|  | 		if (!secureOpts.SNICallback) { | ||||||
|  | 			secureOpts.SNICallback = function(servername, cb) { | ||||||
|  | 				cb(null, null); | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  |   */ | ||||||
|  | 	if (secureOpts.SNICallback) { | ||||||
|  | 		console.warn(); | ||||||
|  | 		console.warn("[warning] Ignoring the given tlsOptions.SNICallback function."); | ||||||
|  | 		console.warn(); | ||||||
|  | 		console.warn("          We're very open to implementing support for this,"); | ||||||
|  | 		console.warn("          we just don't understand the use case yet."); | ||||||
|  | 		console.warn("          Please open an issue to discuss. We'd love to help."); | ||||||
|  | 		console.warn(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	secureOpts.SNICallback = sni.create(opts, greenlock, secureOpts); | ||||||
|  | 	return secureOpts; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function createSecureServer(secureOpts, fn) { | ||||||
|  | 	var major = process.versions.node.split(".")[0]; | ||||||
|  | 
 | ||||||
|  | 	// TODO can we trust earlier versions as well?
 | ||||||
|  | 	if (major >= 12) { | ||||||
|  | 		return require("http2").createSecureServer(secureOpts, fn); | ||||||
|  | 	} else { | ||||||
|  | 		return require("https").createServer(secureOpts, fn); | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								single.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								single.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | require("./main.js"); | ||||||
|  | 
 | ||||||
|  | var Single = module.exports; | ||||||
|  | var Servers = require("./servers.js"); | ||||||
|  | var Greenlock = require("@root/greenlock"); | ||||||
|  | 
 | ||||||
|  | Single.create = function(opts) { | ||||||
|  | 	var greenlock = Greenlock.create(opts); | ||||||
|  | 	var servers = Servers.create(greenlock, opts); | ||||||
|  | 	//var master = Master.create(opts);
 | ||||||
|  | 
 | ||||||
|  | 	var single = { | ||||||
|  | 		worker: function(fn) { | ||||||
|  | 			fn(servers); | ||||||
|  | 			return single; | ||||||
|  | 		}, | ||||||
|  | 		master: function(/*fn*/) { | ||||||
|  | 			// ignore
 | ||||||
|  | 			//fn(master);
 | ||||||
|  | 			return single; | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 	return single; | ||||||
|  | }; | ||||||
							
								
								
									
										184
									
								
								sni.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								sni.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,184 @@ | |||||||
|  | "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)); | ||||||
|  | 
 | ||||||
|  | //secureOpts.SNICallback = sni.create(opts, greenlock, secureOpts);
 | ||||||
|  | sni.create = function(opts, greenlock, secureOpts) { | ||||||
|  | 	var _cache = {}; | ||||||
|  | 	var defaultServername = opts.servername || 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()?
 | ||||||
|  | 			(opts.notify || greenlock.notify || greenlock._notify)(ev, args); | ||||||
|  | 		} catch (e) { | ||||||
|  | 			console.error(e); | ||||||
|  | 			console.error(ev, args); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	function getSecureContext(servername, cb) { | ||||||
|  | 		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) { | ||||||
|  | 			cb(null, secureContext); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		getFreshContext(servername) | ||||||
|  | 			.then(function(secureContext) { | ||||||
|  | 				if (secureContext) { | ||||||
|  | 					cb(null, secureContext); | ||||||
|  | 					return; | ||||||
|  | 				} | ||||||
|  | 				// Note: this does not replace tlsSocket.setSecureContext()
 | ||||||
|  | 				// as it only works when SNI has been sent
 | ||||||
|  | 				cb(null, getDefaultContext()); | ||||||
|  | 			}) | ||||||
|  | 			.catch(function(err) { | ||||||
|  | 				if (!err.context) { | ||||||
|  | 					err.context = "sni_callback"; | ||||||
|  | 				} | ||||||
|  | 				notify("error", 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; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (!meta.refreshAt || Date.now() >= meta.refreshAt) { | ||||||
|  | 			getFreshContext(servername).catch(function(e) { | ||||||
|  | 				if (!e.context) { | ||||||
|  | 					e.context = "sni_background_refresh"; | ||||||
|  | 				} | ||||||
|  | 				notify("error", e); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return meta.secureContext; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	function getFreshContext(servername) { | ||||||
|  | 		var meta = getCachedMeta(servername); | ||||||
|  | 		if (!meta && !validServername(servername)) { | ||||||
|  | 			return Promise.resolve(null); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (meta) { | ||||||
|  | 			// prevent stampedes
 | ||||||
|  | 			meta.refreshAt = Date.now() + randomRefreshOffset(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// TODO greenlock.get({ servername: servername })
 | ||||||
|  | 		// TODO don't get unknown certs at all, rely on auto-updates from greenlock
 | ||||||
|  | 		// Note: greenlock.renew() will return an existing fresh cert or issue a new one
 | ||||||
|  | 		return greenlock.renew({ servername: servername }).then(function(matches) { | ||||||
|  | 			var meta = getCachedMeta(servername); | ||||||
|  | 			if (!meta) { | ||||||
|  | 				meta = _cache[servername] = { secureContext: {} }; | ||||||
|  | 			} | ||||||
|  | 			// prevent from being punked by bot trolls
 | ||||||
|  | 			meta.refreshAt = Date.now() + smallStagger; | ||||||
|  | 
 | ||||||
|  | 			// nothing to do
 | ||||||
|  | 			if (!matches.length) { | ||||||
|  | 				return null; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// we only care about the first one
 | ||||||
|  | 			var pems = matches[0].pems; | ||||||
|  | 			var site = matches[0].site; | ||||||
|  | 			var match = matches[0]; | ||||||
|  | 			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" | ||||||
|  | 				}) | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			// copy this same object into every place
 | ||||||
|  | 			[match.altnames || site.altnames || [match.subject || site.subject]].forEach(function(altname) { | ||||||
|  | 				_cache[altname] = meta; | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	function getDefaultContext() { | ||||||
|  | 		return getCachedContext(defaultServername); | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // whenever we need to know when to refresh next
 | ||||||
|  | function randomRefreshOffset() { | ||||||
|  | 	var stagger = Math.round(refreshStagger / 2) - Math.round(Math.random() * refreshStagger); | ||||||
|  | 	return refreshOffset + stagger; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function validServername(servername) { | ||||||
|  | 	// 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(".."); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function wildname(servername) { | ||||||
|  | 	return ( | ||||||
|  | 		"*." + | ||||||
|  | 		servername | ||||||
|  | 			.split(".") | ||||||
|  | 			.slice(1) | ||||||
|  | 			.join(".") | ||||||
|  | 	); | ||||||
|  | } | ||||||
							
								
								
									
										74
									
								
								worker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								worker.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,74 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | var Worker = module.exports; | ||||||
|  | 
 | ||||||
|  | Worker.create = function(opts) { | ||||||
|  | 	var greenlock = { | ||||||
|  | 		// rename presentChallenge?
 | ||||||
|  | 		getAcmeHttp01ChallengeResponse: presentChallenge, | ||||||
|  | 		notify: notifyMaster, | ||||||
|  | 		get: greenlockRenew | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	var worker = { | ||||||
|  | 		worker: function(fn) { | ||||||
|  | 			var servers = require("./servers.js").create(greenlock, opts); | ||||||
|  | 			fn(servers); | ||||||
|  | 			return worker; | ||||||
|  | 		}, | ||||||
|  | 		master: function() { | ||||||
|  | 			// ignore
 | ||||||
|  | 			return worker; | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 	return worker; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function greenlockRenew(args) { | ||||||
|  | 	return request("renew", { | ||||||
|  | 		servername: args.servername | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function presentChallenge(args) { | ||||||
|  | 	return request("challenge-response", { | ||||||
|  | 		servername: args.servername, | ||||||
|  | 		token: args.token | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function request(typename, msg) { | ||||||
|  | 	return new Promise(function(resolve, reject) { | ||||||
|  | 		var rnd = Math.random() | ||||||
|  | 			.slice(2) | ||||||
|  | 			.toString(16); | ||||||
|  | 		var id = "greenlock:" + rnd; | ||||||
|  | 		var timeout; | ||||||
|  | 
 | ||||||
|  | 		function getResponse(msg) { | ||||||
|  | 			if (msg.id !== id) { | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			clearTimeout(timeout); | ||||||
|  | 			resolve(msg); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		process.on("message", getResponse); | ||||||
|  | 		msg.id = msg; | ||||||
|  | 		msg.type = typename; | ||||||
|  | 		process.send(msg); | ||||||
|  | 
 | ||||||
|  | 		timeout = setTimeout(function() { | ||||||
|  | 			process.removeListener("message", getResponse); | ||||||
|  | 			reject(new Error("process message timeout")); | ||||||
|  | 		}, 30 * 1000); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function notifyMaster(ev, args) { | ||||||
|  | 	process.on("message", { | ||||||
|  | 		type: "notification", | ||||||
|  | 		event: ev, | ||||||
|  | 		parameters: args | ||||||
|  | 	}); | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user