From 318610ed1cded021e63c4cd8119a60102a1b13fa Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 20 Oct 2022 18:35:45 +0200 Subject: [PATCH 01/34] http: add `http.createStaticServer` --- lib/http/server.js | 108 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 lib/http/server.js diff --git a/lib/http/server.js b/lib/http/server.js new file mode 100644 index 00000000000000..32e468c5b3c845 --- /dev/null +++ b/lib/http/server.js @@ -0,0 +1,108 @@ +'use strict'; + +const { + Promise, + RegExpPrototypeExec, + StringPrototypeEndsWith, + StringPrototypeIndexOf, + StringPrototypeLastIndexOf, + StringPrototypeReplace, + StringPrototypeSlice, + StringPrototypeStartsWith, + StringPrototypeToLocaleLowerCase, +} = primordials; + +const { cwd } = process; +const console = require('internal/console/global'); +const { createReadStream } = require('fs'); +const { createServer } = require('http'); +const { URL, pathToFileURL } = require('internal/url'); +const { sep } = require('path'); +const { setImmediate } = require('timers'); + +if (module.isPreloading) { + process.env.NODE_REPL_EXTERNAL_MODULE = 'node:http/server'; + setImmediate(startServer); +} + +function startServer() { + const CWD_URL = pathToFileURL(cwd() + sep); + + const server = createServer(async (req, res) => { + const url = new URL( + req.url === '/' || StringPrototypeStartsWith(req.url, '/?') ? + './index.html' : + StringPrototypeReplace(new URL(req.url, 'root://').toString(), 'root://', '.'), + CWD_URL + ); + + const mime = { + '__proto__': null, + '.html': 'text/html; charset=UTF-8', + '.css': 'text/css; charset=UTF-8', + '.avif': 'image/avif', + '.svg': 'image/svg+xml', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.webp': 'image/webp', + '.mp4': 'image/gif', + '.woff2': 'font/woff2', + }; + const ext = StringPrototypeToLocaleLowerCase( + StringPrototypeSlice(url.pathname, StringPrototypeLastIndexOf(url.pathname, '.')) + ); + if (ext in mime) res.setHeader('Content-Type', mime[ext]); + + try { + try { + const stream = createReadStream(url, { emitClose: false }); + await new Promise((resolve, reject) => { + stream.on('error', reject); + stream.on('close', resolve); + stream.pipe(res); + }); + } catch (err) { + if (err?.code === 'EISDIR') { + if (StringPrototypeEndsWith(req.url, '/') || RegExpPrototypeExec(/^[^?]+\/\?/, req.url) !== null) { + const stream = createReadStream(new URL('./index.html', url), { + emitClose: false, + }); + await new Promise((resolve, reject) => { + stream.on('error', reject); + stream.on('close', resolve); + stream.pipe(res); + }); + } else { + res.statusCode = 307; + const index = StringPrototypeIndexOf(req.url, '?'); + res.setHeader( + 'Location', + index === -1 ? + `${req.url}/` : + `${StringPrototypeSlice(req.url, 0, index)}/${StringPrototypeSlice(req.url, index)}` + ); + res.end('Temporary Redirect'); + } + } else { + throw err; + } + } + } catch (err) { + if (err?.code === 'ENOENT') { + console.log('404', req.url); + res.statusCode = 404; + res.end('Not Found'); + } else { + console.error(`Error while loading ${req.url}:`); + console.error(err); + res.statusCode = 500; + res.end('Internal Error'); + } + } + }).listen(() => { + console.log(`Server started on http://localhost:${server.address().port}`); + }); +} + +module.exports = startServer; From 77e04161529952126219ee1a3d9fcb385e355dfe Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 21 Oct 2022 03:10:46 +0200 Subject: [PATCH 02/34] Add docs and parameters --- doc/api/http.md | 37 +++++++++++++++++++++++++++++++++++++ lib/http/server.js | 41 +++++++++++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/doc/api/http.md b/doc/api/http.md index 30fb7cd030f51e..6c6de6fe1c0e89 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3565,6 +3565,43 @@ server.on('request', (request, res) => { server.listen(8000); ``` +## `http.createSimpleServer([directory[, port[, host]]])` + + + +> Stability: 0 - Experimental + +* `directory` {string|URL} Root directory from which files would be serve. +* `port` {number} +* `host` {string} + +Start a TCP server listening for connections on the given `port` and `host`, and +serve statically local files, using `directory` as the root. Please note that +when specifying a `host` other than `localhost`, you are exposing you local file +system to all the machines that can connect to your computer. + +If `host` is omitted, the server will accept connections on the +[unspecified IPv6 address][] (`::`) when IPv6 is available, or the +[unspecified IPv4 address][] (`0.0.0.0`) otherwise. + +In most operating systems, listening to the [unspecified IPv6 address][] (`::`) +may cause the `net.Server` to also listen on the [unspecified IPv4 address][] +(`0.0.0.0`). + +Also accessible via `require('node:http/server')`. + +```bash +node -r node:http/server # starts serving the cwd on a random port + +# To start serving on the port 8080 using /path/to/dir as the root: +node -r node:http/server /path/to/dir --port 8080 --host localhost + +# If `--host` is omitted, your local file system is exposed: +node -r node:http/server /path/to/dir --port 8080 +``` + ## `http.get(options[, callback])` ## `http.get(url[, options][, callback])` diff --git a/lib/http/server.js b/lib/http/server.js index 32e468c5b3c845..fa1246dcec3fe0 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -12,21 +12,36 @@ const { StringPrototypeToLocaleLowerCase, } = primordials; -const { cwd } = process; +const { cwd, nextTick } = process; const console = require('internal/console/global'); const { createReadStream } = require('fs'); const { createServer } = require('http'); const { URL, pathToFileURL } = require('internal/url'); const { sep } = require('path'); -const { setImmediate } = require('timers'); +const { emitExperimentalWarning } = require('internal/util'); + +emitExperimentalWarning('http/server'); if (module.isPreloading) { + const { parseArgs } = require('internal/util/parse_args/parse_args'); + const { values: { port, host } } = parseArgs({ options: { + port: { + type: 'string', + short: 'p', + }, + host: { + type: 'string', + short: 'b', + default: 'localhost', + }, + } }); + nextTick(startServer, port, host, process.argv[1]); process.env.NODE_REPL_EXTERNAL_MODULE = 'node:http/server'; - setImmediate(startServer); + process.argv[1] = 'node:http/server'; } -function startServer() { - const CWD_URL = pathToFileURL(cwd() + sep); +function startServer(port = undefined, host, directory = cwd()) { + const CWD_URL = pathToFileURL(directory + sep); const server = createServer(async (req, res) => { const url = new URL( @@ -39,6 +54,7 @@ function startServer() { const mime = { '__proto__': null, '.html': 'text/html; charset=UTF-8', + '.js': 'text/javascript; charset=UTF-8', '.css': 'text/css; charset=UTF-8', '.avif': 'image/avif', '.svg': 'image/svg+xml', @@ -46,7 +62,6 @@ function startServer() { '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', - '.mp4': 'image/gif', '.woff2': 'font/woff2', }; const ext = StringPrototypeToLocaleLowerCase( @@ -100,9 +115,19 @@ function startServer() { res.end('Internal Error'); } } - }).listen(() => { - console.log(`Server started on http://localhost:${server.address().port}`); }); + const callback = () => { + const { address, family, port } = server.address(); + const host = family === 'IPv6' ? `[${address}]` : address; + console.log(`Server started on http://${host}:${port}`); + }; + if (host != null) { + server.listen(port, host, callback); + } else if (port != null) { + server.listen(port, callback); + } else { + server.listen(callback); + } } module.exports = startServer; From b1cb361ef733cdb604b02d34d4943f120c51fa5a Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 21 Oct 2022 14:02:27 +0200 Subject: [PATCH 03/34] default to `'localhost'`, return server, update docs --- doc/api/http.md | 20 +++++++++----------- lib/http.js | 2 ++ lib/http/server.js | 19 ++++++++++--------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/doc/api/http.md b/doc/api/http.md index 6c6de6fe1c0e89..e25c962906320b 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3574,21 +3574,19 @@ added: REPLACEME > Stability: 0 - Experimental * `directory` {string|URL} Root directory from which files would be serve. + **Default:** `process.cwd()`. * `port` {number} -* `host` {string} +* `host` {string} **Default:** `'localhost'` + +* Returns: {http.Server} Start a TCP server listening for connections on the given `port` and `host`, and serve statically local files, using `directory` as the root. Please note that when specifying a `host` other than `localhost`, you are exposing you local file system to all the machines that can connect to your computer. -If `host` is omitted, the server will accept connections on the -[unspecified IPv6 address][] (`::`) when IPv6 is available, or the -[unspecified IPv4 address][] (`0.0.0.0`) otherwise. - -In most operating systems, listening to the [unspecified IPv6 address][] (`::`) -may cause the `net.Server` to also listen on the [unspecified IPv4 address][] -(`0.0.0.0`). +If `port` is omitted or is 0, the operating system will assign an arbitrary +unused port, which will be output the standard output. Also accessible via `require('node:http/server')`. @@ -3596,10 +3594,10 @@ Also accessible via `require('node:http/server')`. node -r node:http/server # starts serving the cwd on a random port # To start serving on the port 8080 using /path/to/dir as the root: -node -r node:http/server /path/to/dir --port 8080 --host localhost - -# If `--host` is omitted, your local file system is exposed: node -r node:http/server /path/to/dir --port 8080 + +# Same, but exposing your local file system to the whole IPv4 network: +node -r node:http/server /path/to/dir --port 8080 --host 0.0.0.0 ``` ## `http.get(options[, callback])` diff --git a/lib/http.js b/lib/http.js index 9fce02d6e3b3ac..75f932c2302e6b 100644 --- a/lib/http.js +++ b/lib/http.js @@ -43,6 +43,7 @@ const { Server, ServerResponse, } = require('_http_server'); +const createSimpleServer = require('http/server'); let maxHeaderSize; /** @@ -127,6 +128,7 @@ module.exports = { Server, ServerResponse, createServer, + createSimpleServer, validateHeaderName, validateHeaderValue, get, diff --git a/lib/http/server.js b/lib/http/server.js index fa1246dcec3fe0..6e503e049ff152 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -15,8 +15,8 @@ const { const { cwd, nextTick } = process; const console = require('internal/console/global'); const { createReadStream } = require('fs'); -const { createServer } = require('http'); -const { URL, pathToFileURL } = require('internal/url'); +const { Server } = require('_http_server'); +const { URL, isURLInstance, pathToFileURL } = require('internal/url'); const { sep } = require('path'); const { emitExperimentalWarning } = require('internal/util'); @@ -31,19 +31,18 @@ if (module.isPreloading) { }, host: { type: 'string', - short: 'b', - default: 'localhost', + short: 'h', }, } }); - nextTick(startServer, port, host, process.argv[1]); + nextTick(createSimpleServer, process.argv[1], port, host); process.env.NODE_REPL_EXTERNAL_MODULE = 'node:http/server'; process.argv[1] = 'node:http/server'; } -function startServer(port = undefined, host, directory = cwd()) { - const CWD_URL = pathToFileURL(directory + sep); +function createSimpleServer(directory = cwd(), port = 0, host = 'localhost') { + const CWD_URL = isURLInstance(directory) ? directory : pathToFileURL(directory + sep); - const server = createServer(async (req, res) => { + const server = new Server(async (req, res) => { const url = new URL( req.url === '/' || StringPrototypeStartsWith(req.url, '/?') ? './index.html' : @@ -128,6 +127,8 @@ function startServer(port = undefined, host, directory = cwd()) { } else { server.listen(callback); } + + return server; } -module.exports = startServer; +module.exports = createSimpleServer; From ac549c49b37cdff32c5594563846a3a7674d2ab9 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 21 Oct 2022 23:07:29 +0200 Subject: [PATCH 04/34] Apply suggestions from code review Co-authored-by: Mohammed Keyvanzadeh --- doc/api/http.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/api/http.md b/doc/api/http.md index e25c962906320b..33d9b12c017915 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3573,7 +3573,7 @@ added: REPLACEME > Stability: 0 - Experimental -* `directory` {string|URL} Root directory from which files would be serve. +* `directory` {string|URL} Root directory from which files would be served. **Default:** `process.cwd()`. * `port` {number} * `host` {string} **Default:** `'localhost'` @@ -3581,12 +3581,12 @@ added: REPLACEME * Returns: {http.Server} Start a TCP server listening for connections on the given `port` and `host`, and -serve statically local files, using `directory` as the root. Please note that -when specifying a `host` other than `localhost`, you are exposing you local file +serve static local files, using `directory` as the root. Please note that +when specifying a `host` other than `localhost`, you are exposing your local file system to all the machines that can connect to your computer. If `port` is omitted or is 0, the operating system will assign an arbitrary -unused port, which will be output the standard output. +unused port, which it's output will be the standard output. Also accessible via `require('node:http/server')`. @@ -3596,7 +3596,8 @@ node -r node:http/server # starts serving the cwd on a random port # To start serving on the port 8080 using /path/to/dir as the root: node -r node:http/server /path/to/dir --port 8080 -# Same, but exposing your local file system to the whole IPv4 network: +# Same as above, but exposing your local file system to the whole +# IPv4 network: node -r node:http/server /path/to/dir --port 8080 --host 0.0.0.0 ``` From d3b189f94a305c2899aeaf60ff4664d37ffb5294 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 21 Oct 2022 23:08:54 +0200 Subject: [PATCH 05/34] Ensure the server doesn't start if it's being required --- lib/http/server.js | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/http/server.js b/lib/http/server.js index 6e503e049ff152..b4efce7d6f0f70 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -1,6 +1,7 @@ 'use strict'; const { + ArrayPrototypeIncludes, Promise, RegExpPrototypeExec, StringPrototypeEndsWith, @@ -19,24 +20,29 @@ const { Server } = require('_http_server'); const { URL, isURLInstance, pathToFileURL } = require('internal/url'); const { sep } = require('path'); const { emitExperimentalWarning } = require('internal/util'); +const { getOptionValue } = require('internal/options'); emitExperimentalWarning('http/server'); if (module.isPreloading) { - const { parseArgs } = require('internal/util/parse_args/parse_args'); - const { values: { port, host } } = parseArgs({ options: { - port: { - type: 'string', - short: 'p', - }, - host: { - type: 'string', - short: 'h', - }, - } }); - nextTick(createSimpleServer, process.argv[1], port, host); - process.env.NODE_REPL_EXTERNAL_MODULE = 'node:http/server'; - process.argv[1] = 'node:http/server'; + const requiredModules = getOptionValue('--require'); + if (ArrayPrototypeIncludes(requiredModules, `node:${module.id}`) || + ArrayPrototypeIncludes(requiredModules, module.id)) { + const { parseArgs } = require('internal/util/parse_args/parse_args'); + const { values: { port, host } } = parseArgs({ options: { + port: { + type: 'string', + short: 'p', + }, + host: { + type: 'string', + short: 'h', + }, + } }); + nextTick(createSimpleServer, process.argv[1], port, host); + process.env.NODE_REPL_EXTERNAL_MODULE = 'node:http/server'; + process.argv[1] = 'node:http/server'; + } } function createSimpleServer(directory = cwd(), port = 0, host = 'localhost') { From b6dbf4f26d7828e2a7422a50a778d5cb44e9d1d3 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 21 Oct 2022 23:14:38 +0200 Subject: [PATCH 06/34] use options instead of function arguments --- doc/api/http.md | 17 +++++++++++------ lib/http/server.js | 11 ++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/doc/api/http.md b/doc/api/http.md index 33d9b12c017915..02b3ac898eea20 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3565,7 +3565,7 @@ server.on('request', (request, res) => { server.listen(8000); ``` -## `http.createSimpleServer([directory[, port[, host]]])` +## `http.createSimpleServer([options])` @@ -3587,9 +3587,9 @@ added: REPLACEME * Returns: {http.Server} Start a TCP server listening for connections on the given `port` and `host`, and -serve static local files, using `directory` as the root. Please note that -when specifying a `host` other than `localhost`, you are exposing your local file -system to all the machines that can connect to your computer. +serve static local files, using `directory` as the root. +When specifying a `host` other than `localhost`, you are exposing your local +file system to all the machines that can connect to your computer. If specified and not `null`, `filter` will be called with two arguments: the first one if the request URL string (the URL that is present in the actual HTTP From 60d3f0bae5375f7b8c614bb457fadee71e920bc6 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 26 Oct 2022 03:07:11 +0200 Subject: [PATCH 20/34] rename `http/server` -> `http/static` --- doc/api/http.md | 8 ++++---- lib/http.js | 2 +- lib/http/{server.js => static.js} | 8 ++++---- test/parallel/test-http-createstaticserver.js | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) rename lib/http/{server.js => static.js} (97%) diff --git a/doc/api/http.md b/doc/api/http.md index 32f5fff95110bb..6a0b5e23aa4a51 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3600,20 +3600,20 @@ request will be blocked. If `port` is omitted or is 0, the operating system will assign an arbitrary unused port, which it's output will be the standard output. -Also accessible via `require('node:http/server')`. +Also accessible via `require('node:http/static')`. ```bash # Starts serving the cwd on a random port: -node -r node:http/server +node -r node:http/static node -e 'http.createStaticServer()' # To start serving on the port 8080 using /path/to/dir as the root: -node -r node:http/server /path/to/dir --port 8080 +node -r node:http/static /path/to/dir --port 8080 node -e 'http.createStaticServer({directory: "/path/to/dir", port: 8080})' # Same as above, but exposing your local file system to the whole # IPv4 network: -node -r node:http/server /path/to/dir --port 8080 --host 0.0.0.0 +node -r node:http/static /path/to/dir --port 8080 --host 0.0.0.0 node -e 'http.createStaticServer({directory: "/path/to/dir", port: 8080, host: "0.0.0.0"})' ``` diff --git a/lib/http.js b/lib/http.js index daa9ed375c38fa..718ac20ed73ac7 100644 --- a/lib/http.js +++ b/lib/http.js @@ -43,7 +43,7 @@ const { Server, ServerResponse, } = require('_http_server'); -const createStaticServer = require('http/server'); +const createStaticServer = require('http/static'); let maxHeaderSize; /** diff --git a/lib/http/server.js b/lib/http/static.js similarity index 97% rename from lib/http/server.js rename to lib/http/static.js index 2f583c904e89d4..793dfdc9c011f8 100644 --- a/lib/http/server.js +++ b/lib/http/static.js @@ -23,8 +23,6 @@ const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); const { getOptionValue } = require('internal/options'); const { validateFunction, validatePort } = require('internal/validators'); -emitExperimentalWarning('http/server'); - if (module.isPreloading) { const requiredModules = getOptionValue('--require'); if (ArrayPrototypeIncludes(requiredModules, `node:${module.id}`) || @@ -41,8 +39,8 @@ if (module.isPreloading) { }, } }); nextTick(createStaticServer, { directory: process.argv[1], port, host }); - process.env.NODE_REPL_EXTERNAL_MODULE = 'node:http/server'; - process.argv[1] = 'node:http/server'; + process.env.NODE_REPL_EXTERNAL_MODULE = 'node:http/static'; + process.argv[1] = 'node:http/static'; } } @@ -51,6 +49,8 @@ function filterDotFiles(url, fileURL) { } function createStaticServer(options = kEmptyObject) { + emitExperimentalWarning('http/static'); + const { directory = cwd(), port, diff --git a/test/parallel/test-http-createstaticserver.js b/test/parallel/test-http-createstaticserver.js index 9d0d7e1a9812b1..86cf755df1dcf7 100644 --- a/test/parallel/test-http-createstaticserver.js +++ b/test/parallel/test-http-createstaticserver.js @@ -3,7 +3,7 @@ const { mustCall } = require('../common'); const fixtures = require('../common/fixtures'); const assert = require('node:assert'); const { MIMEType } = require('node:util'); -const createStaticServer = require('node:http/server'); +const createStaticServer = require('node:http/static'); [0, 1, 1n, '', '1', true, false, NaN, Symbol(), {}, []].forEach((filter) => { assert.throws(() => createStaticServer({ filter }), { code: 'ERR_INVALID_ARG_TYPE' }); From 41fc90156d298dc4c0e5d6780e21e7a30282149b Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 26 Oct 2022 03:11:10 +0200 Subject: [PATCH 21/34] list missing features in docs --- doc/api/http.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/api/http.md b/doc/api/http.md index 6a0b5e23aa4a51..74d770d02d67c1 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3571,7 +3571,9 @@ server.listen(8000); added: REPLACEME --> -> Stability: 0 - Experimental +> Stability: 1 - Experimental. The implementation lacks support for partial +> responses (Ranges) and conditional-GET negotiation (If-Match, +> If-Unmodified-Since, If-None-Match, If-Modified-Since). * `options` {Object} * `directory` {string|URL} Root directory from which files would be served. From 47d2f8b6100eabbdb6e5fc39eeecaa382c2d19f6 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 23 Nov 2022 14:10:35 +0100 Subject: [PATCH 22/34] add tests to ensure that using a "hidden" folder as root is blocked by default --- test/parallel/test-http-createstaticserver.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/parallel/test-http-createstaticserver.js b/test/parallel/test-http-createstaticserver.js index 86cf755df1dcf7..18b65bcf0e287e 100644 --- a/test/parallel/test-http-createstaticserver.js +++ b/test/parallel/test-http-createstaticserver.js @@ -18,6 +18,28 @@ const createStaticServer = require('node:http/static'); }); +{ + const server = createStaticServer({ + directory: fixtures.path('static-server', '.foo'), + }).once('listening', mustCall(() => { + const { port } = server.address(); + Promise.all([ + fetch(`http://localhost:${port}/`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 401); + assert.strictEqual(response.statusText, 'Unauthorized'); + }), + fetch(`http://localhost:${port}/bar.js`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 401); + assert.strictEqual(response.statusText, 'Unauthorized'); + }), + ]).then(mustCall()).finally(() => server.close()); + })); +} + { const server = createStaticServer({ directory: fixtures.path('static-server'), From fa52ee0826e0acf479721144a53a63d1087cd0de Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 23 Nov 2022 14:14:18 +0100 Subject: [PATCH 23/34] Use 403 instead of 401 --- lib/http/static.js | 6 +-- test/parallel/test-http-createstaticserver.js | 52 ++++++++++--------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/lib/http/static.js b/lib/http/static.js index 793dfdc9c011f8..caea4f1bbb3fa1 100644 --- a/lib/http/static.js +++ b/lib/http/static.js @@ -75,9 +75,9 @@ function createStaticServer(options = kEmptyObject) { ); if (filter != null && !filter(req.url, url)) { - console.log('401', req.url); - res.statusCode = 401; - res.end('Not Authorized'); + console.log('403', req.url); + res.statusCode = 403; + res.end('Forbidden'); return; } diff --git a/test/parallel/test-http-createstaticserver.js b/test/parallel/test-http-createstaticserver.js index 18b65bcf0e287e..3b12e61de7d6c7 100644 --- a/test/parallel/test-http-createstaticserver.js +++ b/test/parallel/test-http-createstaticserver.js @@ -27,14 +27,18 @@ const createStaticServer = require('node:http/static'); fetch(`http://localhost:${port}/`).then(async (response) => { assert(!response.ok); assert(!response.redirected); - assert.strictEqual(response.status, 401); - assert.strictEqual(response.statusText, 'Unauthorized'); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual(response.headers.get('Content-Type'), null); + assert.strictEqual(await response.text(), 'Forbidden'); }), fetch(`http://localhost:${port}/bar.js`).then(async (response) => { assert(!response.ok); assert(!response.redirected); - assert.strictEqual(response.status, 401); - assert.strictEqual(response.statusText, 'Unauthorized'); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual(response.headers.get('Content-Type'), null); + assert.strictEqual(await response.text(), 'Forbidden'); }), ]).then(mustCall()).finally(() => server.close()); })); @@ -89,32 +93,32 @@ const createStaticServer = require('node:http/static'); fetch(`http://localhost:${port}/.bar`).then(async (response) => { assert(!response.ok); assert(!response.redirected); - assert.strictEqual(response.status, 401); - assert.strictEqual(response.statusText, 'Unauthorized'); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); - assert.strictEqual(await response.text(), 'Not Authorized'); + assert.strictEqual(await response.text(), 'Forbidden'); }), fetch(`http://localhost:${port}/.foo`).then(async (response) => { assert(!response.ok); assert(!response.redirected); - assert.strictEqual(response.status, 401); - assert.strictEqual(response.statusText, 'Unauthorized'); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); }), fetch(`http://localhost:${port}/.foo/`).then(async (response) => { assert(!response.ok); assert(!response.redirected); - assert.strictEqual(response.status, 401); - assert.strictEqual(response.statusText, 'Unauthorized'); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); }), fetch(`http://localhost:${port}/.foo/bar.js`).then(async (response) => { assert(!response.ok); assert(!response.redirected); - assert.strictEqual(response.status, 401); - assert.strictEqual(response.statusText, 'Unauthorized'); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); - assert.strictEqual(await response.text(), 'Not Authorized'); + assert.strictEqual(await response.text(), 'Forbidden'); }), ]).then(mustCall()).finally(() => server.close()); })); @@ -219,10 +223,10 @@ const createStaticServer = require('node:http/static'); fetch(`http://localhost:${port}/test.html`).then(async (response) => { assert(!response.ok); assert(!response.redirected); - assert.strictEqual(response.status, 401); - assert.strictEqual(response.statusText, 'Unauthorized'); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); - assert.strictEqual(await response.text(), 'Not Authorized'); + assert.strictEqual(await response.text(), 'Forbidden'); }), fetch(`http://localhost:${port}/.bar`).then(async (response) => { assert(response.ok); @@ -235,24 +239,24 @@ const createStaticServer = require('node:http/static'); fetch(`http://localhost:${port}/.foo`).then(async (response) => { assert(!response.ok); assert(!response.redirected); - assert.strictEqual(response.status, 401); - assert.strictEqual(response.statusText, 'Unauthorized'); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); }), fetch(`http://localhost:${port}/.foo/`).then(async (response) => { assert(!response.ok); assert(!response.redirected); - assert.strictEqual(response.status, 401); - assert.strictEqual(response.statusText, 'Unauthorized'); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); }), fetch(`http://localhost:${port}/.foo/bar.js`).then(async (response) => { assert(!response.ok); assert(!response.redirected); - assert.strictEqual(response.status, 401); - assert.strictEqual(response.statusText, 'Unauthorized'); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); - assert.strictEqual(await response.text(), 'Not Authorized'); + assert.strictEqual(await response.text(), 'Forbidden'); }), ]).then(mustCall()).finally(() => server.close()); })); From 968efde4c6e951798e6fdc9b9b0be5ee5463f55f Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 23 Nov 2022 14:17:01 +0100 Subject: [PATCH 24/34] fix type in docs --- doc/api/http.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/http.md b/doc/api/http.md index 74d770d02d67c1..17388c5a5824ba 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3583,7 +3583,7 @@ added: REPLACEME * `mimeOverrides` {Object} Dictionary linking file extension to a MIME string, to override or extend the built-in ones. * `filter` {Function|null} should be a function that accepts two arguments and - returns a value that is coercible to a `Boolean` value. When `null`, no + returns a value that is coercible to a {boolean} value. When `null`, no files are filtered. **Default:** filters all dot files. * Returns: {http.Server} From 5e018eff6f0927f7ee874877352092de4dfca714 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 7 Dec 2022 16:25:09 +0100 Subject: [PATCH 25/34] remove `http/static` module --- doc/api/http.md | 5 ---- lib/http.js | 2 +- lib/{ => internal}/http/static.js | 25 +------------------ test/parallel/test-http-createstaticserver.js | 2 +- 4 files changed, 3 insertions(+), 31 deletions(-) rename lib/{ => internal}/http/static.js (84%) diff --git a/doc/api/http.md b/doc/api/http.md index 17388c5a5824ba..b687136f68ab3b 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3602,20 +3602,15 @@ request will be blocked. If `port` is omitted or is 0, the operating system will assign an arbitrary unused port, which it's output will be the standard output. -Also accessible via `require('node:http/static')`. - ```bash # Starts serving the cwd on a random port: -node -r node:http/static node -e 'http.createStaticServer()' # To start serving on the port 8080 using /path/to/dir as the root: -node -r node:http/static /path/to/dir --port 8080 node -e 'http.createStaticServer({directory: "/path/to/dir", port: 8080})' # Same as above, but exposing your local file system to the whole # IPv4 network: -node -r node:http/static /path/to/dir --port 8080 --host 0.0.0.0 node -e 'http.createStaticServer({directory: "/path/to/dir", port: 8080, host: "0.0.0.0"})' ``` diff --git a/lib/http.js b/lib/http.js index 718ac20ed73ac7..c626bb7d70381b 100644 --- a/lib/http.js +++ b/lib/http.js @@ -43,7 +43,7 @@ const { Server, ServerResponse, } = require('_http_server'); -const createStaticServer = require('http/static'); +const createStaticServer = require('internal/http/static'); let maxHeaderSize; /** diff --git a/lib/http/static.js b/lib/internal/http/static.js similarity index 84% rename from lib/http/static.js rename to lib/internal/http/static.js index caea4f1bbb3fa1..33dc106202e9e9 100644 --- a/lib/http/static.js +++ b/lib/internal/http/static.js @@ -1,7 +1,6 @@ 'use strict'; const { - ArrayPrototypeIncludes, Promise, RegExpPrototypeExec, StringPrototypeEndsWith, @@ -14,36 +13,14 @@ const { StringPrototypeToLocaleLowerCase, } = primordials; -const { cwd, nextTick } = process; +const { cwd } = process; const console = require('internal/console/global'); const { createReadStream } = require('fs'); const { Server } = require('_http_server'); const { URL, isURLInstance, pathToFileURL } = require('internal/url'); const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); -const { getOptionValue } = require('internal/options'); const { validateFunction, validatePort } = require('internal/validators'); -if (module.isPreloading) { - const requiredModules = getOptionValue('--require'); - if (ArrayPrototypeIncludes(requiredModules, `node:${module.id}`) || - ArrayPrototypeIncludes(requiredModules, module.id)) { - const { parseArgs } = require('internal/util/parse_args/parse_args'); - const { values: { port, host } } = parseArgs({ options: { - port: { - type: 'string', - short: 'p', - }, - host: { - type: 'string', - short: 'h', - }, - } }); - nextTick(createStaticServer, { directory: process.argv[1], port, host }); - process.env.NODE_REPL_EXTERNAL_MODULE = 'node:http/static'; - process.argv[1] = 'node:http/static'; - } -} - function filterDotFiles(url, fileURL) { return !StringPrototypeIncludes(fileURL.pathname, '/.'); } diff --git a/test/parallel/test-http-createstaticserver.js b/test/parallel/test-http-createstaticserver.js index 3b12e61de7d6c7..b41f04293c84da 100644 --- a/test/parallel/test-http-createstaticserver.js +++ b/test/parallel/test-http-createstaticserver.js @@ -3,7 +3,7 @@ const { mustCall } = require('../common'); const fixtures = require('../common/fixtures'); const assert = require('node:assert'); const { MIMEType } = require('node:util'); -const createStaticServer = require('node:http/static'); +const { createStaticServer } = require('node:http'); [0, 1, 1n, '', '1', true, false, NaN, Symbol(), {}, []].forEach((filter) => { assert.throws(() => createStaticServer({ filter }), { code: 'ERR_INVALID_ARG_TYPE' }); From 3698f0c010463b5feb0357a8bb3686ed8ba15cdb Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 15 Dec 2022 16:15:10 +0100 Subject: [PATCH 26/34] add line return --- lib/internal/http/static.js | 8 ++++---- test/parallel/test-http-createstaticserver.js | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/internal/http/static.js b/lib/internal/http/static.js index 33dc106202e9e9..fcf87895f6ad15 100644 --- a/lib/internal/http/static.js +++ b/lib/internal/http/static.js @@ -54,7 +54,7 @@ function createStaticServer(options = kEmptyObject) { if (filter != null && !filter(req.url, url)) { console.log('403', req.url); res.statusCode = 403; - res.end('Forbidden'); + res.end('Forbidden\n'); return; } @@ -106,7 +106,7 @@ function createStaticServer(options = kEmptyObject) { `${req.url}/` : `${StringPrototypeSlice(req.url, 0, index)}/${StringPrototypeSlice(req.url, index)}` ); - res.end('Temporary Redirect'); + res.end('Temporary Redirect\n'); } } else { throw err; @@ -116,12 +116,12 @@ function createStaticServer(options = kEmptyObject) { if (err?.code === 'ENOENT') { console.log('404', req.url); res.statusCode = 404; - res.end('Not Found'); + res.end('Not Found\n'); } else { console.error(`Error while loading ${req.url}:`); console.error(err); res.statusCode = 500; - res.end('Internal Error'); + res.end('Internal Error\n'); } } }); diff --git a/test/parallel/test-http-createstaticserver.js b/test/parallel/test-http-createstaticserver.js index b41f04293c84da..6efc8288368f64 100644 --- a/test/parallel/test-http-createstaticserver.js +++ b/test/parallel/test-http-createstaticserver.js @@ -30,7 +30,7 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(response.status, 403); assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); - assert.strictEqual(await response.text(), 'Forbidden'); + assert.strictEqual(await response.text(), 'Forbidden\n'); }), fetch(`http://localhost:${port}/bar.js`).then(async (response) => { assert(!response.ok); @@ -38,7 +38,7 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(response.status, 403); assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); - assert.strictEqual(await response.text(), 'Forbidden'); + assert.strictEqual(await response.text(), 'Forbidden\n'); }), ]).then(mustCall()).finally(() => server.close()); })); @@ -96,7 +96,7 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(response.status, 403); assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); - assert.strictEqual(await response.text(), 'Forbidden'); + assert.strictEqual(await response.text(), 'Forbidden\n'); }), fetch(`http://localhost:${port}/.foo`).then(async (response) => { assert(!response.ok); @@ -118,7 +118,7 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(response.status, 403); assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); - assert.strictEqual(await response.text(), 'Forbidden'); + assert.strictEqual(await response.text(), 'Forbidden\n'); }), ]).then(mustCall()).finally(() => server.close()); })); @@ -226,7 +226,7 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(response.status, 403); assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); - assert.strictEqual(await response.text(), 'Forbidden'); + assert.strictEqual(await response.text(), 'Forbidden\n'); }), fetch(`http://localhost:${port}/.bar`).then(async (response) => { assert(response.ok); @@ -256,7 +256,7 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(response.status, 403); assert.strictEqual(response.statusText, 'Forbidden'); assert.strictEqual(response.headers.get('Content-Type'), null); - assert.strictEqual(await response.text(), 'Forbidden'); + assert.strictEqual(await response.text(), 'Forbidden\n'); }), ]).then(mustCall()).finally(() => server.close()); })); From d8ce4b3da23ed76146dbb4ed39c2c3f8750c2076 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 19 Dec 2022 23:18:07 +0100 Subject: [PATCH 27/34] add `log` and `onStart` option to let user control logging --- doc/api/http.md | 4 ++++ lib/internal/http/static.js | 18 +++++++++++------- test/parallel/test-http-createstaticserver.js | 4 ++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/doc/api/http.md b/doc/api/http.md index b687136f68ab3b..5603d152160390 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3585,6 +3585,10 @@ added: REPLACEME * `filter` {Function|null} should be a function that accepts two arguments and returns a value that is coercible to a {boolean} value. When `null`, no files are filtered. **Default:** filters all dot files. + * `log` {Function|null} called when sending a response to the client. + **Default:** `console.log`. + * `onStart` {Function} called the server starts listening to requests. + **Default:** logs the URL to the console. * Returns: {http.Server} diff --git a/lib/internal/http/static.js b/lib/internal/http/static.js index fcf87895f6ad15..af9f0819f3f5ad 100644 --- a/lib/internal/http/static.js +++ b/lib/internal/http/static.js @@ -34,6 +34,8 @@ function createStaticServer(options = kEmptyObject) { host = 'localhost', mimeOverrides, filter = filterDotFiles, + log = console.log, + onStart = (host, port) => console.log(`Server started on http://${host}:${port}`), } = options; const directoryURL = isURLInstance(directory) ? directory : pathToFileURL(directory); // To be used as a base URL, it is necessary that the URL ends with a slash: @@ -52,7 +54,7 @@ function createStaticServer(options = kEmptyObject) { ); if (filter != null && !filter(req.url, url)) { - console.log('403', req.url); + log?.(403, req.url); res.statusCode = 403; res.end('Forbidden\n'); return; @@ -82,9 +84,10 @@ function createStaticServer(options = kEmptyObject) { const stream = createReadStream(url, { emitClose: false }); await new Promise((resolve, reject) => { stream.on('error', reject); - stream.on('close', resolve); + stream.on('end', resolve); stream.pipe(res); }); + log?.(200, req.url); } catch (err) { if (err?.code === 'EISDIR') { if (StringPrototypeEndsWith(req.url, '/') || RegExpPrototypeExec(/^[^?]+\/\?/, req.url) !== null) { @@ -94,10 +97,12 @@ function createStaticServer(options = kEmptyObject) { res.setHeader('Content-Type', mime['.html']); await new Promise((resolve, reject) => { stream.on('error', reject); - stream.on('close', resolve); + stream.on('end', resolve); stream.pipe(res); }); + log?.(200, req.url); } else { + log?.(307, req.url); res.statusCode = 307; const index = StringPrototypeIndexOf(req.url, '?'); res.setHeader( @@ -114,12 +119,11 @@ function createStaticServer(options = kEmptyObject) { } } catch (err) { if (err?.code === 'ENOENT') { - console.log('404', req.url); + log?.(404, req.url); res.statusCode = 404; res.end('Not Found\n'); } else { - console.error(`Error while loading ${req.url}:`); - console.error(err); + log?.(500, req.url, err); res.statusCode = 500; res.end('Internal Error\n'); } @@ -128,7 +132,7 @@ function createStaticServer(options = kEmptyObject) { const callback = () => { const { address, family, port } = server.address(); const host = family === 'IPv6' ? `[${address}]` : address; - console.log(`Server started on http://${host}:${port}`); + onStart?.(host, port); }; if (host != null) { server.listen(port, host, callback); diff --git a/test/parallel/test-http-createstaticserver.js b/test/parallel/test-http-createstaticserver.js index 6efc8288368f64..a8a8e4c4d84f3d 100644 --- a/test/parallel/test-http-createstaticserver.js +++ b/test/parallel/test-http-createstaticserver.js @@ -217,6 +217,10 @@ const { createStaticServer } = require('node:http'); assert(Object.getPrototypeOf(fileURL), URL.prototype); return url === '/.bar'; }, + log: mustCall(function() { + assert.strictEqual(arguments.length, 2); + }, 5), + onStart: mustCall(), }).once('listening', mustCall(() => { const { port } = server.address(); Promise.all([ From 0c91db4e2359635013ae90a33bbc83bd971585e9 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 19 Dec 2022 23:33:49 +0100 Subject: [PATCH 28/34] add tests to ensure using `..` can't escape the root dir --- test/parallel/test-http-createstaticserver.js | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/parallel/test-http-createstaticserver.js b/test/parallel/test-http-createstaticserver.js index a8a8e4c4d84f3d..85f09fe53ad826 100644 --- a/test/parallel/test-http-createstaticserver.js +++ b/test/parallel/test-http-createstaticserver.js @@ -58,6 +58,30 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); }), + fetch(`http://localhost:${port}/test.html?../../../../etc/passwd`).then(async (response) => { + assert(response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.statusText, 'OK'); + assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); + }), + fetch(`http://localhost:${port}/././test.html`).then(async (response) => { + assert(response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.statusText, 'OK'); + assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); + }), + fetch(`http://localhost:${port}/../../test.html`).then(async (response) => { + assert(response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.statusText, 'OK'); + assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); + }), fetch(`http://localhost:${port}/test.html?key=value`).then(async (response) => { assert(response.ok); assert(!response.redirected); @@ -98,6 +122,14 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(response.headers.get('Content-Type'), null); assert.strictEqual(await response.text(), 'Forbidden\n'); }), + fetch(`http://localhost:${port}/.bar/../test.html`).then(async (response) => { + assert(response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.statusText, 'OK'); + assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); + }), fetch(`http://localhost:${port}/.foo`).then(async (response) => { assert(!response.ok); assert(!response.redirected); From 7b404558caeaeb94ec2182f4579ae2ee5049d024 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 20 Dec 2022 16:32:53 +0100 Subject: [PATCH 29/34] add tests with encoded chars and several slashes in a row --- lib/internal/http/static.js | 10 +- test/parallel/test-http-createstaticserver.js | 114 ++++++++++++++++++ 2 files changed, 119 insertions(+), 5 deletions(-) diff --git a/lib/internal/http/static.js b/lib/internal/http/static.js index af9f0819f3f5ad..cb9174d830dab9 100644 --- a/lib/internal/http/static.js +++ b/lib/internal/http/static.js @@ -4,10 +4,8 @@ const { Promise, RegExpPrototypeExec, StringPrototypeEndsWith, - StringPrototypeIncludes, StringPrototypeIndexOf, StringPrototypeLastIndexOf, - StringPrototypeReplace, StringPrototypeSlice, StringPrototypeStartsWith, StringPrototypeToLocaleLowerCase, @@ -17,13 +15,15 @@ const { cwd } = process; const console = require('internal/console/global'); const { createReadStream } = require('fs'); const { Server } = require('_http_server'); -const { URL, isURLInstance, pathToFileURL } = require('internal/url'); +const { URL, formatSymbol, isURLInstance, pathToFileURL } = require('internal/url'); const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); const { validateFunction, validatePort } = require('internal/validators'); +const dot = /\/(\.|%2[eE])/; function filterDotFiles(url, fileURL) { - return !StringPrototypeIncludes(fileURL.pathname, '/.'); + return RegExpPrototypeExec(dot, fileURL.pathname) === null; } +const urlFormatOptions = { auth: false, search: false, fragment: false }; function createStaticServer(options = kEmptyObject) { emitExperimentalWarning('http/static'); @@ -49,7 +49,7 @@ function createStaticServer(options = kEmptyObject) { const url = new URL( req.url === '/' || StringPrototypeStartsWith(req.url, '/?') ? './index.html' : - StringPrototypeReplace(new URL(req.url, 'root://').toString(), 'root://', '.'), + '.' + StringPrototypeSlice(new URL(req.url, 'root://')[formatSymbol](urlFormatOptions), 6), baseDirectoryURL ); diff --git a/test/parallel/test-http-createstaticserver.js b/test/parallel/test-http-createstaticserver.js index 85f09fe53ad826..a8a3ac26937145 100644 --- a/test/parallel/test-http-createstaticserver.js +++ b/test/parallel/test-http-createstaticserver.js @@ -74,6 +74,22 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); }), + fetch(`http://localhost:${port}//test.html`).then(async (response) => { + assert(response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.statusText, 'OK'); + assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); + }), + fetch(`http://localhost:${port}/////test.html`).then(async (response) => { + assert(response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.statusText, 'OK'); + assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); + }), fetch(`http://localhost:${port}/../../test.html`).then(async (response) => { assert(response.ok); assert(!response.redirected); @@ -82,6 +98,50 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); }), + fetch(`http://localhost:${port}/..%2F../test.html`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual((await response.text()).trimEnd(), 'Forbidden'); + }), + fetch(`http://localhost:${port}/%2E%2E%2F%2E%2E/test.html`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual((await response.text()).trimEnd(), 'Forbidden'); + }), + fetch(`http://localhost:${port}/%2E%2E/%2E%2E/test.html`).then(async (response) => { + assert(response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.statusText, 'OK'); + assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); + }), + fetch(`http://localhost:${port}/..%2f../test.html`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual((await response.text()).trimEnd(), 'Forbidden'); + }), + fetch(`http://localhost:${port}/%2e%2e%2f%2e%2e/test.html`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual((await response.text()).trimEnd(), 'Forbidden'); + }), + fetch(`http://localhost:${port}/%2e%2e/%2e%2e/test.html`).then(async (response) => { + assert(response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.statusText, 'OK'); + assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); + }), fetch(`http://localhost:${port}/test.html?key=value`).then(async (response) => { assert(response.ok); assert(!response.redirected); @@ -122,6 +182,46 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(response.headers.get('Content-Type'), null); assert.strictEqual(await response.text(), 'Forbidden\n'); }), + fetch(`http://localhost:${port}///.bar`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual(response.headers.get('Content-Type'), null); + assert.strictEqual(await response.text(), 'Forbidden\n'); + }), + fetch(`http://localhost:${port}/%2Ebar`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual(response.headers.get('Content-Type'), null); + assert.strictEqual(await response.text(), 'Forbidden\n'); + }), + fetch(`http://localhost:${port}///%2Ebar`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual(response.headers.get('Content-Type'), null); + assert.strictEqual(await response.text(), 'Forbidden\n'); + }), + fetch(`http://localhost:${port}/%2ebar`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual(response.headers.get('Content-Type'), null); + assert.strictEqual(await response.text(), 'Forbidden\n'); + }), + fetch(`http://localhost:${port}///%2ebar`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual(response.headers.get('Content-Type'), null); + assert.strictEqual(await response.text(), 'Forbidden\n'); + }), fetch(`http://localhost:${port}/.bar/../test.html`).then(async (response) => { assert(response.ok); assert(!response.redirected); @@ -130,6 +230,20 @@ const { createStaticServer } = require('node:http'); assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); }), + fetch(`http://localhost:${port}/%2Ebar%2F../test.html`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual((await response.text()).trimEnd(), 'Forbidden'); + }), + fetch(`http://localhost:${port}/%2ebar%2f../test.html`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual((await response.text()).trimEnd(), 'Forbidden'); + }), fetch(`http://localhost:${port}/.foo`).then(async (response) => { assert(!response.ok); assert(!response.redirected); From 0bc9f5e655748cc808cad56bcfbdb2a192deedf9 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 18 Sep 2023 11:16:06 +0200 Subject: [PATCH 30/34] liint + `.json` --- lib/internal/http/static.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/internal/http/static.js b/lib/internal/http/static.js index cb9174d830dab9..9180635aff9dee 100644 --- a/lib/internal/http/static.js +++ b/lib/internal/http/static.js @@ -50,7 +50,7 @@ function createStaticServer(options = kEmptyObject) { req.url === '/' || StringPrototypeStartsWith(req.url, '/?') ? './index.html' : '.' + StringPrototypeSlice(new URL(req.url, 'root://')[formatSymbol](urlFormatOptions), 6), - baseDirectoryURL + baseDirectoryURL, ); if (filter != null && !filter(req.url, url)) { @@ -67,6 +67,7 @@ function createStaticServer(options = kEmptyObject) { '.css': 'text/css; charset=UTF-8', '.avif': 'image/avif', '.svg': 'image/svg+xml', + '.json': 'application/json', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', @@ -75,7 +76,7 @@ function createStaticServer(options = kEmptyObject) { ...mimeOverrides, }; const ext = StringPrototypeToLocaleLowerCase( - StringPrototypeSlice(url.pathname, StringPrototypeLastIndexOf(url.pathname, '.')) + StringPrototypeSlice(url.pathname, StringPrototypeLastIndexOf(url.pathname, '.')), ); if (ext in mime) res.setHeader('Content-Type', mime[ext]); @@ -109,7 +110,7 @@ function createStaticServer(options = kEmptyObject) { 'Location', index === -1 ? `${req.url}/` : - `${StringPrototypeSlice(req.url, 0, index)}/${StringPrototypeSlice(req.url, index)}` + `${StringPrototypeSlice(req.url, 0, index)}/${StringPrototypeSlice(req.url, index)}`, ); res.end('Temporary Redirect\n'); } From 29257f9df0f1587d2548957f0ba400b57491ca3e Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 19 Sep 2023 15:45:50 +0200 Subject: [PATCH 31/34] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michaël Zasso Co-authored-by: Tobias Nießen --- doc/api/http.md | 6 ++++-- lib/internal/http/static.js | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/api/http.md b/doc/api/http.md index 5603d152160390..9a32f7e29155b0 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3587,6 +3587,8 @@ added: REPLACEME files are filtered. **Default:** filters all dot files. * `log` {Function|null} called when sending a response to the client. **Default:** `console.log`. + * `statusCode` {integer} The status code that is sent to the client. + * `url` {string} the (origin-relative) URL that was requested. * `onStart` {Function} called the server starts listening to requests. **Default:** logs the URL to the console. @@ -3598,10 +3600,10 @@ When specifying a `host` other than `localhost`, you are exposing your local file system to all the machines that can connect to your computer. If specified and not `null`, `filter` will be called with two arguments: the -first one if the request URL string (the URL that is present in the actual HTTP +first one is the request URL string (the URL that is present in the actual HTTP request), and the second one is the `file:` `URL` that was generated from the base directory and the request URL. If the function returns a falsy value, the -request will be blocked. +server will respond with a 403 HTTP error. If `port` is omitted or is 0, the operating system will assign an arbitrary unused port, which it's output will be the standard output. diff --git a/lib/internal/http/static.js b/lib/internal/http/static.js index 9180635aff9dee..c21f1d76ff24ce 100644 --- a/lib/internal/http/static.js +++ b/lib/internal/http/static.js @@ -71,6 +71,7 @@ function createStaticServer(options = kEmptyObject) { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', + '.wasm': 'application/wasm', '.webp': 'image/webp', '.woff2': 'font/woff2', ...mimeOverrides, From a5ea0d7cad3c45588a3c3701f6df6216e74a4152 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 3 Apr 2024 15:26:12 +0200 Subject: [PATCH 32/34] fix failing test --- lib/internal/http/static.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/internal/http/static.js b/lib/internal/http/static.js index c21f1d76ff24ce..a9cd3e2d4ef685 100644 --- a/lib/internal/http/static.js +++ b/lib/internal/http/static.js @@ -15,7 +15,7 @@ const { cwd } = process; const console = require('internal/console/global'); const { createReadStream } = require('fs'); const { Server } = require('_http_server'); -const { URL, formatSymbol, isURLInstance, pathToFileURL } = require('internal/url'); +const { URL, isURL, pathToFileURL } = require('internal/url'); const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); const { validateFunction, validatePort } = require('internal/validators'); @@ -23,7 +23,6 @@ const dot = /\/(\.|%2[eE])/; function filterDotFiles(url, fileURL) { return RegExpPrototypeExec(dot, fileURL.pathname) === null; } -const urlFormatOptions = { auth: false, search: false, fragment: false }; function createStaticServer(options = kEmptyObject) { emitExperimentalWarning('http/static'); @@ -37,7 +36,7 @@ function createStaticServer(options = kEmptyObject) { log = console.log, onStart = (host, port) => console.log(`Server started on http://${host}:${port}`), } = options; - const directoryURL = isURLInstance(directory) ? directory : pathToFileURL(directory); + const directoryURL = isURL(directory) ? directory : pathToFileURL(directory); // To be used as a base URL, it is necessary that the URL ends with a slash: const baseDirectoryURL = StringPrototypeEndsWith(directoryURL.pathname, '/') ? directoryURL : new URL(`${directoryURL}/`); @@ -49,7 +48,7 @@ function createStaticServer(options = kEmptyObject) { const url = new URL( req.url === '/' || StringPrototypeStartsWith(req.url, '/?') ? './index.html' : - '.' + StringPrototypeSlice(new URL(req.url, 'root://')[formatSymbol](urlFormatOptions), 6), + '.' + StringPrototypeSlice(`${new URL(req.url, 'root://')}`, 6), baseDirectoryURL, ); From c1e6869abeccd6f5b02fb0665be270955266dfaf Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 3 Apr 2024 15:39:49 +0200 Subject: [PATCH 33/34] move `mime` out of `requestHandler` --- lib/internal/http/static.js | 38 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/internal/http/static.js b/lib/internal/http/static.js index a9cd3e2d4ef685..0074c4bdec8725 100644 --- a/lib/internal/http/static.js +++ b/lib/internal/http/static.js @@ -19,6 +19,22 @@ const { URL, isURL, pathToFileURL } = require('internal/url'); const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); const { validateFunction, validatePort } = require('internal/validators'); +const mimeDefault = { + '__proto__': null, + '.html': 'text/html; charset=UTF-8', + '.js': 'text/javascript; charset=UTF-8', + '.css': 'text/css; charset=UTF-8', + '.avif': 'image/avif', + '.svg': 'image/svg+xml', + '.json': 'application/json', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.wasm': 'application/wasm', + '.webp': 'image/webp', + '.woff2': 'font/woff2', +}; + const dot = /\/(\.|%2[eE])/; function filterDotFiles(url, fileURL) { return RegExpPrototypeExec(dot, fileURL.pathname) === null; @@ -36,6 +52,12 @@ function createStaticServer(options = kEmptyObject) { log = console.log, onStart = (host, port) => console.log(`Server started on http://${host}:${port}`), } = options; + const mime = mimeOverrides ? { + __proto__: null, + ...mimeDefault, + ...mimeOverrides, + } : mimeDefault; + const directoryURL = isURL(directory) ? directory : pathToFileURL(directory); // To be used as a base URL, it is necessary that the URL ends with a slash: const baseDirectoryURL = StringPrototypeEndsWith(directoryURL.pathname, '/') ? @@ -59,22 +81,6 @@ function createStaticServer(options = kEmptyObject) { return; } - const mime = { - '__proto__': null, - '.html': 'text/html; charset=UTF-8', - '.js': 'text/javascript; charset=UTF-8', - '.css': 'text/css; charset=UTF-8', - '.avif': 'image/avif', - '.svg': 'image/svg+xml', - '.json': 'application/json', - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.wasm': 'application/wasm', - '.webp': 'image/webp', - '.woff2': 'font/woff2', - ...mimeOverrides, - }; const ext = StringPrototypeToLocaleLowerCase( StringPrototypeSlice(url.pathname, StringPrototypeLastIndexOf(url.pathname, '.')), ); From 2e63aba13f1084273dd1709d0a24f40b7fa3acd7 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 3 Apr 2024 15:45:34 +0200 Subject: [PATCH 34/34] fix lint --- doc/api/http.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/http.md b/doc/api/http.md index 9a32f7e29155b0..59f4dd565388d1 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3587,8 +3587,8 @@ added: REPLACEME files are filtered. **Default:** filters all dot files. * `log` {Function|null} called when sending a response to the client. **Default:** `console.log`. - * `statusCode` {integer} The status code that is sent to the client. - * `url` {string} the (origin-relative) URL that was requested. + * `statusCode` {integer} The status code that is sent to the client. + * `url` {string} the (origin-relative) URL that was requested. * `onStart` {Function} called the server starts listening to requests. **Default:** logs the URL to the console.