Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ability to pass proxyOptions #688

Merged
merged 3 commits into from Oct 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -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.| |
Expand Down
23 changes: 22 additions & 1 deletion bin/http-server
Expand Up @@ -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',
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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');
Expand Down
4 changes: 4 additions & 0 deletions doc/http-server.1
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion lib/http-server.js
Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions 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-----
27 changes: 27 additions & 0 deletions 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-----
96 changes: 96 additions & 0 deletions 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()
})
})