diff --git a/README.md b/README.md index ec6f4c9a..a27a3f2e 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ This will install `http-server` globally so that it may be run from the command |`-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 + |`--username` |Username for basic authentication | | |`--password` |Password for basic authentication | | |`-S` or `--ssl` |Enable https.| | diff --git a/bin/http-server b/bin/http-server index 1a759085..298f0553 100755 --- a/bin/http-server +++ b/bin/http-server @@ -39,6 +39,7 @@ if (argv.h || argv.help) { ' --log-ip Enable logging of the client\'s IP address', '', ' -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', '', ' --username Username for basic authentication [none]', ' Can also be specified with the env variable NODE_HTTP_SERVER_USERNAME', @@ -62,10 +63,24 @@ var port = argv.p || argv.port || parseInt(process.env.PORT, 10), host = argv.a || '0.0.0.0', ssl = argv.S || argv.ssl, proxy = argv.P || argv.proxy, + proxyOptions = argv['proxy-options'], utc = argv.U || argv.utc, version = argv.v || argv.version, logger; +var proxyOptionsBooleanProps = [ + 'ws', 'xfwd', 'secure', 'toProxy', 'prependPath', 'ignorePath', 'changeOrigin', + 'preserveHeaderKeyCase', 'followRedirects', 'selfHandleResponse' +]; + +if (proxyOptions) { + Object.keys(proxyOptions).forEach(function (key) { + if (proxyOptionsBooleanProps.indexOf(key) > -1) { + proxyOptions[key] = proxyOptions[key].toLowerCase() === 'true'; + } + }); +} + if (!argv.s && !argv.silent) { logger = { info: console.log, @@ -127,6 +142,7 @@ function listen(port) { ext: argv.e || argv.ext, logFn: logger.request, proxy: proxy, + proxyOptions: proxyOptions, showDotfiles: argv.dotfiles, mimetypes: argv.mimetypes, username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME, @@ -196,7 +212,12 @@ function listen(port) { } if (typeof proxy === 'string') { - logger.info('Unhandled requests will be served from: ' + proxy); + if (proxyOptions) { + logger.info('Unhandled requests will be served from: ' + proxy + '. Options: ' + JSON.stringify(proxyOptions)); + } + else { + logger.info('Unhandled requests will be served from: ' + proxy); + } } logger.info('Hit CTRL-C to stop the server'); diff --git a/doc/http-server.1 b/doc/http-server.1 index 671e156d..b543064c 100644 --- a/doc/http-server.1 +++ b/doc/http-server.1 @@ -85,6 +85,10 @@ Enable logging of the client IP address. .BI \-P ", " \-\-proxy Fallback proxy if the request cannot be resolved. +.TP +.BI \-\-proxy\-options +Pass proxy options using nested dotted objects. + .TP .BI \-\-username " " \fIUSERNAME\fR Username for basic authentication. diff --git a/lib/http-server.js b/lib/http-server.js index 996db6fd..4b69cffe 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -141,7 +141,8 @@ function HttpServer(options) { })); if (typeof options.proxy === 'string') { - var proxy = httpProxy.createProxyServer({}); + var proxyOptions = options.proxyOptions || {}; + var proxy = httpProxy.createProxyServer(proxyOptions); before.push(function (req, res) { proxy.web(req, res, { target: options.proxy, diff --git a/test/fixtures/https/agent2-cert.pem b/test/fixtures/https/agent2-cert.pem new file mode 100644 index 00000000..852d09f9 --- /dev/null +++ b/test/fixtures/https/agent2-cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEIDCCAggCCQChRDh/XiBF+zANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJ1 +czETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEeMBwGA1UE +AwwVRHVtbXkgSW50ZXJtZWRpYXRlIENBMB4XDTE4MDYyMjIwMzEwNFoXDTMyMDIy +OTIwMzEwNFowUDELMAkGA1UEBhMCdXMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAO +BgNVBAcMB1NlYXR0bGUxGjAYBgNVBAMMEWR1bW15LmV4YW1wbGUuY29tMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJ +SACvkGCQUCJqOceESbg6IHdRzQdoccE4P3sbvNsf9BlbdJKM+neCxabqKaU1PPje +4P0tHT57t6yJrMuUh9NxEz3Bgh1srNHVS7saKvwHmcKm79jc+wxlioPmEQvQagjn +y7oTkyLt0sn4LGxBjrcv2JoHOC9f1pxX7l47MaiN0/ctRau7Nr3PFn+pkB4Yf6Z0 +VyicVJbaUSz39Qo4HQWl1L2hiBP3CS1oKs2Yk0O1aOCMExWrhZQan+ZgHqL1rhgm +kPpw2/zwwPt5Vf9CSakvHwg198EXuTTXtkzYduuIJAm8yp969iEIiG2xTwIDAQAB +MA0GCSqGSIb3DQEBCwUAA4ICAQBnMSIo+kujkeXPh+iErFBmNtu/7EA+i/QnFPbN +lSLngclYYBJAGQI+DhirJI8ghDi6vmlHB2THewDaOJXEKvC1czE8064wioIcA9HJ +l3QJ3YYOFRctYdSHBU4TWdJbPgkLWDzYP5smjOfw8nDdr4WO/5jh9qRFcFpTFmQf +DyU3xgWLsQnNK3qXLdJjWG75pEhHR+7TGo+Ob/RUho/1RX/P89Ux7/oVbzdKqqFu +SErXAsjEIEFzWOM2uDOt6hrxDF6q+8/zudwQNEo422poEcTT9tDEFxMQ391CzZRi +nozBm4igRn1f5S3YZzLI6VEUns0s76BNy2CzvFWn40DziTqNBExAMfFFj76wiMsX +6fTIdcvkaTBa0S9SZB0vN99qahBdcG17rt4RssMHVRH1Wn7NXMwe476L0yXZ6gO7 +Z4uNAPxgaI3BRP75EPfslLutCLZ+BC4Zzu6MY0Salbpfl0Go462EhsKCxvYhE2Dg +T477pICLfETZfA499Fd1tOaIsoLCrILAia/+Yd76uf94MuXUIqykDng/4H7xCc47 +BZhNFJiPC6XHaXzN7NYSEUNX9VOwY8ncxKwtP6TXga96PdMUy/p98KIM8RZlDoxB +Xy9dcZBFNn/zrqjW7R0CCWCUriDIFSmEP0wDZ91YYa6BVuJMb5uL/USkTLpjZS4/ +HNGvug== +-----END CERTIFICATE----- diff --git a/test/fixtures/https/agent2-key.pem b/test/fixtures/https/agent2-key.pem new file mode 100644 index 00000000..4de9ee77 --- /dev/null +++ b/test/fixtures/https/agent2-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJSACvkGCQUCJqOceESbg6 +IHdRzQdoccE4P3sbvNsf9BlbdJKM+neCxabqKaU1PPje4P0tHT57t6yJrMuUh9Nx +Ez3Bgh1srNHVS7saKvwHmcKm79jc+wxlioPmEQvQagjny7oTkyLt0sn4LGxBjrcv +2JoHOC9f1pxX7l47MaiN0/ctRau7Nr3PFn+pkB4Yf6Z0VyicVJbaUSz39Qo4HQWl +1L2hiBP3CS1oKs2Yk0O1aOCMExWrhZQan+ZgHqL1rhgmkPpw2/zwwPt5Vf9CSakv +Hwg198EXuTTXtkzYduuIJAm8yp969iEIiG2xTwIDAQABAoIBAGPIw/C/qJF7HYyv +6T+7GTiaa2o0IiehbP3/Y8NTFLWc49a8obXlHTvMr7Zr2I/tE+ojtIzkH9K1SjkN +eelqsNj9tsOPDI6oIvftsflpxkxqLtclnt8m0oMhoObf4OaONDT/N8dP4SBiSdsM +ZDmacnMFx5NZVWiup4sVf2CYexx7qks9FhyN2K5PArCQ4S9LHjFhSJVH4DSEpv7E +Ykbp30rhpqV7wSwjgUsm8ZYvI2NOlmffzLSiPdt3vy2K5Q25S/MVEAicg83rfDgK +6EluHjeygRI1xU6DJ0hU7tnU7zE9KURoHPUycO3BKzZnzUH26AA36I58Pu4fXWw/ +Cgmbv2ECgYEA+og9E4ziKCEi3p8gqjIfwTRgWZxDLjEzooB/K0UhEearn/xiX29A +FiSzEHKfCB4uSrw5OENg2ckDs8uy08Qmxx7xFXL7AtufAl5fIYaWa0sNSqCaIk7p +ebbUmPcaYhKiLzIEd1EYEL38sXVZ62wvSVMRSWvEMq44g1qnoRlDa/8CgYEAwUTt +talYNwVmR9ZdkVEWm9ZxirdzoM6NaM6u4Tf34ygptpapdmIFSUhfq4iOiEnRGNg/ +tuNqhNCIb3LNpJbhRPEzqN7E7qiF/mp7AcJgbuxLZBm12QuLuJdG3nrisKPFXcY1 +lA4A7CFmNgH3E4THFfgwzyDXsBOxVLXleTqn+rECgYEA9up1P6J3dtOJuV2d5P/3 +ugRz/X173LfTSxJXw36jZDAy8D/feG19/RT4gnplcKvGNhQiVOhbOOnbw0U8n2fQ +TCmbs+cZqyxnH/+AxNsPvvk+RVHZ93xMsY/XIldP4l65B8jFDA+Zp06IESI2mEeM +pzi+bd1Phh+dRSCA2865W2MCgYEAlxYsgmQ1WyX0dFpHYU+zzfXRYzDQyrhOYc2Z +duVK+yCto1iad7pfCY/zgmRJkI+sT7DV9kJIRjXDQuTLkEyHJF8vFGe6KhxCS8aw +DIsI2g4NTd6vg1J8UryoIUqNpqsQoqNNxUVBQVdG0ReuMGsPO8R/W50AIFz0txVP +o/rP0LECgYEA7e/mOwCnR+ovmS/CAksmos3oIqvkRkXNKpKe513FVmp3TpTU38ex +cBkFNU3hEO31FyrX1hGIKp3N5mHYSQ1lyODHM6teHW0OLWWTwIe8rIGvR2jfRLe0 +bbkdj40atYVkfeFmpz9uHHG24CUYxJdPc360jbXTVp4i3q8zqgL5aMY= +-----END RSA PRIVATE KEY----- diff --git a/test/proxy-options.test.js b/test/proxy-options.test.js new file mode 100644 index 00000000..07126925 --- /dev/null +++ b/test/proxy-options.test.js @@ -0,0 +1,96 @@ +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') +} + +// 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 options', (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 + }) + server.listen(8080, async () => { + try { + + // Another server proxies 8081 to 8080 + const proxyServer = httpServer.createServer({ + proxy: 'http://localhost:8080', + root: path.join(__dirname, 'fixtures'), + ssl: true, + https: httpsOpts, + proxyOptions: { + secure: false + } + }) + + await new Promise((resolve) => { + proxyServer.listen(8081, async () => { + try { + // Serve files from proxy root + await requestAsync('https://localhost:8081/root/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 root file content matches') + }).catch(err => t.fail(err.toString())) + + // Proxy fallback + await requestAsync('https://localhost:8081/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(), 'proxy fallback root 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() + }) +})