diff --git a/lib/core/index.js b/lib/core/index.js index 35a4136d..88ec55e0 100644 --- a/lib/core/index.js +++ b/lib/core/index.js @@ -12,6 +12,8 @@ const version = require('../../package.json').version; const status = require('./status-handlers'); const generateEtag = require('./etag'); const optsParser = require('./opts'); +const htmlEncodingSniffer = require("html-encoding-sniffer"); +const { Readable } = require('stream'); let httpServerCore = null; @@ -222,15 +224,18 @@ module.exports = function createMiddleware(_dir, _options) { // and brotli special case. const defaultType = opts.contentType || 'application/octet-stream'; let contentType = mime.lookup(file, defaultType); - let charSet; const range = (req.headers && req.headers.range); const lastModified = (new Date(stat.mtime)).toUTCString(); const etag = generateEtag(stat, weakEtags); let cacheControl = cache; let stream = null; if (contentType && isTextFile(contentType)) { - // Assume text types are utf8 - contentType += '; charset=UTF-8'; + const htmlBytes = fs.readFileSync(file); + const sniffedEncoding = htmlEncodingSniffer(htmlBytes, { + defaultEncoding: 'UTF-8' + }); + contentType += `; charset=${sniffedEncoding}`; + stream = Readable.from(htmlBytes) } if (file === gzippedFile) { // is .gz picked up @@ -312,7 +317,10 @@ module.exports = function createMiddleware(_dir, _options) { return; } - stream = fs.createReadStream(file); + // streams may already have been assigned during encoding sniffing. + if (stream === null) { + stream = fs.createReadStream(file); + } stream.pipe(res); stream.on('error', (err) => { diff --git a/package-lock.json b/package-lock.json index 41e90a35..10398fa9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "http-server", "version": "13.0.2", "license": "MIT", "dependencies": { @@ -12,6 +13,7 @@ "colors": "^1.4.0", "corser": "^2.0.1", "he": "^1.1.0", + "html-encoding-sniffer": "^3.0.0", "http-proxy": "^1.18.0", "mime": "^1.6.0", "minimist": "^1.2.5", @@ -22,7 +24,6 @@ "url-join": "^2.0.5" }, "bin": { - "hs": "bin/http-server", "http-server": "bin/http-server" }, "devDependencies": { @@ -2496,6 +2497,17 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4878,8 +4890,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/secure-compare": { "version": "3.0.1", @@ -7626,6 +7637,28 @@ "node": ">= 0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -10003,6 +10036,14 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -11935,8 +11976,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "secure-compare": { "version": "3.0.1", @@ -13931,6 +13971,24 @@ "vow": "^0.4.17" } }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "requires": { + "iconv-lite": "0.6.3" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index b6d0d235..db161b20 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "colors": "^1.4.0", "corser": "^2.0.1", "he": "^1.1.0", + "html-encoding-sniffer": "^3.0.0", "http-proxy": "^1.18.0", "mime": "^1.6.0", "minimist": "^1.2.5",