goldilocks.js/lib/modules/http.js

247 lines
6.9 KiB
JavaScript

'use strict';
module.exports.create = function (deps, conf, greenlockMiddleware) {
var express = require('express');
var app = express();
var adminApp = require('./admin').create(deps, conf);
var domainMatches = require('../domain-utils').match;
var separatePort = require('../domain-utils').separatePort;
var proxyRoutes = [];
var adminDomains = [
/\blocalhost\.admin\./
, /\blocalhost\.alpha\./
, /\badmin\.localhost\./
, /\balpha\.localhost\./
];
function moduleMatchesHost(req, mod) {
var host = separatePort(req.headers.host).host;
return mod.domains.some(function (pattern) {
return domainMatches(pattern, host);
});
}
function verifyHost(fullHost) {
var host = separatePort(fullHost).host;
if (host === 'localhost') {
return fullHost.replace(host, 'localhost.daplie.me');
}
// Test for IPv4 and IPv6 addresses. These patterns will match some invalid addresses,
// but since those still won't be valid domains that won't really be a problem.
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) || /^\[[0-9a-fA-F:]+\]$/.test(host)) {
if (!conf.http.primaryDomain) {
(conf.http.modules || []).some(function (mod) {
return mod.domains.some(function (domain) {
if (domain[0] !== '*') {
conf.http.primaryDomain = domain;
return true;
}
});
});
}
return fullHost.replace(host, conf.http.primaryDomain || host);
}
return fullHost;
}
// We handle both HTTPS and HTTP traffic on the same ports, and we want to redirect
// any unencrypted requests to the same port they came from unless it came in on
// the default HTTP port, in which case there wont be a port specified in the host.
var redirecters = {};
function redirectHttps(req, res, next) {
if (conf.http.allowInsecure) {
next();
return;
}
var port = separatePort(req.headers.host).port;
if (!redirecters[port]) {
redirecters[port] = require('redirect-https')({
port: port
, trustProxy: conf.http.trustProxy
});
}
// localhost and IP addresses cannot have real SSL certs (and don't contain any useful
// info for redirection either), so we direct some hosts to either localhost.daplie.me
// or the "primary domain" ie the first manually specified domain.
req.headers.host = verifyHost(req.headers.host);
redirecters[port](req, res, next);
}
function handleAdmin(req, res, next) {
var admin = adminDomains.some(function (re) {
return re.test(req.headers.host);
});
if (admin) {
adminApp(req, res);
} else {
next();
}
}
function respond404(req, res) {
res.writeHead(404);
res.end('Not Found');
}
function createProxyRoute(mod) {
// This is the easiest way to override the createConnections function the proxy
// module uses, but take note the since we don't have control over where this is
// called the extra options availabled will be different.
var agent = new require('http').Agent({});
agent.createConnection = deps.net.createConnection;
var proxy = require('http-proxy').createProxyServer({
agent: agent
, target: 'http://' + mod.address
, xfwd: true
, toProxy: true
});
// We want to override the default value for some headers with the extra information we
// have available to us in the opts object attached to the connection.
proxy.on('proxyReq', function (proxyReq, req) {
var conn = req.connection;
var opts = conn.__opts;
proxyReq.setHeader('X-Forwarded-For', opts.remoteAddress || conn.remoteAddress);
});
proxy.on('error', function (err, req, res) {
console.log(err);
res.statusCode = 502;
res.setHeader('Content-Type', 'text/html');
res.setHeader('Connection', 'close');
res.end(require('../proxy-err-resp').getRespBody(err, conf.debug));
});
return {
web: function (req, res, next) {
if (moduleMatchesHost(req, mod)) {
proxy.web(req, res);
} else {
next();
}
}
, ws: function (req, socket, head, next) {
if (moduleMatchesHost(req, mod)) {
proxy.ws(req, socket, head);
} else {
next();
}
}
};
}
function createRedirectRoute(mod) {
// Escape any characters that (can) have special meaning in regular expression
// but that aren't the special characters we have interest in.
var from = mod.from.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&');
// Then modify the characters we are interested in so they do what we want in
// the regular expression after being compiled.
from = from.replace(/\*/g, '(.*)');
var fromRe = new RegExp('^' + from + '/?$');
return function (req, res, next) {
if (!moduleMatchesHost(req, mod)) {
next();
return;
}
var match = fromRe.exec(req.url);
if (!match) {
next();
return;
}
var to = mod.to;
match.slice(1).forEach(function (globMatch, index) {
to = to.replace(':'+(index+1), globMatch);
});
res.writeHead(mod.status || 301, { 'Location': to });
res.end();
};
}
function createStaticRoute(mod) {
var getStaticApp, staticApp;
if (/:hostname/.test(mod.root)) {
staticApp = {};
getStaticApp = function (hostname) {
if (!staticApp[hostname]) {
staticApp[hostname] = express.static(mod.root.replace(':hostname', hostname));
}
return staticApp[hostname];
};
}
else {
staticApp = express.static(mod.root);
getStaticApp = function () {
return staticApp;
};
}
return function (req, res, next) {
if (moduleMatchesHost(req, mod)) {
getStaticApp(separatePort(req.headers.host).host)(req, res, next);
} else {
next();
}
};
}
app.use(greenlockMiddleware);
app.use(redirectHttps);
app.use(handleAdmin);
(conf.http.modules || []).forEach(function (mod) {
if (mod.name === 'proxy') {
var proxyRoute = createProxyRoute(mod);
proxyRoutes.push(proxyRoute);
app.use(proxyRoute.web);
}
else if (mod.name === 'redirect') {
app.use(createRedirectRoute(mod));
}
else if (mod.name === 'static') {
app.use(createStaticRoute(mod));
}
else {
console.warn('unknown HTTP module', mod);
}
});
app.use(respond404);
var server = require('http').createServer(function (req, res) {
app(req, res)
});
server.on('upgrade', function (req, socket, head) {
if (!proxyRoutes.length) {
socket.end();
}
var prs = proxyRoutes.slice();
function proxyWs() {
var proxyRoute = prs.shift();
if (!proxyRoute) {
socket.end();
return;
}
proxyRoute.ws(req, socket, head, proxyWs);
}
proxyWs();
});
return server;
};