index.js 9.58 KB
"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;
};