changed how HTTP proxying works

Note that with the way it is currently, proxying modules take priority
over other modules even if they come later in the list.
This commit is contained in:
tigerbot 2017-05-16 17:19:26 -06:00
parent 474f9766d8
commit febe106a81
2 changed files with 172 additions and 75 deletions

View File

@ -1,12 +1,12 @@
'use strict';
module.exports.create = function (deps, conf, greenlockMiddleware) {
var PromiseA = require('bluebird');
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\./
@ -15,8 +15,65 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
, /\balpha\.localhost\./
];
function parseHeaders(conn, opts) {
// There should already be a `firstChunk` on the opts, but because we might sometimes
// need more than that to get all the headers it's easier to always read the data off
// the connection and put it back later if we need to.
opts.firstChunk = Buffer.alloc(0);
// First we make sure we have all of the headers.
return new PromiseA(function (resolve, reject) {
if (opts.firstChunk.includes('\r\n\r\n')) {
resolve(opts.firstChunk.toString());
return;
}
var errored = false;
function handleErr(err) {
errored = true;
reject(err);
}
conn.once('error', handleErr);
function handleChunk(chunk) {
if (!errored) {
opts.firstChunk = Buffer.concat([opts.firstChunk, chunk]);
if (opts.firstChunk.includes('\r\n\r\n')) {
resolve(opts.firstChunk.toString());
conn.removeListener('error', handleErr);
} else {
conn.once('data', handleChunk);
}
}
}
conn.once('data', handleChunk);
}).then(function (firstStr) {
var headerSection = firstStr.split('\r\n\r\n')[0];
var lines = headerSection.split('\r\n');
var result = {};
lines.slice(1).forEach(function (line) {
var match = /(.*)\s*:\s*(.*)/.exec(line);
if (match) {
result[match[1].toLowerCase()] = match[2];
} else {
console.error('HTTP header line does not match pattern', line);
}
});
var match = /^([a-zA-Z]+)\s+(\S+)\s+HTTP/.exec(lines[0]);
if (!match) {
throw new Error('first line of "HTTP" does not match pattern: '+lines[0]);
}
result.method = match[1].toUpperCase();
result.url = match[2];
return result;
});
}
function moduleMatchesHost(req, mod) {
var host = separatePort(req.headers.host).host;
var host = separatePort((req.headers || req).host).host;
return mod.domains.some(function (pattern) {
return domainMatches(pattern, host);
@ -92,54 +149,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
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.
@ -202,12 +211,7 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
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') {
if (mod.name === 'redirect') {
app.use(createRedirectRoute(mod));
}
else if (mod.name === 'static') {
@ -220,27 +224,121 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
app.use(respond404);
var server = require('http').createServer(function (req, res) {
app(req, res)
});
var server = require('http').createServer(app);
server.on('upgrade', function (req, socket, head) {
if (!proxyRoutes.length) {
socket.end();
function handleHttp(conn, opts) {
server.emit('connection', conn);
// We need to put back whatever data we read off to determine the connection was HTTP
// and to parse the headers. Must be done after data handlers added but before any new
// data comes in.
process.nextTick(function () {
conn.unshift(opts.firstChunk);
});
// Convenience return for all the check* functions.
return true;
}
function checkACME(conn, opts, headers) {
if (headers.url.indexOf('/.well-known/acme-challenge/') !== 0) {
return false;
}
var prs = proxyRoutes.slice();
function proxyWs() {
var proxyRoute = prs.shift();
if (!proxyRoute) {
socket.end();
return;
return handleHttp(conn, opts);
}
function checkRedirect(conn, opts, headers) {
if (conf.http.allowInsecure || conn.encrypted) {
return false;
}
if (conf.http.trustProxy && 'https' === headers['x-forwarded-proto']) {
return false;
}
return handleHttp(conn, opts);
}
function checkAdmin(conn, opts, headers) {
var admin = adminDomains.some(function (re) {
return re.test(headers.host);
});
if (admin) {
return handleHttp(conn, opts);
}
return false;
}
function checkProxy(mod, conn, opts, headers) {
if (!moduleMatchesHost(headers, mod)) {
return false;
}
var connected = false;
var newConnOpts = separatePort(mod.address);
newConnOpts.servername = separatePort(headers.host).host;
newConnOpts.data = opts.firstChunk;
newConnOpts.remoteFamily = opts.family || conn.remoteFamily;
newConnOpts.remoteAddress = opts.address || conn.remoteAddress;
newConnOpts.remotePort = opts.port || conn.remotePort;
var newConn = deps.net.createConnection(newConnOpts, function () {
connected = true;
newConn.write(opts.firstChunk);
newConn.pipe(conn);
conn.pipe(newConn);
});
// Not sure how to effectively report this to the user or client, but we need to listen
// for the event to prevent it from crashing us.
newConn.on('error', function (err) {
if (connected) {
console.error('HTTP proxy remote error', err);
conn.end();
} else {
require('../proxy-err-resp').sendBadGateway(conn, err, conf.debug);
}
});
conn.on('error', function (err) {
console.error('HTTP proxy client error', err);
newConn.end();
});
return true;
}
function handleConnection(conn) {
var opts = conn.__opts;
parseHeaders(conn, opts)
.then(function (headers) {
if (checkACME(conn, opts, headers)) { return; }
if (checkRedirect(conn, opts, headers)) { return; }
if (checkAdmin(conn, opts, headers)) { return; }
var handled = (conf.http.modules || []).some(function (mod) {
if (mod.name === 'proxy') {
return checkProxy(mod, conn, opts, headers);
}
});
if (handled) {
return;
}
server.emit('connection', conn);
process.nextTick(function () {
conn.unshift(opts.firstChunk);
});
})
;
}
return {
emit: function (type, value) {
if (type === 'connection') {
handleConnection(value);
}
proxyRoute.ws(req, socket, head, proxyWs);
}
proxyWs();
});
return server;
};
};

View File

@ -47,7 +47,6 @@
"finalhandler": "^0.4.0",
"greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master",
"greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master",
"http-proxy": "^1.16.2",
"httpolyglot": "^0.1.1",
"ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0",
"ipify": "^1.1.0",