diff --git a/README.md b/README.md index e63e6c5aa..ce6bcf360 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,9 @@ with the provided Dockerfile. |`-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| | -|`--hide-permissions` |Do not show file permissions| | |`--mimetypes` |Path to a .types file for custom mimetype definition| | +|`--hide-permissions` |Do not show file permissions| | +|`--allowed-hosts` |Comma-separated list of hosts allowed to access the server. e.g.: `--allowed-hosts localhost,example.com`| | |`-h` or `--help` |Print this list and exit. | | |`-v` or `--version`|Print the version and exit. | | | `--no-panic` | Don't print error stack in the console, put it in a log file | `false`| diff --git a/bin/http-server b/bin/http-server index 26ceffa3b..444d91010 100755 --- a/bin/http-server +++ b/bin/http-server @@ -87,7 +87,8 @@ if (argv.h || argv.help) { ' -h --help Print this list and exit.', ' -v --version Print the version and exit.', ' --no-panic If error occurs, gracefully shut down and create log file', - ' Can also be specified with the env variable NODE_HTTP_SERVER_NO_PANIC' + ' Can also be specified with the env variable NODE_HTTP_SERVER_NO_PANIC', + ' --allowed-hosts Comma-separated list of hosts allowed to access the server. e.g.: --allowed-hosts localhost,example.com', ].join('\n')); process.exit(); } @@ -106,7 +107,8 @@ var port = argv.p || argv.port || parseInt(process.env.PORT, 10), utc = argv.U || argv.utc, version = argv.v || argv.version, baseDir = argv['base-dir'], - logger; + logger, + allowedHosts = argv['allowed-hosts']; if (nopanic){ process.on('error', (e)=> { @@ -185,6 +187,12 @@ if (!port) { listen(port); } +if (allowedHosts && typeof allowedHosts === 'string') { + allowedHosts = allowedHosts.split(',').map((host) => host.trim().toLowerCase()); +} else { + allowedHosts = undefined; +} + function listen(port) { var options = { root: argv._[0], @@ -209,7 +217,8 @@ function listen(port) { contentType: argv['content-type'], username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME, password: argv.password || process.env.NODE_HTTP_SERVER_PASSWORD, - headers: {} + headers: {}, + allowedHosts, }; function setHeader(str) { @@ -369,8 +378,11 @@ function listen(port) { logger.info(chalk.yellow('\nAvailable on:')); - - if (argv.a && (host !== '::' || host !== '0.0.0.0')) { + if (allowedHosts) { + for (const host of allowedHosts) { + logger.info(` ${protocol}${host}:${chalk.green(port.toString())}${path}`); + } + } else if (argv.a && (host !== '::' || host !== '0.0.0.0')) { logger.info(` ${protocol}${host}:${chalk.green(port.toString())}${path}`); } else { Object.keys(ifaces).forEach(function (dev) { diff --git a/doc/http-server.1 b/doc/http-server.1 index 67ceabc15..e1f3b04be 100644 --- a/doc/http-server.1 +++ b/doc/http-server.1 @@ -180,6 +180,10 @@ Do not show dotfiles. .BI \-\-hide\-permissions Do not show file permissions. +.TP +.BI \-\-allowed\-hosts +Comma-separated list of hosts allowed to access the server. e.g.: \-\-allowed\-hosts localhost,example.com + .TP .BI \-h ", " \-\-help Print usage and exit. diff --git a/lib/http-server.js b/lib/http-server.js index eec54e557..8d8b79494 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -121,6 +121,24 @@ function HttpServer(options) { }); } + if (options.allowedHosts) { + before.push(function (req, res) { + let host = req.headers && req.headers.host; + if (host) { + // don't include port number in host check + host = host.split(':')[0]; + } + + if (!host || !options.allowedHosts.includes(host)) { + res.statusCode = 403; + res.end('Access denied'); + return; + } + + return res.emit('next'); + }); + } + if (options.coop) { this.headers['Cross-Origin-Opener-Policy'] = options.coopHeader || 'same-origin'; this.headers['Cross-Origin-Embedder-Policy'] = 'require-corp'; diff --git a/test/allowed-hosts.test.js b/test/allowed-hosts.test.js new file mode 100644 index 000000000..1dcd58387 --- /dev/null +++ b/test/allowed-hosts.test.js @@ -0,0 +1,57 @@ +const test = require('tap').test +const path = require('path') +const httpServer = require('../lib/http-server') +const request = require('request'); + +// Prevent errors from being swallowed +process.on('uncaughtException', console.error) + +test('allowed hosts functionality', (t) => { + t.plan(4); + new Promise((resolve) => { + const server = httpServer.createServer({ + root: path.join(__dirname, 'fixtures'), + allowedHosts: ['localhost'], + }) + + server.listen(0, async () => { + console.log('server listening on port', server.address().port) + const port = server.address().port + const url = `http://localhost:${port}` + + try { + await new Promise((resolve, reject) => { + request.get({ + url, + headers: { + Host: 'example.com' + } + }, (err, res) => { + console.log('response', err) + t.error(err); + t.equal(res.statusCode, 403); + resolve(); + }) + }) + + await new Promise((resolve, reject) => { + request.get({ + url, + headers: { + Host: 'localhost' + } + }, (err, res) => { + console.log('response', err) + t.error(err); + t.equal(res.statusCode, 200); + resolve(); + }) + }) + } catch (err) { + t.fail(`allowed hosts test failed: ${err.message}`) + } finally { + server.close() + } + }) + }) +}) \ No newline at end of file