http-middleware.js 5.78 KB
"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);
        });

        // Skip unless the path begins with /.well-known/acme-challenge/
        if (!hostname || 0 !== req.url.indexOf(challengePrefix)) {
            skipChallenge(req, res, next, defaultApp);
            return;
        }

        // HEADERS SENT DEBUG NOTE #2
        // at this point, it's most likely Let's Encrypt server
        // (or greenlock itself) performing the verification process
        // Hmmm... perhaps we should change the greenlock prefix to test
        // Anyway, we just got fast the first place where we could
        // be sending headers.

        var token = req.url.slice(challengePrefix.length);

        var done = false;
        var countA = 0;
        var countB = 0;
        gl.getAcmeHttp01ChallengeResponse({ type: "http-01", servername: hostname, token: token })
            .catch(function(err) {
                countA += 1;
                // HEADERS SENT DEBUG NOTE #3
                // This is the second possible time we could be sending headers
                respondToError(gl, res, err, "http_01_middleware_challenge_response", hostname);
                done = true;
                return { __done: true };
            })
            .then(function(result) {
                countB += 1;
                if (result && result.__done) {
                    return;
                }
                if (done) {
                    console.error("Sanity check fail: `done` is in a quantum state of both true and false... huh?");
                    return;
                }
                // HEADERS SENT DEBUG NOTE #4b
                // This is the third/fourth possible time send headers
                return respondWithGrace(res, result, hostname, token);
            })
            .catch(function(err) {
                // HEADERS SENT DEBUG NOTE #5
                // I really don't see how this can be possible.
                // Every case appears to be accounted for
                console.error();
                console.error("[warning] Developer Error:" + (err.code || err.context || ""), countA, countB);
                console.error(err.stack);
                console.error();
                console.error(
                    "This is probably the error that happens routinely on http2 connections, but we're not sure why."
                );
                console.error("To track the status or help contribute,");
                console.error("visit: https://git.rootprojects.org/root/greenlock-express.js/issues/9");
                console.error();
                try {
                    res.end("Internal Server Error [1003]: See logs for details.");
                } catch (e) {
                    // ignore
                }
            });
    };
};

function skipChallenge(req, res, next, defaultApp) {
    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 && result.keyAuthorization;

    // HEADERS SENT DEBUG NOTE #4b
    // This is (still) the third/fourth possible time we could be sending headers
    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;
    }
    // leaving this in the build for now because it will help with existing error reports
    console.error("[warning] network connection error:", (err.context || "") + " " + err.message);
    (gl.notify || gl._notify)("error", err);
    return err;
}

function respondToError(gl, res, err, ctx, hostname) {
    // HEADERS SENT DEBUG NOTE #3b
    // This is (still) the second possible time we could be sending headers
    err = explainError(gl, err, ctx, hostname);
    res.statusCode = 500;
    res.end("Internal Server Error [1004]: 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) || "";
};