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