diff --git a/doc/api/http.md b/doc/api/http.md index 30fb7cd030f51e..59f4dd565388d1 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3565,6 +3565,61 @@ server.on('request', (request, res) => { server.listen(8000); ``` +## `http.createStaticServer([options])` + + + +> 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. + **Default:** `process.cwd()`. + * `port` {number} + * `host` {string} **Default:** `'localhost'` + * `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 + 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. + +* 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. +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 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 +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. + +```bash +# Starts serving the cwd on a random port: +node -e 'http.createStaticServer()' + +# To start serving on the port 8080 using /path/to/dir as the root: +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 -e 'http.createStaticServer({directory: "/path/to/dir", port: 8080, host: "0.0.0.0"})' +``` + ## `http.get(options[, callback])` ## `http.get(url[, options][, callback])` diff --git a/lib/http.js b/lib/http.js index 9fce02d6e3b3ac..c626bb7d70381b 100644 --- a/lib/http.js +++ b/lib/http.js @@ -43,6 +43,7 @@ const { Server, ServerResponse, } = require('_http_server'); +const createStaticServer = require('internal/http/static'); let maxHeaderSize; /** @@ -127,6 +128,7 @@ module.exports = { Server, ServerResponse, createServer, + createStaticServer, validateHeaderName, validateHeaderValue, get, diff --git a/lib/internal/http/static.js b/lib/internal/http/static.js new file mode 100644 index 00000000000000..0074c4bdec8725 --- /dev/null +++ b/lib/internal/http/static.js @@ -0,0 +1,155 @@ +'use strict'; + +const { + Promise, + RegExpPrototypeExec, + StringPrototypeEndsWith, + StringPrototypeIndexOf, + StringPrototypeLastIndexOf, + StringPrototypeSlice, + StringPrototypeStartsWith, + StringPrototypeToLocaleLowerCase, +} = primordials; + +const { cwd } = process; +const console = require('internal/console/global'); +const { createReadStream } = require('fs'); +const { Server } = require('_http_server'); +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; +} + +function createStaticServer(options = kEmptyObject) { + emitExperimentalWarning('http/static'); + + const { + directory = cwd(), + port, + host = 'localhost', + mimeOverrides, + filter = filterDotFiles, + 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, '/') ? + directoryURL : new URL(`${directoryURL}/`); + + if (port != null) validatePort(port); + if (filter != null) validateFunction(filter, 'options.filter'); + + const server = new Server(async (req, res) => { + const url = new URL( + req.url === '/' || StringPrototypeStartsWith(req.url, '/?') ? + './index.html' : + '.' + StringPrototypeSlice(`${new URL(req.url, 'root://')}`, 6), + baseDirectoryURL, + ); + + if (filter != null && !filter(req.url, url)) { + log?.(403, req.url); + res.statusCode = 403; + res.end('Forbidden\n'); + return; + } + + 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('end', resolve); + stream.pipe(res); + }); + log?.(200, req.url); + } catch (err) { + if (err?.code === 'EISDIR') { + if (StringPrototypeEndsWith(req.url, '/') || RegExpPrototypeExec(/^[^?]+\/\?/, req.url) !== null) { + const stream = createReadStream(new URL('./index.html', url), { + emitClose: false, + }); + res.setHeader('Content-Type', mime['.html']); + await new Promise((resolve, reject) => { + stream.on('error', reject); + 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( + 'Location', + index === -1 ? + `${req.url}/` : + `${StringPrototypeSlice(req.url, 0, index)}/${StringPrototypeSlice(req.url, index)}`, + ); + res.end('Temporary Redirect\n'); + } + } else { + throw err; + } + } + } catch (err) { + if (err?.code === 'ENOENT') { + log?.(404, req.url); + res.statusCode = 404; + res.end('Not Found\n'); + } else { + log?.(500, req.url, err); + res.statusCode = 500; + res.end('Internal Error\n'); + } + } + }); + const callback = () => { + const { address, family, port } = server.address(); + const host = family === 'IPv6' ? `[${address}]` : address; + onStart?.(host, port); + }; + if (host != null) { + server.listen(port, host, callback); + } else if (port != null) { + server.listen(port, callback); + } else { + server.listen(callback); + } + + return server; +} + +module.exports = createStaticServer; diff --git a/test/fixtures/static-server/.bar b/test/fixtures/static-server/.bar new file mode 100644 index 00000000000000..9b9f6dfc2e5b98 --- /dev/null +++ b/test/fixtures/static-server/.bar @@ -0,0 +1 @@ +Content of .bar diff --git a/test/fixtures/static-server/.foo/bar.js b/test/fixtures/static-server/.foo/bar.js new file mode 100644 index 00000000000000..243c505301a5c9 --- /dev/null +++ b/test/fixtures/static-server/.foo/bar.js @@ -0,0 +1 @@ +Content of .foo/bar.js diff --git a/test/fixtures/static-server/file.unsupported_extension b/test/fixtures/static-server/file.unsupported_extension new file mode 100644 index 00000000000000..f5586f603b90c0 --- /dev/null +++ b/test/fixtures/static-server/file.unsupported_extension @@ -0,0 +1 @@ +Dummy file to test mimeOverrides option diff --git a/test/fixtures/static-server/subfolder/index.html b/test/fixtures/static-server/subfolder/index.html new file mode 100644 index 00000000000000..107626ecbd0853 --- /dev/null +++ b/test/fixtures/static-server/subfolder/index.html @@ -0,0 +1 @@ +Content of subfolder/index.html diff --git a/test/fixtures/static-server/test.html b/test/fixtures/static-server/test.html new file mode 100644 index 00000000000000..e8ba3ae6f1e442 --- /dev/null +++ b/test/fixtures/static-server/test.html @@ -0,0 +1 @@ +Content of test.html diff --git a/test/parallel/test-http-createstaticserver.js b/test/parallel/test-http-createstaticserver.js new file mode 100644 index 00000000000000..a8a3ac26937145 --- /dev/null +++ b/test/parallel/test-http-createstaticserver.js @@ -0,0 +1,413 @@ +'use strict'; +const { mustCall } = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('node:assert'); +const { MIMEType } = require('node:util'); +const { createStaticServer } = require('node:http'); + +[0, 1, 1n, '', '1', true, false, NaN, Symbol(), {}, []].forEach((filter) => { + assert.throws(() => createStaticServer({ filter }), { code: 'ERR_INVALID_ARG_TYPE' }); +}); + +[true, false, Symbol(), () => {}, {}, [], 0n, 1n, '', Number.MAX_SAFE_INTEGER].forEach((port) => { + assert.throws(() => createStaticServer({ port }), { code: 'ERR_SOCKET_BAD_PORT' }); +}); + +[true, false, Symbol(), () => {}, {}, [], 0n, 1n, 0, 1].forEach((directory) => { + assert.throws(() => createStaticServer({ directory }), { code: 'ERR_INVALID_ARG_TYPE' }); +}); + + +{ + 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, 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.js`).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'); + }), + ]).then(mustCall()).finally(() => server.close()); + })); +} + +{ + const server = createStaticServer({ + directory: fixtures.path('static-server'), + }).once('listening', mustCall(() => { + const { port } = server.address(); + Promise.all([ + 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?../../../../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`).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}/..%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); + 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}/file.unsupported_extension`).then(async (response) => { + assert(response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.statusText, 'OK'); + assert.strictEqual(response.headers.get('Content-Type'), null); + assert.strictEqual((await response.text()).trimEnd(), 'Dummy file to test mimeOverrides option'); + }), + fetch(`http://localhost:${port}/subfolder`).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 subfolder/index.html'); + }), + fetch(`http://localhost:${port}/subfolder/`).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 subfolder/index.html'); + }), + 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}///.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); + 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}/%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); + 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, 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, 403); + assert.strictEqual(response.statusText, 'Forbidden'); + assert.strictEqual(response.headers.get('Content-Type'), null); + assert.strictEqual(await response.text(), 'Forbidden\n'); + }), + ]).then(mustCall()).finally(() => server.close()); + })); +} + +{ + const server = createStaticServer({ + directory: fixtures.fileURL('static-server'), + mimeOverrides: { '.html': 'custom/mime', '.unsupported_extension': 'custom/mime2' }, + }).once('listening', mustCall(() => { + const { port } = server.address(); + Promise.all([ + 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, 'custom/mime'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of test.html'); + }), + fetch(`http://localhost:${port}/file.unsupported_extension`).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, 'custom/mime2'); + assert.strictEqual((await response.text()).trimEnd(), 'Dummy file to test mimeOverrides option'); + }), + fetch(`http://localhost:${port}/subfolder`).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, 'custom/mime'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of subfolder/index.html'); + }), + ]).then(mustCall()).finally(() => server.close()); + })); +} + +{ + const server = createStaticServer({ + directory: fixtures.path('static-server'), + filter: null, + }).once('listening', mustCall(() => { + const { port } = server.address(); + Promise.all([ + 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}/.bar`).then(async (response) => { + assert(response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.statusText, 'OK'); + assert.strictEqual(response.headers.get('Content-Type'), null); + assert.strictEqual((await response.text()).trimEnd(), 'Content of .bar'); + }), + fetch(`http://localhost:${port}/.foo`).then(async (response) => { + assert(!response.ok); + assert(response.redirected); + assert.strictEqual(response.status, 404); + assert.strictEqual(response.statusText, 'Not Found'); + assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); + }), + fetch(`http://localhost:${port}/.foo/`).then(async (response) => { + assert(!response.ok); + assert(!response.redirected); + assert.strictEqual(response.status, 404); + assert.strictEqual(response.statusText, 'Not Found'); + assert.strictEqual(new MIMEType(response.headers.get('Content-Type')).essence, 'text/html'); + }), + fetch(`http://localhost:${port}/.foo/bar.js`).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/javascript'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of .foo/bar.js'); + }), + ]).then(mustCall()).finally(() => server.close()); + })); +} + +{ + const server = createStaticServer({ + directory: fixtures.path('static-server'), + mimeOverrides: { '.bar': 'foo/bar;key=value' }, + filter: (url, fileURL) => { + assert(fileURL instanceof URL); + 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([ + fetch(`http://localhost:${port}/test.html`).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`).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, 'foo/bar'); + assert.strictEqual((await response.text()).trimEnd(), 'Content of .bar'); + }), + fetch(`http://localhost:${port}/.foo`).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); + }), + fetch(`http://localhost:${port}/.foo/`).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); + }), + fetch(`http://localhost:${port}/.foo/bar.js`).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'); + }), + ]).then(mustCall()).finally(() => server.close()); + })); +}