Deviopath › Node.js

Safe Static File Server

Hard · HTTP

express.static() (the serve-static package) turns a folder into a website. It has to be careful: a request like /../../etc/passwd must never escape the folder. Implement a simplified version over a virtual file system.

files maps absolute paths to contents, for example { "/index.html": "<h1>Hi</h1>", "/css/app.css": "..." }. Folders exist implicitly: /css is a folder because /css/app.css exists, and the root / is always a folder. Contents are ASCII (1 character = 1 byte). Write serveStatic(files, url, headers) that returns { status, headers, body }. headers are the request headers with lowercase keys. Apply these steps in order:

1. Drop the query string and fragment (from the first ? or #).

2. Decode with decodeURIComponent. If it throws, or the result contains a null byte (\0), respond 400 with body "Bad Request".

3. Normalize: split on /, ignore empty segments and ., and let .. remove the previous segment. If a .. would go above the root, respond 403 with body "Forbidden". Decoding happened first, so %2e%2e counts as .. too.

4. If any remaining segment starts with . (like /.env), respond 404: hidden files are ignored, like the serve-static default.

5. If the normalized path is a file, serve it. If it is a folder: when the decoded path does not end with /, respond 301 with the header Location set to the normalized path plus / and an empty body (serve-static redirects the same way, with a small HTML body that we skip here); otherwise serve <folder>/index.html if it exists. Everything else is 404 with body "Not Found".

A served file gets the headers Content-Type, Content-Length (a string) and ETag, with status 200:

- Content-Type by the lowercase extension: html → text/html; charset=utf-8, css → text/css; charset=utf-8, js → text/javascript; charset=utf-8, json → application/json; charset=utf-8, txt → text/plain; charset=utf-8, png → image/png, svg → image/svg+xml, anything else → application/octet-stream.

- ETag is a weak ETag: W/"<length in hex>-<simpleHash(content) in hex>". simpleHash is given in the starter.

- If the request has if-none-match and it is * or a comma-separated list that contains our ETag (compare without the W/ prefix: the weak comparison), respond 304 with only the ETag header and an empty body.

Error responses (400, 403, 404) have headers: {}. The driver runStatic(files, requests) is already written; each request is [url, headers].

Next: util.promisify and util.callbackify