"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; };