332 lines
9.5 KiB
JavaScript
332 lines
9.5 KiB
JavaScript
"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) {
|
|
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;
|
|
};
|