Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: add http.createStaticServer #45096

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
318610e
http: add `http.createStaticServer`
aduh95 Oct 20, 2022
77e0416
Add docs and parameters
aduh95 Oct 21, 2022
b1cb361
default to `'localhost'`, return server, update docs
aduh95 Oct 21, 2022
ac549c4
Apply suggestions from code review
aduh95 Oct 21, 2022
d3b189f
Ensure the server doesn't start if it's being required
aduh95 Oct 21, 2022
b6dbf4f
use options instead of function arguments
aduh95 Oct 21, 2022
bc6fd54
s/Simple/Static/
aduh95 Oct 21, 2022
39dbb48
add `mimeOverrides` option
aduh95 Oct 21, 2022
c53b51e
add `serveDotFiles` option
aduh95 Oct 21, 2022
c394473
add `filter` option
aduh95 Oct 22, 2022
6502cbc
`stream.promises.pipeline`
aduh95 Oct 23, 2022
9d408e7
add more validation
aduh95 Oct 23, 2022
9a9caa7
Revert "`stream.promises.pipeline`"
aduh95 Oct 23, 2022
bd9a6bd
fixup! add more validation
aduh95 Oct 23, 2022
a0c7864
pass two args to `filter`
aduh95 Oct 23, 2022
20e3d86
serve index files with correct MIME
aduh95 Oct 23, 2022
98a5563
add tests
aduh95 Oct 23, 2022
1259d43
add fixtures files
aduh95 Oct 23, 2022
f3d663d
lint
aduh95 Oct 23, 2022
60d3f0b
rename `http/server` -> `http/static`
aduh95 Oct 26, 2022
41fc901
list missing features in docs
aduh95 Oct 26, 2022
47d2f8b
add tests to ensure that using a "hidden" folder as root is blocked b…
aduh95 Nov 23, 2022
fa52ee0
Use 403 instead of 401
aduh95 Nov 23, 2022
968efde
fix type in docs
aduh95 Nov 23, 2022
5e018ef
remove `http/static` module
aduh95 Dec 7, 2022
3698f0c
add line return
aduh95 Dec 15, 2022
d8ce4b3
add `log` and `onStart` option to let user control logging
aduh95 Dec 19, 2022
0c91db4
add tests to ensure using `..` can't escape the root dir
aduh95 Dec 19, 2022
7b40455
add tests with encoded chars and several slashes in a row
aduh95 Dec 20, 2022
0bc9f5e
liint + `.json`
aduh95 Sep 18, 2023
29257f9
Apply suggestions from code review
aduh95 Sep 19, 2023
a5ea0d7
fix failing test
aduh95 Apr 3, 2024
c1e6869
move `mime` out of `requestHandler`
aduh95 Apr 3, 2024
2e63aba
fix lint
aduh95 Apr 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -3565,6 +3565,61 @@ server.on('request', (request, res) => {
server.listen(8000);
```

## `http.createStaticServer([options])`

<!-- YAML
added: REPLACEME
-->

> 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* `directory` {string|URL} Root directory from which files would be served.
* `directory` {string|URL} Root directory from which files will be served.

Or "should" / "must"? /cc @nodejs/documentation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^

**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`.
aduh95 marked this conversation as resolved.
Show resolved Hide resolved
* `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])`
Expand Down
2 changes: 2 additions & 0 deletions lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const {
Server,
ServerResponse,
} = require('_http_server');
const createStaticServer = require('internal/http/static');
let maxHeaderSize;

/**
Expand Down Expand Up @@ -127,6 +128,7 @@ module.exports = {
Server,
ServerResponse,
createServer,
createStaticServer,
validateHeaderName,
validateHeaderValue,
get,
Expand Down
155 changes: 155 additions & 0 deletions lib/internal/http/static.js
Original file line number Diff line number Diff line change
@@ -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]);
H4ad marked this conversation as resolved.
Show resolved Hide resolved

try {
try {
const stream = createReadStream(url, { emitClose: false });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know is a nitpick but I think you can cache this object to avoid creating every time.

Also, maybe expose the highWaterMark to the user configure is a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what you mean, do you mean cache the stream? We're going to need a different stream for each request, I think, so I don't see how it'd be useful 🤔 could you send an example maybe?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caching { 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;
1 change: 1 addition & 0 deletions test/fixtures/static-server/.bar
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Content of .bar
1 change: 1 addition & 0 deletions test/fixtures/static-server/.foo/bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Content of .foo/bar.js
1 change: 1 addition & 0 deletions test/fixtures/static-server/file.unsupported_extension
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dummy file to test mimeOverrides option
1 change: 1 addition & 0 deletions test/fixtures/static-server/subfolder/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Content of subfolder/index.html
1 change: 1 addition & 0 deletions test/fixtures/static-server/test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Content of test.html
Loading