From febe106a813b3c009ea8716dff9f73af1b5cc90c Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 16 May 2017 17:19:26 -0600 Subject: [PATCH] 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. --- lib/modules/http.js | 246 +++++++++++++++++++++++++++++++------------- package.json | 1 - 2 files changed, 172 insertions(+), 75 deletions(-) diff --git a/lib/modules/http.js b/lib/modules/http.js index 8a4da01..593e867 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -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; + }; }; diff --git a/package.json b/package.json index 6202868..197b0d4 100644 --- a/package.json +++ b/package.json @@ -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",