From 604e8bcdfd2da40737e37fb39a2816997b691c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20M=C3=BCll?= Date: Thu, 14 Nov 2024 13:11:26 +0100 Subject: [PATCH 1/5] added proxy config for complex proxy rules --- bin/http-server | 16 ++++++++++++++++ lib/http-server.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/bin/http-server b/bin/http-server index a5b79f2f..ae72e9fe 100755 --- a/bin/http-server +++ b/bin/http-server @@ -51,6 +51,7 @@ if (argv.h || argv.help) { '', ' -P --proxy Fallback proxy if the request cannot be resolved. e.g.: http://someurl.com', ' --proxy-options Pass options to proxy using nested dotted objects. e.g.: --proxy-options.secure false', + ' --proxy-config Pass in .json configuration file. e.g.: ./path/to/config.json', '', ' --username Username for basic authentication [none]', ' Can also be specified with the env variable NODE_HTTP_SERVER_USERNAME', @@ -76,6 +77,7 @@ var port = argv.p || argv.port || parseInt(process.env.PORT, 10), sslPassphrase = process.env.NODE_HTTP_SERVER_SSL_PASSPHRASE, proxy = argv.P || argv.proxy, proxyOptions = argv['proxy-options'], + proxyConfig = argv['proxy-config'], utc = argv.U || argv.utc, version = argv.v || argv.version, baseDir = argv['base-dir'], @@ -157,6 +159,7 @@ function listen(port) { logFn: logger.request, proxy: proxy, proxyOptions: proxyOptions, + proxyConfig: proxyConfig, showDotfiles: argv.dotfiles, mimetypes: argv.mimetypes, username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME, @@ -199,6 +202,19 @@ function listen(port) { } } + if (proxyConfig) { + try { + proxyConfig = JSON.parse(fs.readFileSync(proxyConfig)); + } + catch (err) { + logger.info(chalk.red('Error: Invalid proxy config file')); + process.exit(1); + } + // Proxy file overrides cli config + proxy = undefined; + proxyOptions = undefined; + } + if (tls) { options.https = { cert: argv.C || argv.cert || 'cert.pem', diff --git a/lib/http-server.js b/lib/http-server.js index 0e946ac7..5340b8f8 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -158,6 +158,37 @@ function HttpServer(options) { }); } + if (typeof options.proxyConfig === 'object') { + var proxy = httpProxy.createProxyServer(); + before.push(function (req, res) { + var matchOptions = {}; + + for (var key in Object.keys(options.proxyConfig)) { + var regex = new RegExp(key); + if (regex.test(req.url)) { + var matchConfig = options.proxyConfig[key]; + + Object.entries(matchConfig.pathRewrites).forEach(rewrite => { + req.url = req.url.replace(new RegExp(rewrite[0]), rewrite[1]); + }); + + var configEntries = Object.entries(matchConfig); + configEntries.forEach(entry => matchOptions.options[entry[0]] = matchOptions.options[entry[1]]); + break; + } + } + + proxy.web(req, res, matchOptions, function (err, req, res) { + if (options.logFn) { + options.logFn(req, res, { + message: err.message, + status: res.statusCode }); + } + res.emit('next'); + }); + }); + } + var serverOptions = { before: before, headers: this.headers, From 349461620d87bfe86a25c0f2bdeadbc06ae6f792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20M=C3=BCll?= Date: Thu, 14 Nov 2024 13:44:21 +0100 Subject: [PATCH 2/5] fixed wrong proxy config value assignment and filtered match config --- lib/http-server.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/http-server.js b/lib/http-server.js index 5340b8f8..0fa1fe40 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -168,12 +168,12 @@ function HttpServer(options) { if (regex.test(req.url)) { var matchConfig = options.proxyConfig[key]; - Object.entries(matchConfig.pathRewrites).forEach(rewrite => { + Object.entries(matchConfig.pathRewrite).forEach(rewrite => { req.url = req.url.replace(new RegExp(rewrite[0]), rewrite[1]); }); - var configEntries = Object.entries(matchConfig); - configEntries.forEach(entry => matchOptions.options[entry[0]] = matchOptions.options[entry[1]]); + var configEntries = Object.entries(matchConfig).filter(entry => entry[0] !== "pathRewrite"); + configEntries.forEach(entry => matchOptions.options[entry[0]] = entry[1]); break; } } From faa51e03a95e92586f953b5e983f3b0251029efc Mon Sep 17 00:00:00 2001 From: I3ene Date: Sun, 2 Mar 2025 11:36:31 +0100 Subject: [PATCH 3/5] added basic documentation for proxy-config --- README.md | 5 +++-- doc/http-server.1 | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 272a2f35..4d7efa43 100644 --- a/README.md +++ b/README.md @@ -76,11 +76,12 @@ with the provided Dockerfile. |`-U` or `--utc` |Use UTC time format in log messages.| | |`--log-ip` |Enable logging of the client's IP address |`false` | |`-P` or `--proxy` |Proxies all requests which can't be resolved locally to the given url. e.g.: -P http://someurl.com | | -|`--proxy-options` |Pass proxy [options](https://github.com/http-party/node-http-proxy#options) using nested dotted objects. e.g.: --proxy-options.secure false | +|`--proxy-options` |Pass proxy [options](https://github.com/http-party/node-http-proxy#options) using nested dotted objects. e.g.: --proxy-options.secure false | | +|`--proxy-config` |Pass in `.json` configuration file. e.g.: `./path/to/config.json` | | |`--username` |Username for basic authentication | | |`--password` |Password for basic authentication | | |`-S`, `--tls` or `--ssl` |Enable secure request serving with TLS/SSL (HTTPS)|`false`| -|`-C` or `--cert` |Path to ssl cert file |`cert.pem` | +|`-C` or `--cert` |Path to ssl cert file |`cert.pem` | |`-K` or `--key` |Path to ssl key file |`key.pem` | |`-r` or `--robots` | Automatically provide a /robots.txt (The content of which defaults to `User-agent: *\nDisallow: /`) | `false` | |`--no-dotfiles` |Do not show dotfiles| | diff --git a/doc/http-server.1 b/doc/http-server.1 index 1337c5a2..5ac7f5e8 100644 --- a/doc/http-server.1 +++ b/doc/http-server.1 @@ -93,6 +93,10 @@ Fallback proxy if the request cannot be resolved. .BI \-\-proxy\-options Pass proxy options using nested dotted objects. +.TP +.BI \-\-proxy\-config +Pass in .json configuration file. + .TP .BI \-\-username " " \fIUSERNAME\fR Username for basic authentication. From 79bd9521a795b5763c6ba9ef8ed3ecbef78decd1 Mon Sep 17 00:00:00 2001 From: I3ene Date: Sun, 2 Mar 2025 13:19:14 +0100 Subject: [PATCH 4/5] fixed proxy config handling --- lib/http-server.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/http-server.js b/lib/http-server.js index 0fa1fe40..a43dbe30 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -163,17 +163,22 @@ function HttpServer(options) { before.push(function (req, res) { var matchOptions = {}; - for (var key in Object.keys(options.proxyConfig)) { - var regex = new RegExp(key); + for (var key of Object.keys(options.proxyConfig)) { + // Parse simplified regex to real working regex + // TODO: Add single path element regex (e.g. '/some/$/path' -> '/some/[^/]*/path') + var regexString = key.replaceAll('*', '.*'); + var regex = new RegExp(regexString); if (regex.test(req.url)) { var matchConfig = options.proxyConfig[key]; - Object.entries(matchConfig.pathRewrite).forEach(rewrite => { - req.url = req.url.replace(new RegExp(rewrite[0]), rewrite[1]); - }); + if (matchConfig.pathRewrite) { + Object.entries(matchConfig.pathRewrite).forEach(rewrite => { + req.url = req.url.replace(new RegExp(rewrite[0]), rewrite[1]); + }); + } var configEntries = Object.entries(matchConfig).filter(entry => entry[0] !== "pathRewrite"); - configEntries.forEach(entry => matchOptions.options[entry[0]] = entry[1]); + configEntries.forEach(entry => matchOptions[entry[0]] = entry[1]); break; } } From 8cc06b4d6e0e7e7cb6ce952309d22135a5582c58 Mon Sep 17 00:00:00 2001 From: I3ene Date: Sun, 2 Mar 2025 13:32:53 +0100 Subject: [PATCH 5/5] added first basic proxy config test --- test/proxy-config.test.js | 107 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 test/proxy-config.test.js diff --git a/test/proxy-config.test.js b/test/proxy-config.test.js new file mode 100644 index 00000000..6eb23ef8 --- /dev/null +++ b/test/proxy-config.test.js @@ -0,0 +1,107 @@ +const test = require('tap').test +const path = require('path') +const fs = require('fs') +const request = require('request') +const httpServer = require('../lib/http-server') +const promisify = require('util').promisify + +const requestAsync = promisify(request) +const fsReadFile = promisify(fs.readFile) + +// Prevent errors from being swallowed +process.on('uncaughtException', console.error) + +const root = path.join(__dirname, 'fixtures', 'root') +const httpsOpts = { + key: path.join(__dirname, 'fixtures', 'https', 'agent2-key.pem'), + cert: path.join(__dirname, 'fixtures', 'https', 'agent2-cert.pem') +} + +const proxyConfigTest = { + "/rewrite/*": { + "target": "http://localhost:8082", + "pathRewrite": { + "^/rewrite": "" + } + }, + "*": { + "target": "http://localhost:8082", + "changeOrigin": true, + "secure": false + } +} + +// Tests are grouped into those which can run together. The groups are given +// their own port to run on and live inside a Promise. Tests are done when all +// Promise test groups complete. +test('proxy config', (t) => { + new Promise((resolve) => { + const server = httpServer.createServer({ + root, + robots: true, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Credentials': 'true' + }, + cors: true, + corsHeaders: 'X-Test', + ext: true, + brotli: true, + gzip: true + }) + // TODO #723 we should use portfinder + server.listen(8082, async () => { + try { + + // Another server proxies 8083 to 8082 + const proxyServer = httpServer.createServer({ + //tls: true, + //https: httpsOpts, + proxyConfig: proxyConfigTest + }) + + await new Promise((resolve) => { + proxyServer.listen(8083, async () => { + try { + // Serve files from proxy root + await requestAsync('http://localhost:8083/file', { rejectUnauthorized: false }).then(async (res) => { + t.ok(res) + t.equal(res.statusCode, 200) + + // File content matches + const fileData = await fsReadFile(path.join(root, 'file'), 'utf8') + t.equal(res.body.trim(), fileData.trim(), 'proxied file content matches') + }).catch(err => t.fail(err.toString())) + + // Serve files from proxy with rewrite + await requestAsync('http://localhost:8083/rewrite/file', { rejectUnauthorized: false }).then(async (res) => { + t.ok(res) + t.equal(res.statusCode, 200) + + // File content matches + const fileData = await fsReadFile(path.join(root, 'file'), 'utf8') + t.equal(res.body.trim(), fileData.trim(), 'proxied file content matches') + }).catch(err => t.fail(err.toString())) + } catch (err) { + t.fail(err.toString()) + } finally { + proxyServer.close() + resolve() + } + }) + }) + + } catch (err) { + t.fail(err.toString()) + } finally { + server.close() + resolve() + } + }) + }) + .then(() => t.end()) + .catch(err => { + t.fail(err.toString()) + t.end() + }) +})