http-server
Advanced tools
Comparing version 13.0.2 to 14.0.0
@@ -8,2 +8,4 @@ #! /usr/bin/env node | ||
const url = require('url'); | ||
const { Readable } = require('stream'); | ||
const buffer = require('buffer'); | ||
const mime = require('mime'); | ||
@@ -16,2 +18,3 @@ const urlJoin = require('url-join'); | ||
const optsParser = require('./opts'); | ||
const htmlEncodingSniffer = require('html-encoding-sniffer'); | ||
@@ -33,5 +36,10 @@ let httpServerCore = null; | ||
return process.platform === 'win32' | ||
? normalized.replace(/\\/g, '/') : normalized; | ||
? normalized.replace(/\\/g, '/') : normalized; | ||
} | ||
const nonUrlSafeCharsRgx = /[\x00-\x1F\x20\x7F-\uFFFF]+/g; | ||
function ensureUriEncoded(text) { | ||
return text | ||
return String(text).replace(nonUrlSafeCharsRgx, encodeURIComponent); | ||
} | ||
@@ -44,5 +52,5 @@ // Check to see if we should try to compress a file with gzip. | ||
headers['accept-encoding'] | ||
.split(',') | ||
.some(el => ['*', 'compress', 'gzip', 'deflate'].indexOf(el.trim()) !== -1) | ||
; | ||
.split(',') | ||
.some(el => ['*', 'compress', 'gzip', 'deflate'].indexOf(el.trim()) !== -1) | ||
; | ||
} | ||
@@ -62,3 +70,3 @@ | ||
const stream = fs.createReadStream(gzipped, { start: 0, end: 1 }); | ||
let buffer = Buffer(''); | ||
let buffer = Buffer.from(''); | ||
let hasBeenCalled = false; | ||
@@ -167,5 +175,6 @@ | ||
if (opts.weakCompare && clientEtag !== serverEtag | ||
&& clientEtag !== `W/${serverEtag}` && `W/${clientEtag}` !== serverEtag) { | ||
&& clientEtag !== `W/${serverEtag}` && `W/${clientEtag}` !== serverEtag) { | ||
return false; | ||
} else if (!opts.weakCompare && (clientEtag !== serverEtag || clientEtag.indexOf('W/') === 0)) { | ||
} | ||
if (!opts.weakCompare && (clientEtag !== serverEtag || clientEtag.indexOf('W/') === 0)) { | ||
return false; | ||
@@ -231,3 +240,2 @@ } | ||
let contentType = mime.lookup(file, defaultType); | ||
let charSet; | ||
const range = (req.headers && req.headers.range); | ||
@@ -239,4 +247,13 @@ const lastModified = (new Date(stat.mtime)).toUTCString(); | ||
if (contentType && isTextFile(contentType)) { | ||
// Assume text types are utf8 | ||
contentType += '; charset=UTF-8'; | ||
if (stat.size < buffer.constants.MAX_LENGTH) { | ||
const bytes = fs.readFileSync(file); | ||
const sniffedEncoding = htmlEncodingSniffer(bytes, { | ||
defaultEncoding: 'UTF-8' | ||
}); | ||
contentType += `; charset=${sniffedEncoding}`; | ||
stream = Readable.from(bytes) | ||
} else { | ||
// Assume text types are utf8 | ||
contentType += '; charset=UTF-8'; | ||
} | ||
} | ||
@@ -323,3 +340,6 @@ | ||
stream = fs.createReadStream(file); | ||
// stream may already have been assigned during encoding sniffing. | ||
if (stream === null) { | ||
stream = fs.createReadStream(file); | ||
} | ||
@@ -330,2 +350,5 @@ stream.pipe(res); | ||
}); | ||
stream.on('close', () => { | ||
stream.destroy(); | ||
}) | ||
} | ||
@@ -335,70 +358,79 @@ | ||
function statFile() { | ||
fs.stat(file, (err, stat) => { | ||
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) { | ||
if (req.statusCode === 404) { | ||
// This means we're already trying ./404.html and can not find it. | ||
// So send plain text response with 404 status code | ||
status[404](res, next); | ||
} else if (!path.extname(parsed.pathname).length && defaultExt) { | ||
// If there is no file extension in the path and we have a default | ||
// extension try filename and default extension combination before rendering 404.html. | ||
middleware({ | ||
url: `${parsed.pathname}.${defaultExt}${(parsed.search) ? parsed.search : ''}`, | ||
headers: req.headers, | ||
}, res, next); | ||
} else { | ||
// Try to serve default ./404.html | ||
middleware({ | ||
url: (handleError ? `/${path.join(baseDir, `404.${defaultExt}`)}` : req.url), | ||
headers: req.headers, | ||
statusCode: 404, | ||
}, res, next); | ||
} | ||
} else if (err) { | ||
status[500](res, next, { error: err }); | ||
} else if (stat.isDirectory()) { | ||
if (!autoIndex && !opts.showDir) { | ||
status[404](res, next); | ||
return; | ||
} | ||
try { | ||
fs.stat(file, (err, stat) => { | ||
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) { | ||
if (req.statusCode === 404) { | ||
// This means we're already trying ./404.html and can not find it. | ||
// So send plain text response with 404 status code | ||
status[404](res, next); | ||
} else if (!path.extname(parsed.pathname).length && defaultExt) { | ||
// If there is no file extension in the path and we have a default | ||
// extension try filename and default extension combination before rendering 404.html. | ||
middleware({ | ||
url: `${parsed.pathname}.${defaultExt}${(parsed.search) ? parsed.search : ''}`, | ||
headers: req.headers, | ||
}, res, next); | ||
} else { | ||
// Try to serve default ./404.html | ||
const rawUrl = (handleError ? `/${path.join(baseDir, `404.${defaultExt}`)}` : req.url); | ||
const encodedUrl = ensureUriEncoded(rawUrl); | ||
middleware({ | ||
url: encodedUrl, | ||
headers: req.headers, | ||
statusCode: 404, | ||
}, res, next); | ||
} | ||
} else if (err) { | ||
status[500](res, next, { error: err }); | ||
} else if (stat.isDirectory()) { | ||
if (!autoIndex && !opts.showDir) { | ||
status[404](res, next); | ||
return; | ||
} | ||
// 302 to / if necessary | ||
if (!pathname.match(/\/$/)) { | ||
res.statusCode = 302; | ||
const q = parsed.query ? `?${parsed.query}` : ''; | ||
res.setHeader('location', `${parsed.pathname}/${q}`); | ||
res.end(); | ||
return; | ||
} | ||
// 302 to / if necessary | ||
if (!pathname.match(/\/$/)) { | ||
res.statusCode = 302; | ||
const q = parsed.query ? `?${parsed.query}` : ''; | ||
res.setHeader( | ||
'location', | ||
ensureUriEncoded(`${parsed.pathname}/${q}`) | ||
); | ||
res.end(); | ||
return; | ||
} | ||
if (autoIndex) { | ||
middleware({ | ||
url: urlJoin( | ||
encodeURIComponent(pathname), | ||
`/index.${defaultExt}` | ||
), | ||
headers: req.headers, | ||
}, res, (autoIndexError) => { | ||
if (autoIndexError) { | ||
status[500](res, next, { error: autoIndexError }); | ||
return; | ||
} | ||
if (opts.showDir) { | ||
showDir(opts, stat)(req, res); | ||
return; | ||
} | ||
if (autoIndex) { | ||
middleware({ | ||
url: urlJoin( | ||
encodeURIComponent(pathname), | ||
`/index.${defaultExt}` | ||
), | ||
headers: req.headers, | ||
}, res, (autoIndexError) => { | ||
if (autoIndexError) { | ||
status[500](res, next, { error: autoIndexError }); | ||
return; | ||
} | ||
if (opts.showDir) { | ||
showDir(opts, stat)(req, res); | ||
return; | ||
} | ||
status[403](res, next); | ||
}); | ||
return; | ||
} | ||
status[403](res, next); | ||
}); | ||
return; | ||
} | ||
if (opts.showDir) { | ||
showDir(opts, stat)(req, res); | ||
if (opts.showDir) { | ||
showDir(opts, stat)(req, res); | ||
} | ||
} else { | ||
serve(stat); | ||
} | ||
} else { | ||
serve(stat); | ||
} | ||
}); | ||
}); | ||
} catch (err) { | ||
status[500](res, next, { error: err.message }); | ||
} | ||
} | ||
@@ -412,16 +444,20 @@ | ||
function tryServeWithGzip() { | ||
fs.stat(gzippedFile, (err, stat) => { | ||
if (!err && stat.isFile()) { | ||
hasGzipId12(gzippedFile, (gzipErr, isGzip) => { | ||
if (!gzipErr && isGzip) { | ||
file = gzippedFile; | ||
serve(stat); | ||
} else { | ||
statFile(); | ||
} | ||
}); | ||
} else { | ||
statFile(); | ||
} | ||
}); | ||
try { | ||
fs.stat(gzippedFile, (err, stat) => { | ||
if (!err && stat.isFile()) { | ||
hasGzipId12(gzippedFile, (gzipErr, isGzip) => { | ||
if (!gzipErr && isGzip) { | ||
file = gzippedFile; | ||
serve(stat); | ||
} else { | ||
statFile(); | ||
} | ||
}); | ||
} else { | ||
statFile(); | ||
} | ||
}); | ||
} catch (err) { | ||
status[500](res, next, { error: err.message }); | ||
} | ||
} | ||
@@ -431,12 +467,16 @@ | ||
function tryServeWithBrotli(shouldTryGzip) { | ||
fs.stat(brotliFile, (err, stat) => { | ||
if (!err && stat.isFile()) { | ||
file = brotliFile; | ||
serve(stat); | ||
} else if (shouldTryGzip) { | ||
tryServeWithGzip(); | ||
} else { | ||
statFile(); | ||
} | ||
}); | ||
try { | ||
fs.stat(brotliFile, (err, stat) => { | ||
if (!err && stat.isFile()) { | ||
file = brotliFile; | ||
serve(stat); | ||
} else if (shouldTryGzip) { | ||
tryServeWithGzip(); | ||
} else { | ||
statFile(); | ||
} | ||
}); | ||
} catch (err) { | ||
status[500](res, next, { error: err.message }); | ||
} | ||
} | ||
@@ -443,0 +483,0 @@ |
@@ -121,3 +121,3 @@ 'use strict'; | ||
aliases.cors.forEach((k) => { | ||
if (isDeclared(k) && k) { | ||
if (isDeclared(k) && opts[k]) { | ||
handleOptionsMethod = true; | ||
@@ -124,0 +124,0 @@ headers['Access-Control-Allow-Origin'] = '*'; |
'use strict'; | ||
const styles = require('./styles'); | ||
const lastModifiedToString = require('./last-modified-to-string'); | ||
const permsToString = require('./perms-to-string'); | ||
@@ -98,3 +99,3 @@ const sizeToString = require('./size-to-string'); | ||
const isDir = file[1].isDirectory && file[1].isDirectory(); | ||
let href = `${parsed.pathname.replace(/\/$/, '')}/${encodeURIComponent(file[0])}`; | ||
let href = `./${encodeURIComponent(file[0])}`; | ||
@@ -118,2 +119,3 @@ // append trailing slash and query for dir entry | ||
html += | ||
`<td class="last-modified">${lastModifiedToString(file[1])}</td>` + | ||
`<td class="file-size"><code>${sizeToString(file[1], humanReadable, si)}</code></td>` + | ||
@@ -120,0 +122,0 @@ `<td class="display-name"><a href="${href}">${displayName}</a></td>` + |
'use strict'; | ||
var fs = require('fs'), | ||
union = require('union'), | ||
httpServerCore = require('./core'), | ||
auth = require('basic-auth'), | ||
httpProxy = require('http-proxy'), | ||
corser = require('corser'), | ||
path = require('path'), | ||
secureCompare = require('secure-compare'); | ||
union = require('union'), | ||
httpServerCore = require('./core'), | ||
auth = require('basic-auth'), | ||
httpProxy = require('http-proxy'), | ||
corser = require('corser'), | ||
secureCompare = require('secure-compare'); | ||
@@ -36,9 +35,8 @@ // | ||
this.root = options.root; | ||
} | ||
else { | ||
} else { | ||
try { | ||
// eslint-disable-next-line no-sync | ||
fs.lstatSync('./public'); | ||
this.root = './public'; | ||
} | ||
catch (err) { | ||
} catch (err) { | ||
this.root = './'; | ||
@@ -52,7 +50,8 @@ } | ||
this.cache = ( | ||
// eslint-disable-next-line no-nested-ternary | ||
options.cache === undefined ? 3600 : | ||
// -1 is a special case to turn off caching. | ||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Preventing_caching | ||
options.cache === -1 ? 'no-cache, no-store, must-revalidate' : | ||
options.cache // in seconds. | ||
options.cache === -1 ? 'no-cache, no-store, must-revalidate' : | ||
options.cache // in seconds. | ||
); | ||
@@ -109,3 +108,3 @@ this.showDir = options.showDir !== 'false'; | ||
options.corsHeaders.split(/\s*,\s*/) | ||
.forEach(function (h) { this.headers['Access-Control-Allow-Headers'] += ', ' + h; }, this); | ||
.forEach(function (h) { this.headers['Access-Control-Allow-Headers'] += ', ' + h; }, this); | ||
} | ||
@@ -147,3 +146,4 @@ before.push(corser.create(options.corsHeaders ? { | ||
if (typeof options.proxy === 'string') { | ||
var proxy = httpProxy.createProxyServer({}); | ||
var proxyOptions = options.proxyOptions || {}; | ||
var proxy = httpProxy.createProxyServer(proxyOptions); | ||
before.push(function (req, res) { | ||
@@ -153,3 +153,3 @@ proxy.web(req, res, { | ||
changeOrigin: true | ||
}, function (err, req, res, target) { | ||
}, function (err, req, res) { | ||
if (options.logFn) { | ||
@@ -181,3 +181,7 @@ options.logFn(req, res, { | ||
this.server = union.createServer(serverOptions); | ||
this.server = serverOptions.https && serverOptions.https.passphrase | ||
// if passphrase is set, shim must be used as union does not support | ||
? require('./shims/https-server-shim')(serverOptions) | ||
: union.createServer(serverOptions); | ||
if (options.timeout !== undefined) { | ||
@@ -184,0 +188,0 @@ this.server.setTimeout(options.timeout); |
{ | ||
"name": "http-server", | ||
"version": "13.0.2", | ||
"version": "14.0.0", | ||
"description": "A simple zero-configuration command-line http server", | ||
@@ -32,3 +32,3 @@ "main": "./lib/http-server", | ||
"engines": { | ||
"node": ">=6" | ||
"node": ">=12" | ||
}, | ||
@@ -89,20 +89,21 @@ "contributors": [ | ||
"dependencies": { | ||
"basic-auth": "^1.0.3", | ||
"basic-auth": "^2.0.1", | ||
"colors": "^1.4.0", | ||
"corser": "^2.0.1", | ||
"he": "^1.1.0", | ||
"http-proxy": "^1.18.0", | ||
"he": "^1.2.0", | ||
"html-encoding-sniffer": "^3.0.0", | ||
"http-proxy": "^1.18.1", | ||
"mime": "^1.6.0", | ||
"minimist": "^1.2.5", | ||
"opener": "^1.5.1", | ||
"portfinder": "^1.0.25", | ||
"portfinder": "^1.0.28", | ||
"secure-compare": "3.0.1", | ||
"union": "~0.5.0", | ||
"url-join": "^2.0.5" | ||
"url-join": "^4.0.1" | ||
}, | ||
"devDependencies": { | ||
"common-style": "^3.0.0", | ||
"eol": "^0.9.1", | ||
"express": "^4.16.3", | ||
"mkdirp": "^0.5.0", | ||
"eslint": "^4.19.1", | ||
"eslint-config-populist": "^4.2.0", | ||
"express": "^4.17.1", | ||
"request": "^2.88.2", | ||
@@ -109,0 +110,0 @@ "tap": "^14.11.0" |
@@ -61,5 +61,6 @@ [![GitHub Workflow Status (master)](https://img.shields.io/github/workflow/status/http-party/http-server/Node.js%20CI/master?style=flat-square)](https://github.com/http-party/http-server/actions) | ||
|`-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.| | | ||
|`-S`, `--tls` or `--ssl` |Enable secure request serving with TLS/SSL (HTTPS)|`false`| | ||
|`-C` or `--cert` |Path to ssl cert file |`cert.pem` | | ||
@@ -106,2 +107,11 @@ |`-K` or `--key` |Path to ssl key file |`key.pem` | | ||
If you wish to use a passphrase with your private key you can include one in the openssl command via the -passout parameter (using password of foobar) | ||
e.g. | ||
`openssl req -newkey rsa:2048 -passout pass:foobar -keyout key.pem -x509 -days 365 -out cert.pem` | ||
For security reasons, the passphrase will only be read from the `NODE_HTTP_SERVER_SSL_PASSPHRASE` environment variable. | ||
This is what should be output if successful: | ||
@@ -108,0 +118,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
124516
20
1488
150
13
6
2
+ Addedhtml-encoding-sniffer@^3.0.0
+ Addedbasic-auth@2.0.1(transitive)
+ Addedhtml-encoding-sniffer@3.0.0(transitive)
+ Addediconv-lite@0.6.3(transitive)
+ Addedsafe-buffer@5.1.2(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedurl-join@4.0.1(transitive)
+ Addedwhatwg-encoding@2.0.0(transitive)
- Removedbasic-auth@1.1.0(transitive)
- Removedurl-join@2.0.5(transitive)
Updatedbasic-auth@^2.0.1
Updatedhe@^1.2.0
Updatedhttp-proxy@^1.18.1
Updatedportfinder@^1.0.28
Updatedurl-join@^4.0.1