koa-classic-server 2.6.1 → 3.0.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.
Files changed (57) hide show
  1. package/CLAUDE.md +101 -0
  2. package/README.md +564 -591
  3. package/__tests__/benchmark-results-v3.0.0.txt +372 -0
  4. package/__tests__/benchmark.js +1 -1
  5. package/__tests__/caching-headers.test.js +30 -30
  6. package/__tests__/compression-fixtures/data.json +1 -0
  7. package/__tests__/compression-fixtures/large.txt +1 -0
  8. package/__tests__/compression-fixtures/small.txt +1 -0
  9. package/__tests__/compression.test.js +284 -0
  10. package/__tests__/customTest/serversToLoad.util.js +5 -5
  11. package/__tests__/demo-regex-index.js +4 -4
  12. package/__tests__/deprecation-warnings.test.js +71 -183
  13. package/__tests__/directory-sorting-links.test.js +1 -1
  14. package/__tests__/dt-unknown.test.js +39 -28
  15. package/__tests__/ejs.test.js +1 -1
  16. package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
  17. package/__tests__/hidden-fixtures/.env +2 -0
  18. package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
  19. package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
  20. package/__tests__/hidden-fixtures/data.key +1 -0
  21. package/__tests__/hidden-fixtures/file.secret +1 -0
  22. package/__tests__/hidden-fixtures/index.html +1 -0
  23. package/__tests__/hidden-fixtures/normal.txt +1 -0
  24. package/__tests__/hidden-fixtures/subdir/.env +1 -0
  25. package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
  26. package/__tests__/hidden-option.test.js +407 -0
  27. package/__tests__/hideExtension.test.js +70 -13
  28. package/__tests__/index-option.test.js +18 -16
  29. package/__tests__/index.test.js +14 -10
  30. package/__tests__/listing.test.js +437 -0
  31. package/__tests__/logger.test.js +232 -0
  32. package/__tests__/range-fixtures/sample.txt +1 -0
  33. package/__tests__/range.test.js +223 -0
  34. package/__tests__/security-headers.test.js +165 -0
  35. package/__tests__/security.test.js +148 -162
  36. package/__tests__/server-cache-fixtures/large.txt +1 -0
  37. package/__tests__/server-cache-fixtures/small.txt +1 -0
  38. package/__tests__/server-cache.test.js +594 -0
  39. package/__tests__/symlink.test.js +18 -15
  40. package/__tests__/template-timeout.test.js +321 -0
  41. package/docs/ACTION_PLAN.md +293 -0
  42. package/docs/CHANGELOG.md +289 -0
  43. package/docs/CODE_REVIEW.md +2 -0
  44. package/docs/DOCUMENTATION.md +259 -32
  45. package/docs/EXAMPLES_INDEX_OPTION.md +3 -3
  46. package/docs/FLOW_DIAGRAM.md +15 -13
  47. package/docs/INDEX_OPTION_PRIORITY.md +2 -2
  48. package/docs/OPTIMIZATION_HTTP_CACHING.md +2 -0
  49. package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
  50. package/docs/PERFORMANCE_COMPARISON.md +7 -7
  51. package/docs/security_improvement_for_V3.md +421 -0
  52. package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +5 -5
  53. package/docs/template-engine/esempi-incrementali.js +1 -1
  54. package/eslint.config.mjs +17 -0
  55. package/index.cjs +1507 -429
  56. package/index.mjs +1 -5
  57. package/package.json +9 -1
@@ -31,7 +31,7 @@ describe('hideExtension option tests', () => {
31
31
  beforeAll(() => {
32
32
  app = new Koa();
33
33
  app.use(koaClassicServer(rootDir, {
34
- showDirContents: true,
34
+ dirListing: { enabled: true },
35
35
  index: ['index.ejs'],
36
36
  hideExtension: { ext: '.ejs' },
37
37
  template: {
@@ -83,7 +83,7 @@ describe('hideExtension option tests', () => {
83
83
  beforeAll(() => {
84
84
  app = new Koa();
85
85
  app.use(koaClassicServer(rootDir, {
86
- showDirContents: true,
86
+ dirListing: { enabled: true },
87
87
  index: ['index.ejs'],
88
88
  hideExtension: { ext: '.ejs' },
89
89
  template: {
@@ -141,7 +141,7 @@ describe('hideExtension option tests', () => {
141
141
  beforeAll(() => {
142
142
  app = new Koa();
143
143
  app.use(koaClassicServer(rootDir, {
144
- showDirContents: true,
144
+ dirListing: { enabled: true },
145
145
  index: ['index.ejs'],
146
146
  hideExtension: { ext: '.ejs', redirect: 302 }
147
147
  }));
@@ -159,15 +159,15 @@ describe('hideExtension option tests', () => {
159
159
  });
160
160
 
161
161
  // ==========================================
162
- // Directory/file conflict (showDirContents: true)
162
+ // Directory/file conflict (dirListing: { enabled: true })
163
163
  // ==========================================
164
- describe('Directory/file conflict with showDirContents: true', () => {
164
+ describe('Directory/file conflict with dirListing: { enabled: true }', () => {
165
165
  let app, server, request;
166
166
 
167
167
  beforeAll(() => {
168
168
  app = new Koa();
169
169
  app.use(koaClassicServer(rootDir, {
170
- showDirContents: true,
170
+ dirListing: { enabled: true },
171
171
  index: ['index.html'],
172
172
  hideExtension: { ext: '.ejs' },
173
173
  template: {
@@ -208,7 +208,7 @@ describe('hideExtension option tests', () => {
208
208
  beforeAll(() => {
209
209
  app = new Koa();
210
210
  app.use(koaClassicServer(rootDir, {
211
- showDirContents: true,
211
+ dirListing: { enabled: true },
212
212
  hideExtension: { ext: '.ejs' }
213
213
  }));
214
214
  server = app.listen();
@@ -232,7 +232,7 @@ describe('hideExtension option tests', () => {
232
232
  beforeAll(() => {
233
233
  app = new Koa();
234
234
  app.use(koaClassicServer(rootDir, {
235
- showDirContents: true,
235
+ dirListing: { enabled: true },
236
236
  hideExtension: { ext: '.ejs' },
237
237
  template: {
238
238
  ext: ['ejs'],
@@ -275,7 +275,7 @@ describe('hideExtension option tests', () => {
275
275
  });
276
276
 
277
277
  const middleware = koaClassicServer(rootDir, {
278
- showDirContents: true,
278
+ dirListing: { enabled: true },
279
279
  index: ['index.ejs'],
280
280
  urlsReserved: ['/blog'],
281
281
  hideExtension: { ext: '.ejs' },
@@ -335,7 +335,7 @@ describe('hideExtension option tests', () => {
335
335
  });
336
336
 
337
337
  app.use(koaClassicServer(rootDir, {
338
- showDirContents: true,
338
+ dirListing: { enabled: true },
339
339
  useOriginalUrl: false,
340
340
  hideExtension: { ext: '.ejs' },
341
341
  template: {
@@ -377,7 +377,7 @@ describe('hideExtension option tests', () => {
377
377
  beforeAll(() => {
378
378
  app = new Koa();
379
379
  app.use(koaClassicServer(rootDir, {
380
- showDirContents: true,
380
+ dirListing: { enabled: true },
381
381
  hideExtension: { ext: '.ejs' }
382
382
  }));
383
383
  server = app.listen();
@@ -404,7 +404,7 @@ describe('hideExtension option tests', () => {
404
404
  beforeAll(() => {
405
405
  app = new Koa();
406
406
  app.use(koaClassicServer(rootDir, {
407
- showDirContents: true,
407
+ dirListing: { enabled: true },
408
408
  hideExtension: { ext: '.ejs' }
409
409
  }));
410
410
  server = app.listen();
@@ -442,7 +442,7 @@ describe('hideExtension option tests', () => {
442
442
  beforeAll(() => {
443
443
  app = new Koa();
444
444
  app.use(koaClassicServer(rootDir, {
445
- showDirContents: true,
445
+ dirListing: { enabled: true },
446
446
  index: ['index.ejs'],
447
447
  hideExtension: { ext: '.ejs' },
448
448
  template: {
@@ -547,4 +547,61 @@ describe('hideExtension option tests', () => {
547
547
  }).not.toThrow();
548
548
  });
549
549
  });
550
+
551
+ // ==========================================
552
+ // Security: open-redirect via protocol-relative URL
553
+ // ==========================================
554
+ describe('Open-redirect guard on hideExtension Location header', () => {
555
+ let app, server, port;
556
+
557
+ beforeAll((done) => {
558
+ app = new Koa();
559
+ app.use(koaClassicServer(rootDir, {
560
+ dirListing: { enabled: true },
561
+ hideExtension: { ext: '.ejs' }
562
+ }));
563
+ server = app.listen(0, () => {
564
+ port = server.address().port;
565
+ done();
566
+ });
567
+ });
568
+
569
+ afterAll(() => { server.close(); });
570
+
571
+ // Send the request path as raw bytes so the leading "//" survives any
572
+ // client-side URL normalization. supertest/url.parse would collapse it.
573
+ function rawGet(path) {
574
+ const http = require('http');
575
+ return new Promise((resolve, reject) => {
576
+ const req = http.request({
577
+ host: '127.0.0.1',
578
+ port,
579
+ method: 'GET',
580
+ path
581
+ }, (res) => {
582
+ res.resume();
583
+ resolve({ status: res.statusCode, location: res.headers.location });
584
+ });
585
+ req.on('error', reject);
586
+ req.end();
587
+ });
588
+ }
589
+
590
+ test('GET //evil.com/foo.ejs → Location must not be protocol-relative', async () => {
591
+ const res = await rawGet('//evil.com/foo.ejs');
592
+ expect(res.status).toBe(301);
593
+ expect(res.location).toBeDefined();
594
+ // The Location must not start with "//" or "/\" (would navigate off-origin)
595
+ expect(res.location.startsWith('//')).toBe(false);
596
+ expect(res.location.startsWith('/\\')).toBe(false);
597
+ expect(res.location).toBe('/evil.com/foo');
598
+ });
599
+
600
+ test('GET ///a/b.ejs → leading slashes collapsed in Location', async () => {
601
+ const res = await rawGet('///a/b.ejs');
602
+ expect(res.status).toBe(301);
603
+ expect(res.location.startsWith('//')).toBe(false);
604
+ expect(res.location).toBe('/a/b');
605
+ });
606
+ });
550
607
  });
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Enhanced Index Option Tests
3
3
  *
4
- * Tests for the new index option that supports:
5
- * - String (backward compatible)
4
+ * Tests for the index option that supports:
6
5
  * - Array of strings
7
6
  * - Array of RegExp
8
7
  * - Mixed array (strings + RegExp)
9
8
  * - Priority handling (first match wins)
9
+ *
10
+ * v3.0.0: string format removed — passing a non-empty string throws an Error.
10
11
  */
11
12
 
12
13
  const Koa = require('koa');
@@ -39,21 +40,20 @@ describe('Enhanced Index Option Tests', () => {
39
40
  }
40
41
  });
41
42
 
42
- describe('Backward Compatibility - String index', () => {
43
- test('String: "index.html" should work as before', async () => {
44
- // Create index.html
45
- fs.writeFileSync(path.join(tempDir, 'index.html'), '<h1>Index HTML</h1>');
46
-
47
- app = new Koa();
48
- app.use(koaClassicServer(tempDir, { index: 'index.html' }));
49
- server = app.listen();
43
+ describe('String index removed in v3.0.0', () => {
44
+ test('Non-empty string should throw an Error', () => {
45
+ expect(() => {
46
+ new Koa().use(koaClassicServer(tempDir, { index: 'index.html' }));
47
+ }).toThrow('"index" option no longer accepts a string in v3.0.0');
48
+ });
50
49
 
51
- const res = await supertest(server).get('/');
52
- expect(res.status).toBe(200);
53
- expect(res.text).toContain('Index HTML');
50
+ test('Throw message should include migration hint', () => {
51
+ expect(() => {
52
+ new Koa().use(koaClassicServer(tempDir, { index: 'default.htm' }));
53
+ }).toThrow('index: ["default.htm"]');
54
54
  });
55
55
 
56
- test('Empty string should show directory listing', async () => {
56
+ test('Empty string should show directory listing (no throw)', async () => {
57
57
  fs.writeFileSync(path.join(tempDir, 'test.txt'), 'test');
58
58
 
59
59
  app = new Koa();
@@ -281,7 +281,8 @@ describe('Enhanced Index Option Tests', () => {
281
281
 
282
282
  const res = await supertest(server).get('/');
283
283
  expect(res.status).toBe(200);
284
- expect(res.text).toContain('EJS content');
284
+ const body = res.text !== undefined ? res.text : res.body.toString('utf8');
285
+ expect(body).toContain('EJS content');
285
286
  });
286
287
  });
287
288
 
@@ -362,7 +363,8 @@ describe('Enhanced Index Option Tests', () => {
362
363
 
363
364
  const res = await supertest(server).get('/');
364
365
  expect(res.status).toBe(200);
365
- expect(res.text).toContain('pug content');
366
+ const body = res.text !== undefined ? res.text : res.body.toString('utf8');
367
+ expect(body).toContain('pug content');
366
368
  });
367
369
 
368
370
  test('Case-insensitive filesystem (Windows-like): matches any case', async () => {
@@ -33,7 +33,7 @@ const filesAndDirArray = getFilesRecursivelySync(rootDir);
33
33
  const options0 = {
34
34
  urlPrefix: '/public', // Il prefisso URL che il middleware dovrà intercettare
35
35
  method: ['GET'],// I metodi HTTP ammessi (default 'GET')
36
- showDirContents: true,// Se mostrare il contenuto della directory in caso di richiesta ad una cartella
36
+ dirListing: { enabled: true },// Se mostrare il contenuto della directory in caso di richiesta ad una cartella
37
37
  //index: 'index.html', // Nome del file index da cercare all'interno di una directory (se presente)
38
38
  };
39
39
 
@@ -79,7 +79,7 @@ describe(` koaClassicServer options0: ${JSON.stringify(options0)}`, () => {
79
79
  //START option1
80
80
  const options1 = {
81
81
  method: ['GET'],
82
- showDirContents: true,
82
+ dirListing: { enabled: true },
83
83
  };
84
84
 
85
85
  describe(` koaClassicServer options1: ${JSON.stringify(options1)}`, () => {
@@ -114,8 +114,8 @@ describe(` koaClassicServer options1: ${JSON.stringify(options1)}`, () => {
114
114
  //STASRT option2
115
115
  const options2 = {
116
116
  method: ['GET'],
117
- showDirContents: false,
118
- index: 'index.html',
117
+ dirListing: { enabled: false },
118
+ index: ['index.html'],
119
119
  };
120
120
 
121
121
  describe(` koaClassicServer options2: ${JSON.stringify(options2)}`, () => {
@@ -141,7 +141,7 @@ describe(` koaClassicServer options2: ${JSON.stringify(options2)}`, () => {
141
141
  //STASRT option3
142
142
  const options3 = {
143
143
  method: ['GET'],
144
- showDirContents: false,
144
+ dirListing: { enabled: false },
145
145
  urlsReserved : Array('/percorso_riservato', '/percorso riservato con spazi')
146
146
  };
147
147
 
@@ -188,7 +188,7 @@ describe(` koaClassicServer options2: ${JSON.stringify(options2)}`, () => {
188
188
  // I metodi HTTP ammessi (default 'GET')
189
189
  method: ['GET'],
190
190
  // Se mostrare il contenuto della directory in caso di richiesta ad una cartella
191
- showDirContents: true,
191
+ dirListing: { enabled: true },
192
192
  // Nome del file index da cercare all'interno di una directory (se presente)
193
193
  //index: 'index.html',
194
194
  }; */
@@ -213,7 +213,7 @@ function getFilesRecursivelySync(dir) {
213
213
  results = results.concat(getFilesRecursivelySync(fullPath));
214
214
  } else if (entry.isFile()) {
215
215
  // Se l'entry è un file, lo aggiungiamo all'array dei risultati
216
- const mimeType = mime.lookup(entry.name) || 'false';//false --> mimetype non riconosciuto , cosi lo trasmette il server , da approfondire
216
+ const mimeType = mime.lookup(entry.name) || 'application/octet-stream'; // fallback for unknown MIME types
217
217
  entry.type = 'file';
218
218
  entry.mimeType = mimeType;
219
219
  results.push(entry);
@@ -263,9 +263,12 @@ function testAllPathByFileList(filesAndDirArray, getServer, options) {
263
263
  expect(res.status).toBe(200);
264
264
  const content = fs.readFileSync(entry.fullPath, 'utf8');
265
265
  expect(res.type).toBe(entry.mimeType);
266
- expect(res.text).toBe(content);
266
+ // For binary content-types (e.g. application/octet-stream), supertest populates
267
+ // res.body (Buffer) instead of res.text — normalise to string for comparison
268
+ const responseBody = res.text !== undefined ? res.text : res.body.toString('utf8');
269
+ expect(responseBody).toBe(content);
267
270
  } else {//è una directory
268
- if( options.showDirContents === false ){
271
+ if( options.dirListing && options.dirListing.enabled === false ){
269
272
  // FIX: Quando directory listing è disabilitato, restituisce 404
270
273
  expect(res.status).toBe(404);
271
274
  expect(res.type).toBe('text/html');
@@ -328,7 +331,8 @@ function testAllPathByFileList(filesAndDirArray, getServer, options) {
328
331
  if (entry.type === 'file') {
329
332
  const content = fs.readFileSync(entry.fullPath, 'utf8');
330
333
  expect(res.type).toBe(entry.mimeType);
331
- expect(res.text).toBe(content);
334
+ const responseBody = res.text !== undefined ? res.text : res.body.toString('utf8');
335
+ expect(responseBody).toBe(content);
332
336
  } else {
333
337
  expect(res.type).toBe('text/html');
334
338
  expect(res.text).toContain('<!DOCTYPE html>');