diff --git a/README.md b/README.md index 61ea461c..8f2c5398 100644 --- a/README.md +++ b/README.md @@ -76,12 +76,14 @@ 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-config` |Pass in `.json` configuration file or stringified JSON. e.g.: `./path/to/config.json` | | |`--proxy-all` |Forward every request to the proxy target instead of serving local files|`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 | |`--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/bin/http-server b/bin/http-server index 4d3e1a1a..de063051 100755 --- a/bin/http-server +++ b/bin/http-server @@ -63,6 +63,7 @@ if (argv.h || argv.help) { ' -P --proxy Fallback proxy if the request cannot be resolved. e.g.: http://someurl.com', ' --proxy-all Send every request to the proxy target instead of serving local files', ' --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', ' --websocket Enable websocket proxy', '', ' --username Username for basic authentication [none]', @@ -89,6 +90,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'], websocket = argv.websocket, proxyAll = Boolean(argv['proxy-all']), utc = argv.U || argv.utc, @@ -124,6 +126,12 @@ if (!argv.s && !argv.silent) { chalk.red(error.status.toString()), chalk.red(error.message) ); } + else if (req.proxy) { + logger.info( + '[%s] %s "%s" (%s)-> "%s"', + date, ip, chalk.cyan(req.url), chalk.magenta('Proxy'), chalk.cyan(req.proxy.target) + ); + } else { logger.info( '[%s] %s "%s %s" "%s"', @@ -172,6 +180,7 @@ function listen(port) { logFn: logger.request, proxy: proxy, proxyOptions: proxyOptions, + proxyConfig: proxyConfig, proxyAll: proxyAll, showDotfiles: argv.dotfiles, mimetypes: argv.mimetypes, @@ -228,6 +237,38 @@ function listen(port) { } } + if (proxyConfig) { + try { + if (fs.existsSync(proxyConfig)) { + proxyConfig = fs.readFileSync(proxyConfig, 'utf8'); + } + if (typeof proxyConfig === 'string') { + proxyConfig = JSON.parse(proxyConfig); + } + if (typeof proxyConfig !== 'object') { + throw new Error('Invalid proxy config'); + } + } + catch (err) { + logger.info(chalk.red('Error: Invalid proxy config or file')); + process.exit(1); + } + // Proxy file overrides cli config + proxy = undefined; + proxyOptions = undefined; + } + + if (proxyAll && proxyConfig) { + logger.info(chalk.red('Error: --proxy-all cannot be used with --proxy-config')); + logger.info( + '%s\n%s\n%s', + chalk.yellow('Hint: Use'), + chalk.cyan('"/**": {\n "target": "your-proxy"\n}'), + chalk.yellow('in the proxy config to achieve the same effect.') + ); + process.exit(1); + } + if (proxyAll && !proxy) { logger.info(chalk.red('Error: --proxy-all requires --proxy to be set')); process.exit(1); diff --git a/doc/http-server.1 b/doc/http-server.1 index 3f962044..afb453e3 100644 --- a/doc/http-server.1 +++ b/doc/http-server.1 @@ -106,6 +106,10 @@ Requires \-\-proxy. .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. diff --git a/lib/http-server.js b/lib/http-server.js index 9092d828..200cd3dc 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -7,6 +7,7 @@ var fs = require('fs'), httpProxy = require('http-proxy'), corser = require('corser'), secureCompare = require('secure-compare'); +var { minimatch } = require('minimatch'); // // Remark: backwards compatibility for previous @@ -144,6 +145,58 @@ function HttpServer(options) { }); } + if (typeof options.proxyConfig === 'object') { + var proxy = httpProxy.createProxyServer(); + before.push(function (req, res, next) { + for (var key of Object.keys(options.proxyConfig)) { + if (!minimatch(req.url, key)) continue; + req.proxy ??= {}; + var matchConfig = options.proxyConfig[key]; + + 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 => req.proxy[entry[0]] = entry[1]); + break; + } + + if (req.proxy) { + if (options.logFn) { + options.logFn(req, res); + } + proxy.web(req, res, req.proxy, function (err, req, res) { + if (options.logFn) { + options.logFn(req, res, { + message: err.message, + status: res.statusCode }); + } + res.emit('next'); + }); + } else { + next(); + } + }); + } + + before.push(httpServerCore({ + root: this.root, + baseDir: options.baseDir, + cache: this.cache, + showDir: this.showDir, + showDotfiles: this.showDotfiles, + autoIndex: this.autoIndex, + defaultExt: this.ext, + gzip: this.gzip, + brotli: this.brotli, + contentType: this.contentType, + mimetypes: options.mimetypes, + handleError: typeof options.proxy !== 'string' + })); + if (!proxyAll) { before.push(httpServerCore({ root: this.root, diff --git a/package-lock.json b/package-lock.json index ac168127..71a1900d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "html-encoding-sniffer": "^3.0.0", "http-proxy": "^1.18.1", "mime": "^1.6.0", + "minimatch": "^10.1.1", "minimist": "^1.2.6", "opener": "^1.5.1", "portfinder": "^1.0.28", @@ -223,6 +224,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/eslintrc/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -241,6 +255,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1814,9 +1849,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2426,8 +2463,10 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" }, "node_modules/concat-stream": { "version": "1.6.2", @@ -3057,6 +3096,19 @@ "node": ">=4" } }, + "node_modules/eslint-find-rules/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-find-rules/node_modules/p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -3432,6 +3484,19 @@ "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", "dev": true }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3847,6 +3912,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/flat-cache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/flat-cache/node_modules/rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -4106,30 +4184,6 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -5365,15 +5419,18 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -7629,15 +7686,6 @@ "node": "20 || >=22" } }, - "node_modules/tshy/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/tshy/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -7650,21 +7698,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/tshy/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/tshy/node_modules/mkdirp": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", @@ -8530,6 +8563,15 @@ "esprima": "^4.0.0" } }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8544,6 +8586,19 @@ } } }, + "@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==" + }, + "@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "requires": { + "@isaacs/balanced-match": "^4.0.1" + } + }, "@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -9668,8 +9723,9 @@ } }, "brace-expansion": { - "version": "1.1.11", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -10102,7 +10158,8 @@ }, "concat-map": { "version": "0.0.1", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "concat-stream": { @@ -10601,6 +10658,15 @@ "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", "dev": true }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10772,6 +10838,15 @@ "path-exists": "^3.0.0" } }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -11220,6 +11295,15 @@ "path-is-absolute": "^1.0.0" } }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -11374,26 +11458,6 @@ "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } } }, "glob-parent": { @@ -12312,12 +12376,11 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "requires": { - "brace-expansion": "^1.1.7" + "@isaacs/brace-expansion": "^5.0.0" } }, "minimist": { @@ -14042,30 +14105,12 @@ "walk-up-path": "^4.0.0" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, "chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true }, - "minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, "mkdirp": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", diff --git a/package.json b/package.json index 7bf90a0f..d4417f7c 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "html-encoding-sniffer": "^3.0.0", "http-proxy": "^1.18.1", "mime": "^1.6.0", + "minimatch": "^10.1.1", "minimist": "^1.2.6", "opener": "^1.5.1", "portfinder": "^1.0.28", diff --git a/test/proxy-config.test.js b/test/proxy-config.test.js new file mode 100644 index 00000000..05db6166 --- /dev/null +++ b/test/proxy-config.test.js @@ -0,0 +1,103 @@ +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": "" + } + } +} + +// 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({ + root, + //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(), 'none 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() + }) +})