wip: API looks good, on to testing
This commit is contained in:
parent
9ab7844ea8
commit
0dd3641dc2
|
@ -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");
|
||||
});
|
|
@ -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);
|
||||
};
|
|
@ -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) || "";
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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(".")
|
||||
);
|
||||
}
|
|
@ -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…
Reference in New Issue