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:
parent
474f9766d8
commit
febe106a81
|
@ -1,12 +1,12 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports.create = function (deps, conf, greenlockMiddleware) {
|
module.exports.create = function (deps, conf, greenlockMiddleware) {
|
||||||
|
var PromiseA = require('bluebird');
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
var app = express();
|
var app = express();
|
||||||
var adminApp = require('./admin').create(deps, conf);
|
var adminApp = require('./admin').create(deps, conf);
|
||||||
var domainMatches = require('../domain-utils').match;
|
var domainMatches = require('../domain-utils').match;
|
||||||
var separatePort = require('../domain-utils').separatePort;
|
var separatePort = require('../domain-utils').separatePort;
|
||||||
var proxyRoutes = [];
|
|
||||||
|
|
||||||
var adminDomains = [
|
var adminDomains = [
|
||||||
/\blocalhost\.admin\./
|
/\blocalhost\.admin\./
|
||||||
|
@ -15,8 +15,65 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
|
||||||
, /\balpha\.localhost\./
|
, /\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) {
|
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 mod.domains.some(function (pattern) {
|
||||||
return domainMatches(pattern, host);
|
return domainMatches(pattern, host);
|
||||||
|
@ -92,54 +149,6 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
|
||||||
res.end('Not Found');
|
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) {
|
function createRedirectRoute(mod) {
|
||||||
// Escape any characters that (can) have special meaning in regular expression
|
// Escape any characters that (can) have special meaning in regular expression
|
||||||
// but that aren't the special characters we have interest in.
|
// 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);
|
app.use(handleAdmin);
|
||||||
|
|
||||||
(conf.http.modules || []).forEach(function (mod) {
|
(conf.http.modules || []).forEach(function (mod) {
|
||||||
if (mod.name === 'proxy') {
|
if (mod.name === 'redirect') {
|
||||||
var proxyRoute = createProxyRoute(mod);
|
|
||||||
proxyRoutes.push(proxyRoute);
|
|
||||||
app.use(proxyRoute.web);
|
|
||||||
}
|
|
||||||
else if (mod.name === 'redirect') {
|
|
||||||
app.use(createRedirectRoute(mod));
|
app.use(createRedirectRoute(mod));
|
||||||
}
|
}
|
||||||
else if (mod.name === 'static') {
|
else if (mod.name === 'static') {
|
||||||
|
@ -220,27 +224,121 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
|
||||||
|
|
||||||
app.use(respond404);
|
app.use(respond404);
|
||||||
|
|
||||||
var server = require('http').createServer(function (req, res) {
|
var server = require('http').createServer(app);
|
||||||
app(req, res)
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on('upgrade', function (req, socket, head) {
|
function handleHttp(conn, opts) {
|
||||||
if (!proxyRoutes.length) {
|
server.emit('connection', conn);
|
||||||
socket.end();
|
|
||||||
|
// 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();
|
return handleHttp(conn, opts);
|
||||||
function proxyWs() {
|
}
|
||||||
var proxyRoute = prs.shift();
|
|
||||||
if (!proxyRoute) {
|
function checkRedirect(conn, opts, headers) {
|
||||||
socket.end();
|
if (conf.http.allowInsecure || conn.encrypted) {
|
||||||
return;
|
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;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,7 +47,6 @@
|
||||||
"finalhandler": "^0.4.0",
|
"finalhandler": "^0.4.0",
|
||||||
"greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master",
|
"greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master",
|
||||||
"greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master",
|
"greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master",
|
||||||
"http-proxy": "^1.16.2",
|
|
||||||
"httpolyglot": "^0.1.1",
|
"httpolyglot": "^0.1.1",
|
||||||
"ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0",
|
"ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0",
|
||||||
"ipify": "^1.1.0",
|
"ipify": "^1.1.0",
|
||||||
|
|
Loading…
Reference in New Issue