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.
このコミットが含まれているのは:
コミット
febe106a81
@ -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;
|
||||
};
|
||||
};
|
||||
|
@ -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",
|
||||
|
読み込み中…
x
新しいイシューから参照
ユーザーをブロックする