diff --git a/README.md b/README.md index 968c53e6e..006fc0f1a 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,18 @@ Using `npx` you can run the script without installing it first: `-v` or `--version` Print the version and exit. +### Configuration file + +In addition to command line options, `http-server` will look for options in an `.httpserverrc` file: + + { + "p": "8888", + "a": "localhost", + "silent": true + } + +The search for `.httpserverrc` starts in the directory to be served, traversing up the directory tree until one is found. + ## Magic Files - `index.html` will be served as the default file to any directory requests. diff --git a/bin/http-server b/bin/http-server index 83e69084e..634322eed 100755 --- a/bin/http-server +++ b/bin/http-server @@ -8,74 +8,163 @@ var colors = require('colors/safe'), portfinder = require('portfinder'), opener = require('opener'), fs = require('fs'), - argv = require('optimist') - .boolean('cors') - .boolean('log-ip') - .argv, + nconf = require('nconf'), + path = require('path'), pjson = require('../package.json'); -var ifaces = os.networkInterfaces(); +var config = nconf.argv({ + p: { + alias: 'port', + describe: 'Port to use. By default, searches for an open port starting from 8080.', + number: true + }, + a: { + default: '0.0.0.0', + describe: 'Address to use' + }, + d: { + boolean: true, + default: true, + describe: 'Show directory listings' + }, + i: { + boolean: true, + default: true, + describe: 'Display autoIndex' + }, + g: { + alias: 'gzip', + boolean: true, + default: false, + describe: 'Serve gzip files when possible' + }, + b: { + alias: 'brotli', + boolean: true, + default: false, + describe: 'Serve brotli files when possible. If both brotli and gzip are enabled, brotli takes precedence.' + }, + e: { + alias: 'ext', + describe: 'Default file extension if none supplied' + }, + s: { + alias: 'silent', + boolean: true, + describe: 'Suppress log messages from output' + }, + cors: { + describe: 'Enable CORS via the "Access-Control-Allow-Origin" header. Optionally provide CORS headers list separated by commas.' + }, + o: { + describe: 'Open browser window after starting the server. Optionally provide a URL path to open the browser window to.' + }, + c: { + default: 3600, + describe: 'Cache time (max-age) in seconds, e.g. -c10 for 10 seconds. To disable caching, use -c-1.', + number: true + }, + t: { + describe: 'Connections timeout in seconds [120], e.g. -t60 for 1 minute. To disable timeout, use -t0', + number: true + }, + U: { + alias: 'utc', + boolean: true, + describe: 'Use UTC time format in log messages.' + }, + 'log-ip': { + boolean: true, + describe: 'Enable logging of the client\'s IP address' + }, + P: { + alias: 'proxy', + describe: 'Fallback proxy if the request cannot be resolved. e.g.: http://someurl.com' + }, + username: { + describe: 'Username for basic authentication. Can also be specified with the env variable NODE_HTTP_SERVER_USERNAME.' + }, + password: { + describe: 'Password for basic authentication. Can also be specified with the env variable NODE_HTTP_SERVER_PASSWORD' + }, + S: { + alias: 'ssl', + boolean: true, + describe: 'Enable https.' + }, + C: { + alias: 'cert', + default: 'cert.pem', + describe: 'Path to ssl cert file.' + }, + K: { + alias: 'key', + default: 'key.pem', + describe: 'Path to ssl key file.' + }, + r: { + alias: 'robots', + default: 'User-agent: *\\nDisallow: /', + describe: 'Respond to /robots.txt' + }, + 'no-dotfiles': { + boolean: true, + describe: 'Do not show dotfiles' + }, + h: { + alias: 'help', + describe: 'Print this list and exit.' + }, + v: { + alias: 'version', + describe: 'Print the version and exit.' + } + }), + ifaces = os.networkInterfaces(); process.title = 'http-server'; -if (argv.h || argv.help) { +if (config.get('h')) { console.log([ 'usage: http-server [path] [options]', '', - 'options:', - ' -p --port Port to use [8080]', - ' -a Address to use [0.0.0.0]', - ' -d Show directory listings [true]', - ' -i Display autoIndex [true]', - ' -g --gzip Serve gzip files when possible [false]', - ' -b --brotli Serve brotli files when possible [false]', - ' If both brotli and gzip are enabled, brotli takes precedence', - ' -e --ext Default file extension if none supplied [none]', - ' -s --silent Suppress log messages from output', - ' --cors[=headers] Enable CORS via the "Access-Control-Allow-Origin" header', - ' Optionally provide CORS headers list separated by commas', - ' -o [path] Open browser window after starting the server.', - ' Optionally provide a URL path to open the browser window to.', - ' -c Cache time (max-age) in seconds [3600], e.g. -c10 for 10 seconds.', - ' To disable caching, use -c-1.', - ' -t Connections timeout in seconds [120], e.g. -t60 for 1 minute.', - ' To disable timeout, use -t0', - ' -U --utc Use UTC time format in log messages.', - ' --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', - '', - ' --username Username for basic authentication [none]', - ' Can also be specified with the env variable NODE_HTTP_SERVER_USERNAME', - ' --password Password for basic authentication [none]', - ' Can also be specified with the env variable NODE_HTTP_SERVER_PASSWORD', - '', - ' -S --ssl Enable https.', - ' -C --cert Path to ssl cert file (default: cert.pem).', - ' -K --key Path to ssl key file (default: key.pem).', - '', - ' -r --robots Respond to /robots.txt [User-agent: *\\nDisallow: /]', - ' --no-dotfiles Do not show dotfiles', - ' -h --help Print this list and exit.', - ' -v --version Print the version and exit.' + config.stores.argv.help() ].join('\n')); process.exit(); } -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, - utc = argv.U || argv.utc, +var root = config.get('_')[0]; + +if (!root) { + try { + fs.lstatSync('./public'); + root = './public'; + } + catch (err) { + root = './'; + } +} + +config.file({ + file: '.httpserverrc', + dir: path.resolve(root), + search: true +}); + +var port = config.get('p') || parseInt(process.env.PORT, 10), + host = config.get('a'), + ssl = config.get('S'), + proxy = config.get('P'), + utc = config.get('U'), logger, version = pjson.version; -if (!argv.s && !argv.silent) { +if (!config.get('s')) { logger = { info: console.log, request: function (req, res, error) { var date = utc ? new Date().toUTCString() : new Date(); - var ip = argv['log-ip'] + var ip = config.get('log-ip') ? req.headers['x-forwarded-for'] || '' + req.connection.remoteAddress : ''; if (error) { @@ -113,40 +202,40 @@ else { listen(port); } -if (argv.v || argv.version) { +if (config.get('v')) { console.log(version); process.exit(); } function listen(port) { var options = { - root: argv._[0], - cache: argv.c, - timeout: argv.t, - showDir: argv.d, - autoIndex: argv.i, - gzip: argv.g || argv.gzip, - brotli: argv.b || argv.brotli, - robots: argv.r || argv.robots, - ext: argv.e || argv.ext, + root: config.get('_')[0], + cache: config.get('c'), + timeout: config.get('t'), + showDir: config.get('d'), + autoIndex: config.get('i'), + gzip: config.get('g'), + brotli: config.get('b'), + robots: config.get('r'), + ext: config.get('e'), logFn: logger.request, proxy: proxy, - showDotfiles: argv.dotfiles, - username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME, - password: argv.password || process.env.NODE_HTTP_SERVER_PASSWORD + showDotfiles: config.get('dotfiles'), + username: config.get('username') || process.env.NODE_HTTP_SERVER_USERNAME, + password: config.get('password') || process.env.NODE_HTTP_SERVER_PASSWORD }; - if (argv.cors) { + if (config.get('cors')) { options.cors = true; - if (typeof argv.cors === 'string') { - options.corsHeaders = argv.cors; + if (typeof config.get('cors') === 'string') { + options.corsHeaders = config.get('cors'); } } if (ssl) { options.https = { - cert: argv.C || argv.cert || 'cert.pem', - key: argv.K || argv.key || 'key.pem' + cert: config.get('C'), + key: config.get('K') }; try { fs.lstatSync(options.https.cert); @@ -175,7 +264,7 @@ function listen(port) { colors.yellow('\nAvailable on:') ].join('')); - if (argv.a && host !== '0.0.0.0') { + if (config.get('a') && host !== '0.0.0.0') { logger.info((' ' + protocol + canonicalHost + ':' + colors.green(port.toString()))); } else { @@ -193,10 +282,11 @@ function listen(port) { } logger.info('Hit CTRL-C to stop the server'); - if (argv.o) { + var o = config.get('o'); + if (o) { var openUrl = protocol + canonicalHost + ':' + port; - if (typeof argv.o === 'string') { - openUrl += argv.o[0] === '/' ? argv.o : '/' + argv.o; + if (typeof o === 'string') { + openUrl += o[0] === '/' ? o : '/' + o; } logger.info('open: ' + openUrl); opener(openUrl); diff --git a/package-lock.json b/package-lock.json index 6b84be830..cace70b0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,8 +57,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "2.2.1", @@ -464,6 +463,11 @@ } } }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, "color": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/color/-/color-0.8.0.tgz", @@ -705,8 +709,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "deep-equal": { "version": "1.0.1", @@ -1290,11 +1293,15 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, + "ini": { + "version": "1.3.5", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/ini/-/ini-1.3.5.tgz", + "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" + }, "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" }, "is-buffer": { "version": "1.1.6", @@ -1311,6 +1318,14 @@ "number-is-nan": "^1.0.0" } }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, "is-integer": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-integer/-/is-integer-1.0.7.tgz", @@ -1548,7 +1563,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, "requires": { "invert-kv": "^1.0.0" } @@ -1716,6 +1730,53 @@ "integrity": "sha1-H5bWDjFBysG20FZTzg2urHY69qo=", "dev": true }, + "nconf": { + "version": "0.10.0", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/nconf/-/nconf-0.10.0.tgz", + "integrity": "sha1-2hKF7pXQqSLKbO51rc+GH0ggWtI=", + "requires": { + "async": "^1.4.0", + "ini": "^1.3.0", + "secure-keys": "^1.0.0", + "yargs": "^3.19.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "requires": { + "camelcase": "^2.0.1", + "cliui": "^3.0.3", + "decamelize": "^1.1.1", + "os-locale": "^1.4.0", + "string-width": "^1.0.1", + "window-size": "^0.1.4", + "y18n": "^3.2.0" + } + } + } + }, "ncp": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz", @@ -1766,8 +1827,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { "version": "0.9.0", @@ -1795,27 +1855,10 @@ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==" }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" - } - } - }, "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, "requires": { "lcid": "^1.0.0" } @@ -2172,6 +2215,11 @@ "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", "integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=" }, + "secure-keys": { + "version": "1.0.0", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/secure-keys/-/secure-keys-1.0.0.tgz", + "integrity": "sha1-8MgtmKOxOah3aogIBQuCRDEIf8o=" + }, "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", @@ -2263,6 +2311,16 @@ "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", "dev": true }, + "string-width": { + "version": "1.0.2", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -2285,7 +2343,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2588,10 +2645,14 @@ } } }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://plangrid.jfrog.io/plangrid/api/npm/npm/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } }, "wrappy": { "version": "1.0.2", @@ -2619,8 +2680,7 @@ "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" }, "yargs": { "version": "3.5.4", diff --git a/package.json b/package.json index c12cceede..6854db8ff 100644 --- a/package.json +++ b/package.json @@ -84,8 +84,8 @@ "corser": "^2.0.1", "ecstatic": "^3.3.2", "http-proxy": "^1.17.0", + "nconf": "^0.10.0", "opener": "^1.5.1", - "optimist": "~0.6.1", "portfinder": "^1.0.20", "secure-compare": "3.0.1", "union": "~0.5.0"