koa-classic-server 3.0.1 → 3.1.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 (106) hide show
  1. package/README.md +44 -120
  2. package/index.cjs +200 -21
  3. package/package.json +10 -5
  4. package/.github/workflows/npm-publish.yml +0 -98
  5. package/CLAUDE.md +0 -101
  6. package/__tests__/benchmark-results-baseline-v1.2.0.txt +0 -354
  7. package/__tests__/benchmark-results-optimized-v2.0.0.txt +0 -354
  8. package/__tests__/benchmark-results-v3.0.0.txt +0 -372
  9. package/__tests__/benchmark.js +0 -239
  10. package/__tests__/caching-headers.test.js +0 -556
  11. package/__tests__/compression-fixtures/data.json +0 -1
  12. package/__tests__/compression-fixtures/large.txt +0 -1
  13. package/__tests__/compression-fixtures/small.txt +0 -1
  14. package/__tests__/compression.test.js +0 -284
  15. package/__tests__/customTest/README.md +0 -6
  16. package/__tests__/customTest/loadConfig.util.js +0 -41
  17. package/__tests__/customTest/serversToLoad.util.js +0 -93
  18. package/__tests__/demo-regex-index.js +0 -140
  19. package/__tests__/deprecation-warnings.test.js +0 -105
  20. package/__tests__/directory-sorting-links.test.js +0 -135
  21. package/__tests__/dt-unknown.test.js +0 -635
  22. package/__tests__/ejs.test.js +0 -299
  23. package/__tests__/head-method.test.js +0 -160
  24. package/__tests__/hidden-fixtures/.dot-dir/inside.txt +0 -1
  25. package/__tests__/hidden-fixtures/.env +0 -2
  26. package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +0 -1
  27. package/__tests__/hidden-fixtures/config/secrets/password.txt +0 -1
  28. package/__tests__/hidden-fixtures/data.key +0 -1
  29. package/__tests__/hidden-fixtures/file.secret +0 -1
  30. package/__tests__/hidden-fixtures/index.html +0 -1
  31. package/__tests__/hidden-fixtures/normal.txt +0 -1
  32. package/__tests__/hidden-fixtures/subdir/.env +0 -1
  33. package/__tests__/hidden-fixtures/subdir/regular.txt +0 -1
  34. package/__tests__/hidden-option.test.js +0 -407
  35. package/__tests__/hideExtension.test.js +0 -607
  36. package/__tests__/index-option.test.js +0 -449
  37. package/__tests__/index.test.js +0 -345
  38. package/__tests__/listing.test.js +0 -437
  39. package/__tests__/logger.test.js +0 -232
  40. package/__tests__/performance.test.js +0 -370
  41. package/__tests__/publicWwwTest/cartella/sottocartella/ciao.html +0 -11
  42. package/__tests__/publicWwwTest/cartella/sottocartella/provaEjs/testEjs.ejs +0 -11
  43. package/__tests__/publicWwwTest/cartella vuota con spazi nel nome/file con spazio nel nome .txt +0 -1
  44. package/__tests__/publicWwwTest/cartella vuota con spazi nel nome /file con spazio nel nome .txt +0 -1
  45. package/__tests__/publicWwwTest/ejs-templates/complex.ejs +0 -56
  46. package/__tests__/publicWwwTest/ejs-templates/index.ejs +0 -30
  47. package/__tests__/publicWwwTest/ejs-templates/simple.ejs +0 -13
  48. package/__tests__/publicWwwTest/ejs-templates/with-conditional.ejs +0 -28
  49. package/__tests__/publicWwwTest/ejs-templates/with-escaping.ejs +0 -26
  50. package/__tests__/publicWwwTest/ejs-templates/with-loop.ejs +0 -16
  51. package/__tests__/publicWwwTest/hideext-test/about/index.html +0 -1
  52. package/__tests__/publicWwwTest/hideext-test/about.EJS +0 -1
  53. package/__tests__/publicWwwTest/hideext-test/about.ejs +0 -2
  54. package/__tests__/publicWwwTest/hideext-test/blog/articolo.ejs +0 -2
  55. package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina +0 -1
  56. package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina.ejs +0 -1
  57. package/__tests__/publicWwwTest/hideext-test/file.ejs.bak +0 -1
  58. package/__tests__/publicWwwTest/hideext-test/index.ejs +0 -2
  59. package/__tests__/publicWwwTest/hideext-test/photo.txt +0 -1
  60. package/__tests__/publicWwwTest/hideext-test/sezione/index.ejs +0 -1
  61. package/__tests__/publicWwwTest/hideext-test/style.css +0 -1
  62. package/__tests__/publicWwwTest/ile_vuoto.txt +0 -0
  63. package/__tests__/publicWwwTest/index.html +0 -11
  64. package/__tests__/publicWwwTest/prova file .txt +0 -2
  65. package/__tests__/publicWwwTest/semplicetxt.txt +0 -1
  66. package/__tests__/publicWwwTest/test-page.html +0 -1
  67. package/__tests__/publicWwwTest/test.txt +0 -1
  68. package/__tests__/range-fixtures/sample.txt +0 -1
  69. package/__tests__/range.test.js +0 -223
  70. package/__tests__/security-headers.test.js +0 -165
  71. package/__tests__/security.test.js +0 -322
  72. package/__tests__/server-cache-fixtures/large.txt +0 -1
  73. package/__tests__/server-cache-fixtures/small.txt +0 -1
  74. package/__tests__/server-cache.test.js +0 -594
  75. package/__tests__/setup-benchmark.js +0 -178
  76. package/__tests__/symlink.test.js +0 -441
  77. package/__tests__/template-timeout.test.js +0 -321
  78. package/__tests__/test-regex-quick.js +0 -158
  79. package/__tests__/useOriginalUrl.test.js +0 -213
  80. package/docs/ACTION_PLAN.md +0 -293
  81. package/docs/BENCHMARKS.md +0 -317
  82. package/docs/CHANGELOG.md +0 -906
  83. package/docs/CODE_REVIEW.md +0 -300
  84. package/docs/DEBUG_REPORT.md +0 -593
  85. package/docs/DOCUMENTATION.md +0 -1864
  86. package/docs/EXAMPLES_INDEX_OPTION.md +0 -395
  87. package/docs/FLOW_DIAGRAM.md +0 -954
  88. package/docs/INDEX_OPTION_PRIORITY.md +0 -527
  89. package/docs/OPTIMIZATION_HTTP_CACHING.md +0 -689
  90. package/docs/OPTIMIZATION_ROADMAP_for_V3.md +0 -864
  91. package/docs/PERFORMANCE_ANALYSIS.md +0 -839
  92. package/docs/PERFORMANCE_COMPARISON.md +0 -388
  93. package/docs/noteExports.md +0 -148
  94. package/docs/security_improvement_for_V3.md +0 -421
  95. package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +0 -1734
  96. package/docs/template-engine/esempi-incrementali.js +0 -192
  97. package/docs/template-engine/examples/esempio1-nessun-dato.ejs +0 -12
  98. package/docs/template-engine/examples/esempio2-una-variabile.ejs +0 -11
  99. package/docs/template-engine/examples/esempio3-piu-variabili.ejs +0 -15
  100. package/docs/template-engine/examples/esempio4-condizionale.ejs +0 -18
  101. package/docs/template-engine/examples/esempio5-loop.ejs +0 -18
  102. package/docs/template-engine/examples/index-esempi.html +0 -181
  103. package/docs/template-engine/examples/index.html +0 -40
  104. package/docs/template-engine/examples/test.ejs +0 -64
  105. package/eslint.config.mjs +0 -17
  106. package/jest.config.js +0 -18
@@ -1,607 +0,0 @@
1
- //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2
- //
3
- // TEST FOR hideExtension OPTION
4
- // This test verifies that the hideExtension option works correctly:
5
- // - Clean URL resolution (URL without extension → file with extension)
6
- // - Redirect from URL with extension to clean URL
7
- // - Query string preservation
8
- // - Conflict resolution (directory vs file, extensionless vs extension)
9
- // - Input validation
10
- // - Interaction with existing options (urlsReserved, useOriginalUrl, template)
11
- //
12
- //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
13
-
14
- const supertest = require('supertest');
15
- const koaClassicServer = require('../index.cjs');
16
- const Koa = require('koa');
17
- const fs = require('fs');
18
- const path = require('path');
19
- const ejs = require('ejs');
20
-
21
- const rootDir = path.join(__dirname, 'publicWwwTest', 'hideext-test');
22
-
23
- describe('hideExtension option tests', () => {
24
-
25
- // ==========================================
26
- // Clean URL Resolution
27
- // ==========================================
28
- describe('Clean URL resolution', () => {
29
- let app, server, request;
30
-
31
- beforeAll(() => {
32
- app = new Koa();
33
- app.use(koaClassicServer(rootDir, {
34
- dirListing: { enabled: true },
35
- index: ['index.ejs'],
36
- hideExtension: { ext: '.ejs' },
37
- template: {
38
- ext: ['ejs'],
39
- render: async (ctx, next, filePath) => {
40
- const content = await fs.promises.readFile(filePath, 'utf-8');
41
- ctx.type = 'text/html';
42
- ctx.body = content;
43
- }
44
- }
45
- }));
46
- server = app.listen();
47
- request = supertest(server);
48
- });
49
-
50
- afterAll(() => { server.close(); });
51
-
52
- test('/about serves about.ejs', async () => {
53
- const response = await request.get('/about');
54
- expect(response.status).toBe(200);
55
- expect(response.text).toContain('About Page');
56
- });
57
-
58
- test('/blog/articolo serves blog/articolo.ejs (multi-level path)', async () => {
59
- const response = await request.get('/blog/articolo');
60
- expect(response.status).toBe(200);
61
- expect(response.text).toContain('Blog Article');
62
- });
63
-
64
- test('/style.css serves style.css (no interference with other extensions)', async () => {
65
- const response = await request.get('/style.css');
66
- expect(response.status).toBe(200);
67
- expect(response.text).toContain('body { color: red; }');
68
- });
69
-
70
- test('/ serves the index file via existing index flow', async () => {
71
- const response = await request.get('/');
72
- expect(response.status).toBe(200);
73
- expect(response.text).toContain('Home Page');
74
- });
75
- });
76
-
77
- // ==========================================
78
- // Redirect URL with extension → clean URL
79
- // ==========================================
80
- describe('Redirect URL with extension to clean URL', () => {
81
- let app, server, request;
82
-
83
- beforeAll(() => {
84
- app = new Koa();
85
- app.use(koaClassicServer(rootDir, {
86
- dirListing: { enabled: true },
87
- index: ['index.ejs'],
88
- hideExtension: { ext: '.ejs' },
89
- template: {
90
- ext: ['ejs'],
91
- render: async (ctx, next, filePath) => {
92
- const content = await fs.promises.readFile(filePath, 'utf-8');
93
- ctx.type = 'text/html';
94
- ctx.body = content;
95
- }
96
- }
97
- }));
98
- server = app.listen();
99
- request = supertest(server);
100
- });
101
-
102
- afterAll(() => { server.close(); });
103
-
104
- test('/about.ejs → redirect 301 to /about', async () => {
105
- const response = await request.get('/about.ejs');
106
- expect(response.status).toBe(301);
107
- expect(response.headers.location).toBe('/about');
108
- });
109
-
110
- test('/blog/articolo.ejs → redirect 301 to /blog/articolo', async () => {
111
- const response = await request.get('/blog/articolo.ejs');
112
- expect(response.status).toBe(301);
113
- expect(response.headers.location).toBe('/blog/articolo');
114
- });
115
-
116
- test('/about.ejs?lang=it → redirect 301 to /about?lang=it (preserves query string)', async () => {
117
- const response = await request.get('/about.ejs?lang=it');
118
- expect(response.status).toBe(301);
119
- expect(response.headers.location).toBe('/about?lang=it');
120
- });
121
-
122
- test('/index.ejs → redirect to /', async () => {
123
- const response = await request.get('/index.ejs');
124
- expect(response.status).toBe(301);
125
- expect(response.headers.location).toBe('/');
126
- });
127
-
128
- test('/sezione/index.ejs → redirect to /sezione/', async () => {
129
- const response = await request.get('/sezione/index.ejs');
130
- expect(response.status).toBe(301);
131
- expect(response.headers.location).toBe('/sezione/');
132
- });
133
- });
134
-
135
- // ==========================================
136
- // Custom redirect code (302)
137
- // ==========================================
138
- describe('Custom redirect code', () => {
139
- let app, server, request;
140
-
141
- beforeAll(() => {
142
- app = new Koa();
143
- app.use(koaClassicServer(rootDir, {
144
- dirListing: { enabled: true },
145
- index: ['index.ejs'],
146
- hideExtension: { ext: '.ejs', redirect: 302 }
147
- }));
148
- server = app.listen();
149
- request = supertest(server);
150
- });
151
-
152
- afterAll(() => { server.close(); });
153
-
154
- test('/about.ejs → redirect 302 to /about', async () => {
155
- const response = await request.get('/about.ejs');
156
- expect(response.status).toBe(302);
157
- expect(response.headers.location).toBe('/about');
158
- });
159
- });
160
-
161
- // ==========================================
162
- // Directory/file conflict (dirListing: { enabled: true })
163
- // ==========================================
164
- describe('Directory/file conflict with dirListing: { enabled: true }', () => {
165
- let app, server, request;
166
-
167
- beforeAll(() => {
168
- app = new Koa();
169
- app.use(koaClassicServer(rootDir, {
170
- dirListing: { enabled: true },
171
- index: ['index.html'],
172
- hideExtension: { ext: '.ejs' },
173
- template: {
174
- ext: ['ejs'],
175
- render: async (ctx, next, filePath) => {
176
- const content = await fs.promises.readFile(filePath, 'utf-8');
177
- ctx.type = 'text/html';
178
- ctx.body = content;
179
- }
180
- }
181
- }));
182
- server = app.listen();
183
- request = supertest(server);
184
- });
185
-
186
- afterAll(() => { server.close(); });
187
-
188
- test('/about serves about.ejs (file wins over directory)', async () => {
189
- const response = await request.get('/about');
190
- expect(response.status).toBe(200);
191
- expect(response.text).toContain('About Page');
192
- });
193
-
194
- test('/about/ shows the directory index or directory contents', async () => {
195
- const response = await request.get('/about/');
196
- expect(response.status).toBe(200);
197
- // The about/ directory has index.html, so it's served as the index file
198
- expect(response.text).toContain('About Directory Index');
199
- });
200
- });
201
-
202
- // ==========================================
203
- // Trailing slash without directory → 404
204
- // ==========================================
205
- describe('Trailing slash without directory', () => {
206
- let app, server, request;
207
-
208
- beforeAll(() => {
209
- app = new Koa();
210
- app.use(koaClassicServer(rootDir, {
211
- dirListing: { enabled: true },
212
- hideExtension: { ext: '.ejs' }
213
- }));
214
- server = app.listen();
215
- request = supertest(server);
216
- });
217
-
218
- afterAll(() => { server.close(); });
219
-
220
- test('/nonexistent/ returns 404', async () => {
221
- const response = await request.get('/nonexistent/');
222
- expect(response.status).toBe(404);
223
- });
224
- });
225
-
226
- // ==========================================
227
- // File without extension vs .ejs conflict
228
- // ==========================================
229
- describe('File without extension vs .ejs conflict', () => {
230
- let app, server, request;
231
-
232
- beforeAll(() => {
233
- app = new Koa();
234
- app.use(koaClassicServer(rootDir, {
235
- dirListing: { enabled: true },
236
- hideExtension: { ext: '.ejs' },
237
- template: {
238
- ext: ['ejs'],
239
- render: async (ctx, next, filePath) => {
240
- const content = await fs.promises.readFile(filePath, 'utf-8');
241
- ctx.type = 'text/html';
242
- ctx.body = content;
243
- }
244
- }
245
- }));
246
- server = app.listen();
247
- request = supertest(server);
248
- });
249
-
250
- afterAll(() => { server.close(); });
251
-
252
- test('/conflict-test/pagina serves pagina.ejs (ejs wins over extensionless file)', async () => {
253
- const response = await request.get('/conflict-test/pagina');
254
- expect(response.status).toBe(200);
255
- expect(response.text).toContain('Pagina EJS');
256
- });
257
- });
258
-
259
- // ==========================================
260
- // Interaction with urlsReserved
261
- // ==========================================
262
- describe('Interaction with urlsReserved', () => {
263
- let app, server, request;
264
-
265
- beforeAll(() => {
266
- app = new Koa();
267
-
268
- // Add a "next" middleware to catch reserved URLs
269
- app.use(async (ctx, next) => {
270
- await next();
271
- if (ctx.status === 404 && ctx._passedToNext) {
272
- ctx.status = 200;
273
- ctx.body = 'RESERVED';
274
- }
275
- });
276
-
277
- const middleware = koaClassicServer(rootDir, {
278
- dirListing: { enabled: true },
279
- index: ['index.ejs'],
280
- urlsReserved: ['/blog'],
281
- hideExtension: { ext: '.ejs' },
282
- template: {
283
- ext: ['ejs'],
284
- render: async (ctx, next, filePath) => {
285
- const content = await fs.promises.readFile(filePath, 'utf-8');
286
- ctx.type = 'text/html';
287
- ctx.body = content;
288
- }
289
- }
290
- });
291
-
292
- // Wrap middleware to track next() calls
293
- app.use(async (ctx, next) => {
294
- const originalNext = next;
295
- await middleware(ctx, async () => {
296
- ctx._passedToNext = true;
297
- await originalNext();
298
- });
299
- });
300
-
301
- server = app.listen();
302
- request = supertest(server);
303
- });
304
-
305
- afterAll(() => { server.close(); });
306
-
307
- test('/blog is reserved (passed to next middleware)', async () => {
308
- const response = await request.get('/blog');
309
- // blog is a directory and is reserved, so it passes to next
310
- expect(response.text).toBe('RESERVED');
311
- });
312
-
313
- test('/about still resolves about.ejs normally', async () => {
314
- const response = await request.get('/about');
315
- expect(response.status).toBe(200);
316
- expect(response.text).toContain('About Page');
317
- });
318
- });
319
-
320
- // ==========================================
321
- // Interaction with useOriginalUrl
322
- // ==========================================
323
- describe('Interaction with useOriginalUrl', () => {
324
- let app, server, request;
325
-
326
- beforeAll(() => {
327
- app = new Koa();
328
-
329
- // i18n middleware that rewrites URLs
330
- app.use(async (ctx, next) => {
331
- if (ctx.path.match(/^\/it\//)) {
332
- ctx.url = ctx.path.replace(/^\/it/, '');
333
- }
334
- await next();
335
- });
336
-
337
- app.use(koaClassicServer(rootDir, {
338
- dirListing: { enabled: true },
339
- useOriginalUrl: false,
340
- hideExtension: { ext: '.ejs' },
341
- template: {
342
- ext: ['ejs'],
343
- render: async (ctx, next, filePath) => {
344
- const content = await fs.promises.readFile(filePath, 'utf-8');
345
- ctx.type = 'text/html';
346
- ctx.body = content;
347
- }
348
- }
349
- }));
350
-
351
- server = app.listen();
352
- request = supertest(server);
353
- });
354
-
355
- afterAll(() => { server.close(); });
356
-
357
- test('redirect uses ctx.originalUrl (preserves /it/ prefix)', async () => {
358
- const response = await request.get('/it/about.ejs');
359
- expect(response.status).toBe(301);
360
- // Redirect should use originalUrl: /it/about (not /about)
361
- expect(response.headers.location).toBe('/it/about');
362
- });
363
-
364
- test('clean URL resolves through rewritten URL', async () => {
365
- const response = await request.get('/it/about');
366
- expect(response.status).toBe(200);
367
- expect(response.text).toContain('About Page');
368
- });
369
- });
370
-
371
- // ==========================================
372
- // Case-sensitive extension matching
373
- // ==========================================
374
- describe('Case-sensitive extension matching', () => {
375
- let app, server, request;
376
-
377
- beforeAll(() => {
378
- app = new Koa();
379
- app.use(koaClassicServer(rootDir, {
380
- dirListing: { enabled: true },
381
- hideExtension: { ext: '.ejs' }
382
- }));
383
- server = app.listen();
384
- request = supertest(server);
385
- });
386
-
387
- afterAll(() => { server.close(); });
388
-
389
- test('/about.EJS is not handled by hideExtension (case-sensitive)', async () => {
390
- const response = await request.get('/about.EJS');
391
- // The file about.EJS exists, should be served normally (not redirected)
392
- expect(response.status).toBe(200);
393
- // Should NOT be a redirect
394
- expect(response.status).not.toBe(301);
395
- });
396
- });
397
-
398
- // ==========================================
399
- // URLs with different extensions (no interference)
400
- // ==========================================
401
- describe('URLs with different extensions', () => {
402
- let app, server, request;
403
-
404
- beforeAll(() => {
405
- app = new Koa();
406
- app.use(koaClassicServer(rootDir, {
407
- dirListing: { enabled: true },
408
- hideExtension: { ext: '.ejs' }
409
- }));
410
- server = app.listen();
411
- request = supertest(server);
412
- });
413
-
414
- afterAll(() => { server.close(); });
415
-
416
- test('/file.ejs.bak is not interfered with', async () => {
417
- const response = await request.get('/file.ejs.bak');
418
- // .bak is the extension, not .ejs - should be served normally
419
- expect(response.status).toBe(200);
420
- expect(response.status).not.toBe(301);
421
- });
422
-
423
- test('/photo.txt is not interfered with', async () => {
424
- const response = await request.get('/photo.txt');
425
- expect(response.status).toBe(200);
426
- expect(response.status).not.toBe(301);
427
- });
428
-
429
- test('/style.css is not interfered with', async () => {
430
- const response = await request.get('/style.css');
431
- expect(response.status).toBe(200);
432
- expect(response.status).not.toBe(301);
433
- });
434
- });
435
-
436
- // ==========================================
437
- // Template engine integration
438
- // ==========================================
439
- describe('Template engine integration', () => {
440
- let app, server, request;
441
-
442
- beforeAll(() => {
443
- app = new Koa();
444
- app.use(koaClassicServer(rootDir, {
445
- dirListing: { enabled: true },
446
- index: ['index.ejs'],
447
- hideExtension: { ext: '.ejs' },
448
- template: {
449
- ext: ['ejs'],
450
- render: async (ctx, next, filePath) => {
451
- const templateContent = await fs.promises.readFile(filePath, 'utf-8');
452
- const html = ejs.render(templateContent, { title: 'Test Title' });
453
- ctx.type = 'text/html';
454
- ctx.body = html;
455
- }
456
- }
457
- }));
458
- server = app.listen();
459
- request = supertest(server);
460
- });
461
-
462
- afterAll(() => { server.close(); });
463
-
464
- test('file resolved via hideExtension passes correctly to template engine', async () => {
465
- const response = await request.get('/about');
466
- expect(response.status).toBe(200);
467
- expect(response.type).toBe('text/html');
468
- expect(response.text).toContain('About Page');
469
- });
470
- });
471
-
472
- // ==========================================
473
- // Input Validation
474
- // ==========================================
475
- describe('Input validation', () => {
476
-
477
- test('hideExtension: true → throws Error', () => {
478
- expect(() => {
479
- koaClassicServer(rootDir, {
480
- hideExtension: true
481
- });
482
- }).toThrow();
483
- });
484
-
485
- test('hideExtension: {} → throws Error (missing ext)', () => {
486
- expect(() => {
487
- koaClassicServer(rootDir, {
488
- hideExtension: {}
489
- });
490
- }).toThrow();
491
- });
492
-
493
- test('hideExtension: { ext: "" } → throws Error (empty ext)', () => {
494
- expect(() => {
495
- koaClassicServer(rootDir, {
496
- hideExtension: { ext: '' }
497
- });
498
- }).toThrow();
499
- });
500
-
501
- test('hideExtension: { ext: "ejs" } → warning + normalizes to ".ejs"', () => {
502
- const originalWarn = console.warn;
503
- const warnings = [];
504
- console.warn = (...args) => { warnings.push(args); };
505
-
506
- try {
507
- const middleware = koaClassicServer(rootDir, {
508
- hideExtension: { ext: 'ejs' }
509
- });
510
- // Should not throw
511
- expect(middleware).toBeDefined();
512
- // Should have issued a warning
513
- expect(warnings.length).toBeGreaterThan(0);
514
- expect(warnings[0][1]).toContain('hideExtension.ext should start with a dot');
515
- } finally {
516
- console.warn = originalWarn;
517
- }
518
- });
519
-
520
- test('hideExtension: { ext: ".ejs", redirect: "abc" } → throws Error (redirect not numeric)', () => {
521
- expect(() => {
522
- koaClassicServer(rootDir, {
523
- hideExtension: { ext: '.ejs', redirect: 'abc' }
524
- });
525
- }).toThrow();
526
- });
527
-
528
- test('hideExtension: { ext: ".ejs" } → valid, default redirect 301', () => {
529
- expect(() => {
530
- koaClassicServer(rootDir, {
531
- hideExtension: { ext: '.ejs' }
532
- });
533
- }).not.toThrow();
534
- });
535
-
536
- test('hideExtension: { ext: ".ejs", redirect: 302 } → valid', () => {
537
- expect(() => {
538
- koaClassicServer(rootDir, {
539
- hideExtension: { ext: '.ejs', redirect: 302 }
540
- });
541
- }).not.toThrow();
542
- });
543
-
544
- test('hideExtension: undefined → feature disabled (no error)', () => {
545
- expect(() => {
546
- koaClassicServer(rootDir, {});
547
- }).not.toThrow();
548
- });
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
- });
607
- });