@techiev2/vajra 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,9 @@ Like the Vajra, this server delivers maximum power in minimal form.
22
22
 
23
23
  ## Changelog
24
24
 
25
+ ### 1.5.0 (2026-01-03)
26
+ - Adds support for res.sendFile.
27
+
25
28
  ### 1.4.3 (2026-01-02)
26
29
  - Adds guardrails for unsafe operations with template paths.
27
30
 
package/_ CHANGED
@@ -1,5 +0,0 @@
1
- const MIMES={html: 'text/html',js: 'application/javascript',css: 'text/css',json: 'application/json',png: 'image/png',jpg: 'image/jpeg',jpeg: 'image/jpeg',gif: 'image/gif',svg: 'image/svg+xml'}
2
-
3
- res.sendFile = (filePath) => new Promise((resolve) => {
4
- createReadStream(filePath).on('open', () => { res.setHeader('Content-Type', MIMES[filePath.split('.').pop()?.toLowerCase()] || 'application/octet-stream'); !res.getHeader('Accept-Ranges') && res.setHeader('Accept-Ranges', 'bytes'); }).on('error', (err) => { (err.code === 'ENOENT' ? default_404(req, res, `${filePath.split('/').slice(-1)} not found.`) : default_500(req, res)); resolve() }).pipe(res).on('finish', resolve).on('error', resolve);
5
- });
package/benchmark.sh ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ declare -A portMap=(
4
+ ["elysia"]=4000
5
+ ["express"]=4001
6
+ ["fastify"]=4002
7
+ ["hono"]=4003
8
+ ["vajra"]=4004
9
+ )
10
+ declare -A runnerMap=(
11
+ ["elysia"]="index.js"
12
+ ["elysia_node"]="index.js"
13
+ ["express"]="index.js"
14
+ ["fastify"]="index.js"
15
+ ["hono"]="index.js"
16
+ ["vajra"]="examples/api.js"
17
+ )
18
+ frameworks=$(ls -d */ | sed 's|/$||' | grep -v auth | grep vajra)
19
+
20
+ LOOPS=5
21
+
22
+ runWrk() {
23
+ port=$1
24
+ sync && echo 3 > tee /proc/sys/vm/drop_caches
25
+ wrk -t16 -c600 -d10s \
26
+ -H "Content-Type: multipart/form-data; boundary=BOUNDARY123456" \
27
+ "http://localhost:${port}/upload" < ../test_upload_2mb.txt
28
+ }
29
+ main() {
30
+ echo "" > report.txt
31
+ for framework in ${frameworks[@]}; do
32
+ cd $framework
33
+ mkdir -p benchmarks
34
+ echo "" > benchmarks/report.txt
35
+ port=${portMap[$framework]}
36
+ runner=${runnerMap[$framework]}
37
+ for i in $(seq 1 $LOOPS); do
38
+ bun $runner &
39
+ sleep 2
40
+ runningPID=$(lsof | grep -P ":$port" | head -n1 | awk '{print $2}')
41
+ echo "$framework running at $runningPID"
42
+ sleep 2
43
+ runWrk $port >> benchmarks/report.txt
44
+ sleep 2
45
+ echo "Killing $framework running at $runningPID"
46
+ kill -9 $runningPID
47
+ done
48
+ avg_rps=$(grep "Requests/sec:" benchmarks/report.txt | awk -F' ' '{print $2}' | awk '{x+=$1; n++} END { print x/n}')
49
+ avg_tps=$(grep "Transfer/sec:" benchmarks/report.txt | awk -F' ' '{print $2}' | awk '{x+=$1; n++} END { print x/n}')
50
+ echo "${framework}
51
+ Average RPS: ${avg_rps}/s
52
+ Average TPS: ${avg_tps}MB/s
53
+
54
+ " >> "../report.txt"
55
+ cd ..
56
+ done
57
+ }
58
+ main
package/examples/api.js CHANGED
@@ -51,4 +51,8 @@ get('/web/users', async ({ query }, res) => {
51
51
  const users = await getUsers(query)
52
52
  const headers = Object.keys(users[0]).slice(0, 2)
53
53
  return res.html(`users.html`, { users, headers })
54
+ })
55
+
56
+ get('/files/:path', async ({ params, params: { path }}, res) => {
57
+ return res.sendFile(`${import.meta.dirname}/${path}`)
54
58
  })
package/index.js CHANGED
@@ -1,25 +1,16 @@
1
1
  import { createServer } from 'node:http'
2
2
  import { readFile, access } from 'node:fs/promises'
3
- import { resolve, isAbsolute } from 'path'
3
+ import { createReadStream, existsSync } from 'node:fs'
4
+ import { resolve } from 'path'
4
5
 
5
6
  const BLOCK_MATCHER=/{{\s*#\s*(?<grpStart>\w+)\s*}}\s*(?<block>.*?)\s*{{\s*\/\s*(?<grpEnd>\w+)\s*}}/gmsi; const INNER_BLOCK_MATCHER = /{\s*(.*?)\s*}/gmsi
6
7
  const LOOP_MATCHER=/({\s*\w+@\s*})/gmis; const ESCAPE = { regex: /[&<>"']/g, map: { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;' }}
8
+ const MIMES={html: 'text/html; charset=utf-8',js: 'application/javascript; charset=utf-8',css: 'text/css; charset=utf-8',json: 'application/json; charset=utf-8',png: 'image/png',jpg: 'image/jpeg',jpeg: 'image/jpeg',gif: 'image/gif',svg: 'image/svg+xml', webp: 'image/webp', txt: 'text/plain; charset=utf-8'}
7
9
 
8
- function default_404({ url, method, isPossibleJSON }, res) {
9
- const message = `Route ${url} not found for method ${method}.`
10
- res.status(404); return isPossibleJSON ? res.json({ message }) : res.writeMessage(message)
11
- }
12
- function default_405({ url, method, isPossibleJSON }, res) {
13
- const message = `Method ${method} not allowed by route ${url}.`
14
- res.status(405); return isPossibleJSON ? res.json({ message }) : res.writeMessage(message)
15
- }
16
- function default_500({ url, method }, res, error) {
17
- process.env.DEBUG && console.log({ error })
18
- res.status(500).writeMessage(process.env.DEBUG ? `${error.stack}` : `Server error.\nRoute: ${url}\nMethod: ${method}\nTimestamp: ${new Date().getTime()}\n`)
19
- }
20
- function default_413(res) {
21
- res.status(413).writeMessage('Payload Too Large')
22
- }
10
+ function default_404({ url, method, isPossibleJSON }, res, message) { message = message || `Route ${url} not found for method ${method}.`; res.status(404); return isPossibleJSON ? res.json({ message }) : res.writeMessage(message) }
11
+ function default_405({ url, method, isPossibleJSON }, res, message) { message = message || `Method ${method} not allowed by route ${url}.`; res.status(405); return isPossibleJSON ? res.json({ message }) : res.writeMessage(message) }
12
+ function default_500({ url, method }, res, error) { process.env.DEBUG && console.log({ error }); res.status(500).writeMessage(process.env.DEBUG && error?.stack ? `${error.stack}` : `Server error.\nRoute: ${url}\nMethod: ${method}\nTimestamp: ${new Date().getTime()}\n`) }
13
+ function default_413(res) { res.status(413).writeMessage('Payload Too Large') }
23
14
 
24
15
  const MAX_MB = 2; const MAX_FILE_SIZE = MAX_MB * 1024 * 1024
25
16
 
@@ -36,13 +27,19 @@ export default class Vajra {
36
27
  res.sent = false; res.status = (/**@type{code} Number*/ code) => { if (!+code || +code < 100 || +code > 599) { throw new Error(`Invalid status code ${code}`) }; res.statusCode = code; res.statusSet = true; return res }
37
28
  res.json = data => {
38
29
  if (res.sent) return res; if (!res.statusSet) res.statusCode = 200
39
- const response = JSON.stringify(data); res.sent = true; res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Length', Buffer.from(response).byteLength); res.write(response); res.end(); return res
30
+ const _data = JSON.stringify(data); res.sent = true; res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Length', Buffer.byteLength(_data)); res.write(_data); res.end(); return res
40
31
  }
41
32
  res.writeMessage = (message = '') => {
42
33
  if (res.sent) { return res }; if (!message) { res.status(500); message = 'Server error'; }
43
- res.sent = true; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Length', Buffer.from(message).byteLength); res.write(message); res.end()
34
+ res.sent = true; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Length', Buffer.byteLength(message)); res.write(message); res.end()
44
35
  return res
45
36
  }
37
+ res.sendFile = (filePath) => {
38
+ let [filePath_, extension] = filePath.split('.')
39
+ const _filePath = !extension ? filePath : existsSync(filePath) ? filePath : existsSync(`${filePath_}.${extension.toLowerCase()}`) ? `${filePath_}.${extension.toLowerCase()}` : existsSync(`${filePath_}.${extension.toUpperCase()}`) ? `${filePath_}.${extension.toLowerCase()}` : ''
40
+ if (!filePath) { return default_404(req, res, new Error(`File ${filePath} not found`)) }
41
+ return new Promise((resolve) => createReadStream(_filePath).on('open', () => { res.setHeader('Content-Type', MIMES[filePath.split('.').pop()?.toLowerCase()] || 'application/octet-stream'); !res.getHeader('Accept-Ranges') && res.setHeader('Accept-Ranges', 'bytes'); }).on('error', (err) => { (err.code === 'ENOENT' ? default_404(req, res, `${filePath.split('/').slice(-1)} not found.`) : default_500(req, res, err)); resolve() }).pipe(res).on('finish', resolve).on('error', resolve))
42
+ };
46
43
  function sanitize(value) { if (value == null) return ''; return String(value).replace(ESCAPE.regex, m => ESCAPE.map[m]); }
47
44
  res.html = async (templatePath, data = {}) => {
48
45
  templatePath = resolve(templatePath); const appRoot = resolve(process.cwd()); const root = this.#props.viewRoot ? resolve(this.#props.viewRoot) : appRoot;
@@ -70,19 +67,19 @@ export default class Vajra {
70
67
  const formDataMatcher = /Content-Disposition: form-data; name=['"](?<name>[^"']+)['"]\s+(?<value>.*?)$/smi;
71
68
  let boundaryMatch = (req.headers['Content-Type'] || '').match(/boundary=(.*)/); const boundary = boundaryMatch ? '--' + boundaryMatch[1] : null; const fileDataMatcher = /^Content-Disposition:.*?name=["'](?<field>[^"']+)["'].*?filename=["'](?<fileName>[^"']+)["'].*?Content-Type:\s*(?<contentType>[^\r\n]*)\r?\n\r?\n(?<content>[\s\S]*)$/ims
72
69
  req.on('end', async () => {
73
- req.files = []; if (boundary) { req.rawData.split(boundary).filter(Boolean).map((line) => {
70
+ req.files = []; if (boundary) {
71
+ req.rawData.split(boundary).filter(Boolean).map((line) => {
74
72
  let key, value; if (line.includes('filename')) { req.files.push(fileDataMatcher.exec(line)?.groups || {}); return }
75
- [key, value] = Object.values(line.match(formDataMatcher)?.groups || {}); (key && value) && Object.assign(req.formData, { [key]: value }); return
73
+ [key, value] = Object.values(line.match(formDataMatcher)?.groups || {}); (key && value) && Object.assign(req.formData, { [key]: value }); return
76
74
  })
77
75
  }
78
76
  if (Object.keys(req.formData).length) { req.body = req.formData } else {
79
77
  try { req.body = JSON.parse(req.rawData); req.isPossibleJSON = true } catch (_) { req.body = Object.fromEntries(req.rawData.split('&').map((pair) => pair.split('='))) }
80
78
  }; setImmediate(runMiddlwares)
81
- })
82
- req.cookies = Object.fromEntries((req.headers.Cookie || req.headers.cookie || '').split(/;\s*/).map((k) => k.split('=')).map(([k, v]) => [k.trim(), decodeURIComponent(v).trim()]))
79
+ }); req.cookies = Object.fromEntries((req.headers.Cookie || req.headers.cookie || '').split(/;\s*/).map((k) => k.split('=')).map(([k, v]) => [k.trim(), decodeURIComponent(v).trim()]))
83
80
  })
84
81
  async function handleRoute() {
85
- let _url = req.url.split('?')[0]; if (_url.endsWith('/')) _url = _url.split('/').slice(0, -1).join('/')
82
+ req.url = decodeURIComponent(req.url); let _url = req.url.split('?')[0]; if (_url.endsWith('/')) _url = _url.split('/').slice(0, -1).join('/')
86
83
  let match_; const directHandler = (Vajra.#straightRoutes[_url] || Vajra.#straightRoutes[`${_url}/`] || {})[req.method.toLowerCase()]
87
84
  if (directHandler) { try { await directHandler(req, res); if (!res.sent && !res.writableEnded) res.end() } catch (error) { return default_500(req, res, error) }; return }
88
85
  Object.entries(Vajra.#routes).map(([route, handler]) => {
@@ -97,7 +94,7 @@ export default class Vajra {
97
94
  function start({ port, host = '127.0.0.1' }, cb) { Vajra.#app.listen(port, () => { console.log(`App listening at http://${host}:${port}`); if (typeof cb === 'function') { cb() } }); return defaults }
98
95
  function register(method, path, handler) {
99
96
  const paramMatcher = /.*?(?<param>\:[a-zA-Z]{1,})/g; let pathMatcherStr = path
100
- path.matchAll(paramMatcher).forEach(match => pathMatcherStr = pathMatcherStr.replace(match.groups.param, `{0,1}(?<${match.groups.param.slice(1)}>[\\w|\.]+)`))
97
+ path.matchAll(paramMatcher).forEach(match => pathMatcherStr = pathMatcherStr.replace(match.groups.param, `{0,1}(?<${match.groups.param.slice(1)}>[\\w|\\/|\\.|\\s|\\-]+)`))
101
98
  if (path !== '/' && pathMatcherStr.endsWith('/')) { pathMatcherStr = pathMatcherStr.replace(/(\/)$/, '/?') }
102
99
  if (!paramMatcher.exec(path)?.groups) { Vajra.#straightRoutes[pathMatcherStr] = Object.assign(Vajra.#straightRoutes[pathMatcherStr] || {}, { [method]: handler }); return }
103
100
  Vajra.#routes[pathMatcherStr] = Object.assign(Vajra.#routes[pathMatcherStr] || {}, {[method]: handler}); return defaults
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techiev2/vajra",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Blazing-fast, zero-dependency Node.js server with routing, middleware, multipart uploads, and templating. 111 lines · ~95k req/s · ~52 MB idle.",
5
5
  "keywords": [
6
6
  "http-server",
package/tests/tests.js CHANGED
@@ -2,6 +2,8 @@ import assert from 'node:assert';
2
2
  import { encode } from 'node:querystring';
3
3
  import { afterEach, beforeEach, suite, test } from 'node:test';
4
4
  import { randomBytes, randomUUID } from 'node:crypto';
5
+ import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
6
+ import path, { resolve } from 'node:path';
5
7
 
6
8
  import pkg from '../package.json' with {type: 'json'}
7
9
  import { sign, verify } from '../libs/auth/jwt.js';
@@ -17,9 +19,9 @@ async function getJSON(url, method = 'GET', body) {
17
19
  return (await getResponse(url, method, body)).json()
18
20
  }
19
21
 
22
+ const BASE_URL = 'http://localhost:4002'
20
23
 
21
24
  suite('Test HTTP API at port 4002', () => {
22
- const BASE_URL = 'http://localhost:4002'
23
25
  suite('Test HTTP GET', () => {
24
26
  test('Basic HTTP GET to respond with a now and empty query/params', async () => {
25
27
  assert.deepEqual((await (await getResponse(BASE_URL)).text()), 'Hello from Vajra ⚡')
@@ -35,6 +37,48 @@ suite('Test HTTP API at port 4002', () => {
35
37
  assert.deepEqual(res_query, query)
36
38
  assert.deepEqual(res_params, {})
37
39
  })
40
+ test('Plain ../ traversal should be normalized by URL parser and result in 404 (safe)', async () => {
41
+ const url = `${BASE_URL}/files/../../../../etc/passwd`
42
+ const res = await getResponse(url)
43
+ // It should NOT succeed (200) and typically 404 because file doesn't exist inside app root
44
+ assert.notStrictEqual(res.status, 200)
45
+ assert.strictEqual(res.status, 404)
46
+ })
47
+
48
+ test('URL-encoded ../ (%2e%2e) should be decoded BEFORE normalization, still resulting in safe 404', async () => {
49
+ const url = `${BASE_URL}/files/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd`
50
+ const res = await getResponse(url)
51
+ assert.notStrictEqual(res.status, 200)
52
+ // Expected: pathname becomes /etc/passwd → safe 404
53
+ })
54
+
55
+ test('Double-encoded traversal should also be normalized safely', async () => {
56
+ const url = `${BASE_URL}/files/%252e%252e/%252e%252e/%252e%252e/%252e%252e/etc/passwd`
57
+ const res = await getResponse(url)
58
+ assert.notStrictEqual(res.status, 200)
59
+ })
60
+
61
+ test('Attempt with null byte (old exploit) should not crash and return error', async () => {
62
+ // Note: modern Node.js rejects %00 in URLs early, often with parse error
63
+ const url = `${BASE_URL}/files/test_files/hello.txt%00../../etc/passwd`
64
+ const res = await getResponse(url)
65
+ assert.notStrictEqual(res.status, 200)
66
+ // Likely 400 or 404 depending on framework
67
+ })
68
+
69
+ test('Absolute path attempt should be treated as relative (inside app root) and 404', async () => {
70
+ const url = `${BASE_URL}/files//etc/passwd` // leading // becomes /
71
+ const res = await getResponse(url)
72
+ assert.notStrictEqual(res.status, 200)
73
+ })
74
+
75
+ test('Very deep normalized path (many ../ collapsing to root) should still be safe', async () => {
76
+ const deep = '../'.repeat(50) + 'hello.txt'
77
+ const url = `${BASE_URL}/files/${deep}`
78
+ const res = await getResponse(url)
79
+ // After normalization: /hello.txt → tries to open hello.txt in app root → 404 (unless exists)
80
+ assert.strictEqual(res.status, 404) // or whatever your non-existent handler returns
81
+ })
38
82
  })
39
83
  suite('Test HTTP POST', () => {
40
84
  test('Basic HTTP POST to respond with a now and body', async () => {
@@ -299,4 +343,163 @@ suite('Tests for library functions', () => {
299
343
  })
300
344
  });
301
345
 
302
- })
346
+ })
347
+
348
+
349
+ suite('res.sendFile(path)', async () => {
350
+
351
+ const TEST_DIR = path.resolve(`${import.meta.dirname}/../examples/test_files`);
352
+ const textContent = 'Hello from Vajra!\nThis is a test file.'
353
+ const textContentBuffer = new Uint8Array(Buffer.from(textContent))
354
+ const minimalJpeg = Buffer.from([
355
+ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,
356
+ 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,
357
+ 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
358
+ 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07,
359
+ 0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b, 0x0a, 0x0b, 0x0b, 0x0d,
360
+ 0x0e, 0x12, 0x10, 0x0d, 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10,
361
+ 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0c, 0x0f, 0x17, 0x18, 0x16, 0x14,
362
+ 0x18, 0x12, 0x14, 0x15, 0x14, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00, 0x01,
363
+ 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xff, 0xc4, 0x00, 0x14, 0x00, 0x01,
364
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
365
+ 0x00, 0x00, 0x00, 0x08, 0xff, 0xc4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00,
366
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
367
+ 0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3f, 0x00,
368
+ 0xd2, 0xcf, 0x20, 0xff, 0xd9
369
+ ]);
370
+ beforeEach(async () => {
371
+ await mkdir(TEST_DIR, { recursive: true });
372
+ await writeFile(`${TEST_DIR}/test.txt`, textContentBuffer)
373
+ await writeFile(`${TEST_DIR}/test.TXT`, textContentBuffer)
374
+ await writeFile(path.join(TEST_DIR, 'test.jpg'), minimalJpeg);
375
+ await writeFile(path.join(TEST_DIR, 'test.JPG'), minimalJpeg);
376
+ await writeFile(`${TEST_DIR}/unknown.ext`, new Uint8Array(Buffer.from('some data')))
377
+ });
378
+ afterEach(async () => {
379
+ await rm(TEST_DIR, { recursive: true, force: true });
380
+ });
381
+ test('On GET at /files/test.txt, server should return the file contents', async () => {
382
+ const filePath = `test_files/test.txt`
383
+ const url = `${BASE_URL}/files/${filePath}`
384
+ const apiResponseText = await (await getResponse(url)).text()
385
+ const filetext = (await readFile(resolve(`${import.meta.dirname}/../examples/${filePath}`))).toString()
386
+ assert.strictEqual(apiResponseText, filetext)
387
+ })
388
+ test('On GET at /files/random.txt, server should return the file contents', async () => {
389
+ const id = randomUUID()
390
+ const filePath = `test_files/${id}`
391
+ const url = `${BASE_URL}/files/${filePath}`
392
+ const res = await getResponse(url)
393
+ const json = await res.json()
394
+ assert.strictEqual(res.status, 404)
395
+ assert.strictEqual(json.message, `${id} not found.`)
396
+ })
397
+ test('On GET at /files/test.txt, server should return the file contents with correct Content-Type', async () => {
398
+ const filePath = 'test_files/test.txt'
399
+ const url = `${BASE_URL}/files/${filePath}`
400
+ const res = await getResponse(url)
401
+
402
+ assert.strictEqual(res.status, 200)
403
+ assert.strictEqual(res.headers.get('content-type'), 'text/plain; charset=utf-8')
404
+ assert.strictEqual(res.headers.get('accept-ranges'), 'bytes')
405
+
406
+ const apiResponseText = await res.text()
407
+ assert.strictEqual(apiResponseText.trim(), textContent.trim())
408
+ })
409
+
410
+ test('On GET at /files/test.png, server should return the image with correct Content-Type and exact bytes', async () => {
411
+ const filePath = 'test_files/test.jpg'
412
+ const url = `${BASE_URL}/files/${filePath}`
413
+ const res = await getResponse(url)
414
+
415
+ assert.strictEqual(res.status, 200)
416
+ assert.strictEqual(res.headers.get('content-type'), 'image/jpeg')
417
+ assert.strictEqual(res.headers.get('accept-ranges'), 'bytes')
418
+
419
+ const buffer = Buffer.from(await res.arrayBuffer())
420
+ const fileData = await readFile(resolve(`${import.meta.dirname}/../examples/test_files/test.jpg`))
421
+ assert.strictEqual(buffer.length, fileData.length)
422
+ assert.ok(buffer.equals(fileData))
423
+ })
424
+
425
+ test('On GET at non-existent random file, server should return 404 with filename in message', async () => {
426
+ const id = randomUUID()
427
+ const filePath = `test_files/${id}.txt`
428
+ const url = `${BASE_URL}/files/${filePath}`
429
+ const res = await getResponse(url)
430
+ const json = await res.json()
431
+
432
+ assert.strictEqual(res.status, 404)
433
+ assert.strictEqual(json.message, `${id}.txt not found.`)
434
+ })
435
+
436
+ test('On GET at file with unknown extension, server should use application/octet-stream', async () => {
437
+ const unknownFileName = 'unknown.ext'
438
+ await writeFile(path.join(TEST_DIR, unknownFileName), 'some data')
439
+
440
+ const filePath = `test_files/${unknownFileName}`
441
+ const url = `${BASE_URL}/files/${filePath}`
442
+ const res = await getResponse(url)
443
+
444
+ assert.strictEqual(res.status, 200)
445
+ assert.strictEqual(res.headers.get('content-type'), 'application/octet-stream')
446
+ assert.strictEqual(await res.text(), 'some data')
447
+ })
448
+
449
+ test('Path traversal with ../ should not allow access outside test_files directory', async () => {
450
+ const url = `${BASE_URL}/files/../examples/test_files/test.txt`
451
+ const res = await getResponse(url)
452
+ assert.notStrictEqual(res.status, 200) // Should be 404 (or 400/403 if sanitized better)
453
+ })
454
+
455
+ test('Path traversal with encoded %2e%2e should be blocked', async () => {
456
+ const url = `${BASE_URL}/files/%2e%2e/%2e%2e/examples/test_files/test.txt`
457
+ const res = await getResponse(url)
458
+ assert.notStrictEqual(res.status, 200)
459
+ })
460
+
461
+ test('Path traversal with null byte %00 should be rejected (if not already handled by framework)', async () => {
462
+ const url = `${BASE_URL}/files/test_files/test.txt%00../../etc/passwd`
463
+ const res = await getResponse(url)
464
+ assert.notStrictEqual(res.status, 200)
465
+ })
466
+
467
+ test('Request for directory (trailing slash) should not serve or list contents', async () => {
468
+ const url = `${BASE_URL}/files/test_files/`
469
+ const res = await getResponse(url)
470
+ assert.notStrictEqual(res.status, 200) // Expect 404 or 400, no directory listing
471
+ })
472
+
473
+ test('File with multiple extensions should use MIME based on final extension', async () => {
474
+ const multiFile = 'archive.tar.gz'
475
+ await writeFile(path.join(TEST_DIR, multiFile), 'gzipped tar')
476
+
477
+ const filePath = `test_files/${multiFile}`
478
+ const url = `${BASE_URL}/files/${filePath}`
479
+ const res = await getResponse(url)
480
+
481
+ assert.strictEqual(res.status, 200)
482
+ const ct = res.headers.get('content-type')
483
+ assert.ok(ct.includes('gzip') || ct.includes('octet-stream'))
484
+ })
485
+
486
+ test('Case-insensitive extension handling', async () => {
487
+ const upperFile = 'test.JPG'
488
+ // await writeFile(path.join(TEST_DIR, upperFile), imageContent)
489
+ const filePath = `test_files/${upperFile}`
490
+ const url = `${BASE_URL}/files/${filePath}`
491
+ const res = await getResponse(url)
492
+ assert.strictEqual(res.status, 200)
493
+ assert.strictEqual(res.headers.get('content-type'), 'image/jpeg')
494
+ })
495
+
496
+ test('Query parameters should be ignored and file still served correctly', async () => {
497
+ const url = `${BASE_URL}/files/test_files/test.txt?cache_bust=123`
498
+ const res = await getResponse(url)
499
+
500
+ assert.strictEqual(res.status, 200)
501
+ const text = await res.text()
502
+ assert.strictEqual(text.trim(), textContent.trim())
503
+ })
504
+
505
+ });