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.
- package/CLAUDE.md +101 -0
- package/README.md +564 -591
- package/__tests__/benchmark-results-v3.0.0.txt +372 -0
- package/__tests__/benchmark.js +1 -1
- package/__tests__/caching-headers.test.js +30 -30
- package/__tests__/compression-fixtures/data.json +1 -0
- package/__tests__/compression-fixtures/large.txt +1 -0
- package/__tests__/compression-fixtures/small.txt +1 -0
- package/__tests__/compression.test.js +284 -0
- package/__tests__/customTest/serversToLoad.util.js +5 -5
- package/__tests__/demo-regex-index.js +4 -4
- package/__tests__/deprecation-warnings.test.js +71 -183
- package/__tests__/directory-sorting-links.test.js +1 -1
- package/__tests__/dt-unknown.test.js +39 -28
- package/__tests__/ejs.test.js +1 -1
- package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
- package/__tests__/hidden-fixtures/.env +2 -0
- package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
- package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
- package/__tests__/hidden-fixtures/data.key +1 -0
- package/__tests__/hidden-fixtures/file.secret +1 -0
- package/__tests__/hidden-fixtures/index.html +1 -0
- package/__tests__/hidden-fixtures/normal.txt +1 -0
- package/__tests__/hidden-fixtures/subdir/.env +1 -0
- package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
- package/__tests__/hidden-option.test.js +407 -0
- package/__tests__/hideExtension.test.js +70 -13
- package/__tests__/index-option.test.js +18 -16
- package/__tests__/index.test.js +14 -10
- package/__tests__/listing.test.js +437 -0
- package/__tests__/logger.test.js +232 -0
- package/__tests__/range-fixtures/sample.txt +1 -0
- package/__tests__/range.test.js +223 -0
- package/__tests__/security-headers.test.js +165 -0
- package/__tests__/security.test.js +148 -162
- package/__tests__/server-cache-fixtures/large.txt +1 -0
- package/__tests__/server-cache-fixtures/small.txt +1 -0
- package/__tests__/server-cache.test.js +594 -0
- package/__tests__/symlink.test.js +18 -15
- package/__tests__/template-timeout.test.js +321 -0
- package/docs/ACTION_PLAN.md +293 -0
- package/docs/CHANGELOG.md +289 -0
- package/docs/CODE_REVIEW.md +2 -0
- package/docs/DOCUMENTATION.md +259 -32
- package/docs/EXAMPLES_INDEX_OPTION.md +3 -3
- package/docs/FLOW_DIAGRAM.md +15 -13
- package/docs/INDEX_OPTION_PRIORITY.md +2 -2
- package/docs/OPTIMIZATION_HTTP_CACHING.md +2 -0
- package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
- package/docs/PERFORMANCE_COMPARISON.md +7 -7
- package/docs/security_improvement_for_V3.md +421 -0
- package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +5 -5
- package/docs/template-engine/esempi-incrementali.js +1 -1
- package/eslint.config.mjs +17 -0
- package/index.cjs +1507 -429
- package/index.mjs +1 -5
- package/package.json +9 -1
|
@@ -97,7 +97,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
97
97
|
const app = new Koa();
|
|
98
98
|
app.use(koaClassicServer(tmpDir, {
|
|
99
99
|
index: ['index.html'],
|
|
100
|
-
|
|
100
|
+
dirListing: { enabled: true }
|
|
101
101
|
}));
|
|
102
102
|
const server = app.listen();
|
|
103
103
|
const request = supertest(server);
|
|
@@ -120,7 +120,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
120
120
|
const app = new Koa();
|
|
121
121
|
app.use(koaClassicServer(tmpDir, {
|
|
122
122
|
index: ['subdir'],
|
|
123
|
-
|
|
123
|
+
dirListing: { enabled: true }
|
|
124
124
|
}));
|
|
125
125
|
const server = app.listen();
|
|
126
126
|
const request = supertest(server);
|
|
@@ -143,7 +143,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
143
143
|
const app = new Koa();
|
|
144
144
|
app.use(koaClassicServer(tmpDir, {
|
|
145
145
|
index: ['nonexistent.html'],
|
|
146
|
-
|
|
146
|
+
dirListing: { enabled: true }
|
|
147
147
|
}));
|
|
148
148
|
const server = app.listen();
|
|
149
149
|
const request = supertest(server);
|
|
@@ -172,7 +172,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
172
172
|
const app = new Koa();
|
|
173
173
|
app.use(koaClassicServer(tmpDir, {
|
|
174
174
|
index: ['index.html'],
|
|
175
|
-
|
|
175
|
+
dirListing: { enabled: true }
|
|
176
176
|
}));
|
|
177
177
|
const server = app.listen();
|
|
178
178
|
const request = supertest(server);
|
|
@@ -190,14 +190,18 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
test('should return false for DT_UNKNOWN entry pointing to a regular file', async () => {
|
|
193
|
-
// Files should NOT be treated as directories
|
|
194
|
-
//
|
|
193
|
+
// Files should NOT be treated as directories.
|
|
194
|
+
// Use a RegExp index pattern to force the readdir-based slow-path in
|
|
195
|
+
// findIndexFile (the string fast-path uses stat() directly and would bypass
|
|
196
|
+
// the mock). The regex never matches, so no index file is found and the
|
|
197
|
+
// directory listing is shown, confirming the DT_UNKNOWN files are treated
|
|
198
|
+
// as regular files (not directories).
|
|
195
199
|
const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt']);
|
|
196
200
|
|
|
197
201
|
const app = new Koa();
|
|
198
202
|
app.use(koaClassicServer(tmpDir, {
|
|
199
|
-
index: [
|
|
200
|
-
|
|
203
|
+
index: [/NOMATCH_SENTINEL/],
|
|
204
|
+
dirListing: { enabled: true }
|
|
201
205
|
}));
|
|
202
206
|
const server = app.listen();
|
|
203
207
|
const request = supertest(server);
|
|
@@ -224,7 +228,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
224
228
|
const app = new Koa();
|
|
225
229
|
app.use(koaClassicServer(tmpDir, {
|
|
226
230
|
index: ['index.html'],
|
|
227
|
-
|
|
231
|
+
dirListing: { enabled: true }
|
|
228
232
|
}));
|
|
229
233
|
const server = app.listen();
|
|
230
234
|
const request = supertest(server);
|
|
@@ -246,7 +250,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
246
250
|
const app = new Koa();
|
|
247
251
|
app.use(koaClassicServer(tmpDir, {
|
|
248
252
|
index: ['index.ejs'],
|
|
249
|
-
|
|
253
|
+
dirListing: { enabled: true }
|
|
250
254
|
}));
|
|
251
255
|
const server = app.listen();
|
|
252
256
|
const request = supertest(server);
|
|
@@ -254,8 +258,9 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
254
258
|
try {
|
|
255
259
|
const res = await request.get('/');
|
|
256
260
|
expect(res.status).toBe(200);
|
|
257
|
-
|
|
258
|
-
expect(
|
|
261
|
+
const body = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
262
|
+
expect(body).toContain('EJS DT_UNKNOWN');
|
|
263
|
+
expect(body).not.toContain('Index of');
|
|
259
264
|
} finally {
|
|
260
265
|
server.close();
|
|
261
266
|
spy.mockRestore();
|
|
@@ -268,7 +273,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
268
273
|
const app = new Koa();
|
|
269
274
|
app.use(koaClassicServer(tmpDir, {
|
|
270
275
|
index: [/index\.[eE][jJ][sS]/],
|
|
271
|
-
|
|
276
|
+
dirListing: { enabled: true }
|
|
272
277
|
}));
|
|
273
278
|
const server = app.listen();
|
|
274
279
|
const request = supertest(server);
|
|
@@ -276,8 +281,9 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
276
281
|
try {
|
|
277
282
|
const res = await request.get('/');
|
|
278
283
|
expect(res.status).toBe(200);
|
|
279
|
-
|
|
280
|
-
expect(
|
|
284
|
+
const body = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
285
|
+
expect(body).toContain('EJS DT_UNKNOWN');
|
|
286
|
+
expect(body).not.toContain('Index of');
|
|
281
287
|
} finally {
|
|
282
288
|
server.close();
|
|
283
289
|
spy.mockRestore();
|
|
@@ -285,12 +291,17 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
285
291
|
});
|
|
286
292
|
|
|
287
293
|
test('should not find index in directory with only subdirectories (all DT_UNKNOWN)', async () => {
|
|
294
|
+
// Use a RegExp to force the readdir-based slow-path (the string fast-path
|
|
295
|
+
// would stat() index.html directly, finding the real file on disk).
|
|
296
|
+
// The regex never matches, so the mocked readdir result (['subdir']) is used:
|
|
297
|
+
// 'subdir' has DT_UNKNOWN → stat fallback → isDirectory() → not a file →
|
|
298
|
+
// fileNames stays empty → no index found → directory listing shown.
|
|
288
299
|
const spy = mockReaddirWithDtUnknown(tmpDir, ['subdir']);
|
|
289
300
|
|
|
290
301
|
const app = new Koa();
|
|
291
302
|
app.use(koaClassicServer(tmpDir, {
|
|
292
|
-
index: [
|
|
293
|
-
|
|
303
|
+
index: [/NOMATCH_SENTINEL/],
|
|
304
|
+
dirListing: { enabled: true }
|
|
294
305
|
}));
|
|
295
306
|
const server = app.listen();
|
|
296
307
|
const request = supertest(server);
|
|
@@ -298,7 +309,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
298
309
|
try {
|
|
299
310
|
const res = await request.get('/');
|
|
300
311
|
expect(res.status).toBe(200);
|
|
301
|
-
// No file matches
|
|
312
|
+
// No file matches the pattern, so directory listing should appear
|
|
302
313
|
expect(res.text).toContain('Index of');
|
|
303
314
|
} finally {
|
|
304
315
|
server.close();
|
|
@@ -317,7 +328,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
317
328
|
const app = new Koa();
|
|
318
329
|
app.use(koaClassicServer(tmpDir, {
|
|
319
330
|
index: [],
|
|
320
|
-
|
|
331
|
+
dirListing: { enabled: true }
|
|
321
332
|
}));
|
|
322
333
|
const server = app.listen();
|
|
323
334
|
const request = supertest(server);
|
|
@@ -345,7 +356,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
345
356
|
const app = new Koa();
|
|
346
357
|
app.use(koaClassicServer(tmpDir, {
|
|
347
358
|
index: [],
|
|
348
|
-
|
|
359
|
+
dirListing: { enabled: true }
|
|
349
360
|
}));
|
|
350
361
|
const server = app.listen();
|
|
351
362
|
const request = supertest(server);
|
|
@@ -375,7 +386,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
375
386
|
const app = new Koa();
|
|
376
387
|
app.use(koaClassicServer(tmpDir, {
|
|
377
388
|
index: [],
|
|
378
|
-
|
|
389
|
+
dirListing: { enabled: true }
|
|
379
390
|
}));
|
|
380
391
|
const server = app.listen();
|
|
381
392
|
const request = supertest(server);
|
|
@@ -399,7 +410,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
399
410
|
const app = new Koa();
|
|
400
411
|
app.use(koaClassicServer(tmpDir, {
|
|
401
412
|
index: [],
|
|
402
|
-
|
|
413
|
+
dirListing: { enabled: true }
|
|
403
414
|
}));
|
|
404
415
|
const server = app.listen();
|
|
405
416
|
const request = supertest(server);
|
|
@@ -429,7 +440,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
429
440
|
const app = new Koa();
|
|
430
441
|
app.use(koaClassicServer(tmpDir, {
|
|
431
442
|
index: ['index.html'],
|
|
432
|
-
|
|
443
|
+
dirListing: { enabled: true }
|
|
433
444
|
}));
|
|
434
445
|
const server = app.listen();
|
|
435
446
|
const request = supertest(server);
|
|
@@ -454,7 +465,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
454
465
|
const app = new Koa();
|
|
455
466
|
app.use(koaClassicServer(tmpDir, {
|
|
456
467
|
index: [],
|
|
457
|
-
|
|
468
|
+
dirListing: { enabled: true }
|
|
458
469
|
}));
|
|
459
470
|
const server = app.listen();
|
|
460
471
|
const request = supertest(server);
|
|
@@ -475,7 +486,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
475
486
|
const app = new Koa();
|
|
476
487
|
app.use(koaClassicServer(tmpDir, {
|
|
477
488
|
index: [],
|
|
478
|
-
|
|
489
|
+
dirListing: { enabled: true }
|
|
479
490
|
}));
|
|
480
491
|
const server = app.listen();
|
|
481
492
|
const request = supertest(server);
|
|
@@ -511,7 +522,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
511
522
|
const app = new Koa();
|
|
512
523
|
app.use(koaClassicServer(tmpDir, {
|
|
513
524
|
index: ['index.ejs'],
|
|
514
|
-
|
|
525
|
+
dirListing: { enabled: true },
|
|
515
526
|
template: {
|
|
516
527
|
ext: ['ejs'],
|
|
517
528
|
render: async (ctx, next, filePath) => {
|
|
@@ -562,7 +573,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
562
573
|
const app = new Koa();
|
|
563
574
|
app.use(koaClassicServer(tmpDir, {
|
|
564
575
|
index: [],
|
|
565
|
-
|
|
576
|
+
dirListing: { enabled: true }
|
|
566
577
|
}));
|
|
567
578
|
const server = app.listen();
|
|
568
579
|
const request = supertest(server);
|
|
@@ -604,7 +615,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
|
|
|
604
615
|
const app = new Koa();
|
|
605
616
|
app.use(koaClassicServer(tmpDir, {
|
|
606
617
|
index: ['index.html', 'index.ejs'],
|
|
607
|
-
|
|
618
|
+
dirListing: { enabled: true }
|
|
608
619
|
}));
|
|
609
620
|
const server = app.listen();
|
|
610
621
|
const request = supertest(server);
|
package/__tests__/ejs.test.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dot dir file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
acme
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
password
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
key data
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
secret data
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
regular
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
normal
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
NESTED_SECRET=nestedvalue
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
regular in subdir
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
const supertest = require('supertest');
|
|
2
|
+
const koaClassicServer = require('../index.cjs');
|
|
3
|
+
const Koa = require('koa');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const root = path.join(__dirname, 'hidden-fixtures');
|
|
7
|
+
|
|
8
|
+
function createApp(hiddenOpts) {
|
|
9
|
+
const app = new Koa();
|
|
10
|
+
app.use(koaClassicServer(root, { dirListing: { enabled: true }, hidden: hiddenOpts }));
|
|
11
|
+
return app.listen();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Note: prior to v3.0.0 the dotFiles default was 'hidden' (a v3-alpha
|
|
15
|
+
// security-by-default choice) and a once-per-process warning was emitted
|
|
16
|
+
// when the option was implicit. The default was reverted to 'visible' to
|
|
17
|
+
// align with the "HTTP file server first" design philosophy (see
|
|
18
|
+
// CLAUDE.md). The warning is no longer emitted because the default is no
|
|
19
|
+
// longer a surprising restriction; tests for it have been removed.
|
|
20
|
+
|
|
21
|
+
// ─── Option validation ────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
describe('hidden option — validation', () => {
|
|
24
|
+
test('throws when dotFiles.default is not "hidden" or "visible"', () => {
|
|
25
|
+
expect(() =>
|
|
26
|
+
koaClassicServer(root, { hidden: { dotFiles: { default: 'yes' } } })
|
|
27
|
+
).toThrow(/hidden\.dotFiles\.default must be "hidden" or "visible"/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('throws when dotDirs.default is not "hidden" or "visible"', () => {
|
|
31
|
+
expect(() =>
|
|
32
|
+
koaClassicServer(root, { hidden: { dotDirs: { default: 'yes' } } })
|
|
33
|
+
).toThrow(/hidden\.dotDirs\.default must be "hidden" or "visible"/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('does not throw when hidden option is omitted', () => {
|
|
37
|
+
expect(() => koaClassicServer(root, {})).not.toThrow();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('does not throw when hidden is a valid object', () => {
|
|
41
|
+
expect(() =>
|
|
42
|
+
koaClassicServer(root, {
|
|
43
|
+
hidden: {
|
|
44
|
+
dotFiles: { default: 'hidden', whitelist: ['.well-known'], blacklist: ['.env'] },
|
|
45
|
+
dotDirs: { default: 'visible', blacklist: [/^\.git/] },
|
|
46
|
+
alwaysHide: ['*.secret', /\.key$/]
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
).not.toThrow();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─── dotFiles — default visible (system default in v3.0+) ────────────────────
|
|
54
|
+
|
|
55
|
+
describe('dotFiles — default visible (system default)', () => {
|
|
56
|
+
let server;
|
|
57
|
+
beforeAll(() => { server = createApp(undefined); });
|
|
58
|
+
afterAll(() => server.close());
|
|
59
|
+
|
|
60
|
+
// Per the V3 "HTTP file server first" philosophy (see CLAUDE.md), dot-files
|
|
61
|
+
// are visible by default. Operators harden by setting hidden.dotFiles.default
|
|
62
|
+
// to 'hidden' or by using blacklist / alwaysHide.
|
|
63
|
+
test('GET /.env returns 200 by default', async () => {
|
|
64
|
+
const res = await supertest(server).get('/.env');
|
|
65
|
+
expect(res.status).toBe(200);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('GET /.gitignore returns 200 by default', async () => {
|
|
69
|
+
const res = await supertest(server).get('/.gitignore');
|
|
70
|
+
expect(res.status).toBe(200);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('directory listing includes dot-files', async () => {
|
|
74
|
+
const res = await supertest(server).get('/');
|
|
75
|
+
expect(res.status).toBe(200);
|
|
76
|
+
expect(res.text).toContain('.env');
|
|
77
|
+
expect(res.text).toContain('.gitignore');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('regular files remain accessible', async () => {
|
|
81
|
+
const res = await supertest(server).get('/normal.txt');
|
|
82
|
+
expect(res.status).toBe(200);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ─── dotFiles default: 'hidden' (opt-in hardening) ───────────────────────────
|
|
87
|
+
|
|
88
|
+
describe('dotFiles — explicit default hidden (opt-in)', () => {
|
|
89
|
+
let server;
|
|
90
|
+
beforeAll(() => {
|
|
91
|
+
server = createApp({ dotFiles: { default: 'hidden' } });
|
|
92
|
+
});
|
|
93
|
+
afterAll(() => server.close());
|
|
94
|
+
|
|
95
|
+
test('GET /.env returns 404 when dotFiles.default = "hidden"', async () => {
|
|
96
|
+
const res = await supertest(server).get('/.env');
|
|
97
|
+
expect(res.status).toBe(404);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('GET /.gitignore returns 404', async () => {
|
|
101
|
+
const res = await supertest(server).get('/.gitignore');
|
|
102
|
+
expect(res.status).toBe(404);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('GET /subdir/.env returns 404 (dot-file in subdirectory)', async () => {
|
|
106
|
+
const res = await supertest(server).get('/subdir/.env');
|
|
107
|
+
expect(res.status).toBe(404);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('directory listing does not include .env', async () => {
|
|
111
|
+
const res = await supertest(server).get('/');
|
|
112
|
+
expect(res.status).toBe(200);
|
|
113
|
+
expect(res.text).not.toContain('.env');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('directory listing does not include .gitignore', async () => {
|
|
117
|
+
const res = await supertest(server).get('/');
|
|
118
|
+
expect(res.text).not.toContain('.gitignore');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('regular files remain accessible', async () => {
|
|
122
|
+
const res = await supertest(server).get('/normal.txt');
|
|
123
|
+
expect(res.status).toBe(200);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── dotFiles default: 'visible' (kept for completeness with explicit opt-in) ─
|
|
128
|
+
|
|
129
|
+
describe('dotFiles — default visible', () => {
|
|
130
|
+
let server;
|
|
131
|
+
beforeAll(() => {
|
|
132
|
+
server = createApp({ dotFiles: { default: 'visible' } });
|
|
133
|
+
});
|
|
134
|
+
afterAll(() => server.close());
|
|
135
|
+
|
|
136
|
+
test('GET /.env returns 200 when dotFiles.default is "visible"', async () => {
|
|
137
|
+
const res = await supertest(server).get('/.env');
|
|
138
|
+
expect(res.status).toBe(200);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('directory listing includes .env when dotFiles.default is "visible"', async () => {
|
|
142
|
+
const res = await supertest(server).get('/');
|
|
143
|
+
expect(res.text).toContain('.env');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ─── dotFiles whitelist ───────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
describe('dotFiles — whitelist exceptions', () => {
|
|
150
|
+
let server;
|
|
151
|
+
beforeAll(() => {
|
|
152
|
+
server = createApp({
|
|
153
|
+
dotFiles: { default: 'hidden', whitelist: ['.well-known'] },
|
|
154
|
+
dotDirs: { default: 'hidden', whitelist: ['.well-known'] }
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
afterAll(() => server.close());
|
|
158
|
+
|
|
159
|
+
test('GET /.well-known/ is accessible (whitelisted dir)', async () => {
|
|
160
|
+
const res = await supertest(server).get('/.well-known/');
|
|
161
|
+
expect(res.status).toBe(200);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('GET /.well-known/acme-challenge.txt is accessible (inside whitelisted dir)', async () => {
|
|
165
|
+
const res = await supertest(server).get('/.well-known/acme-challenge.txt');
|
|
166
|
+
expect(res.status).toBe(200);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('.well-known appears in root listing', async () => {
|
|
170
|
+
const res = await supertest(server).get('/');
|
|
171
|
+
expect(res.text).toContain('.well-known');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('GET /.env still returns 404 (not whitelisted)', async () => {
|
|
175
|
+
const res = await supertest(server).get('/.env');
|
|
176
|
+
expect(res.status).toBe(404);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('whitelist with RegExp: /^\\.public/ matches .public-assets', async () => {
|
|
180
|
+
// This tests the regex matching logic; .public-assets doesn't exist so we
|
|
181
|
+
// expect 404 from "not found", not from "hidden" — indirectly confirms regex is not blocking
|
|
182
|
+
const server2 = createApp({
|
|
183
|
+
dotFiles: { default: 'hidden', whitelist: [/^\.public/] }
|
|
184
|
+
});
|
|
185
|
+
// .env is not matched by /^\.public/ so it stays hidden
|
|
186
|
+
const res = await supertest(server2).get('/.env');
|
|
187
|
+
expect(res.status).toBe(404);
|
|
188
|
+
server2.close();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ─── dotFiles blacklist ───────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
describe('dotFiles — blacklist', () => {
|
|
195
|
+
let server;
|
|
196
|
+
beforeAll(() => {
|
|
197
|
+
server = createApp({
|
|
198
|
+
dotFiles: { default: 'visible', blacklist: ['.env'] }
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
afterAll(() => server.close());
|
|
202
|
+
|
|
203
|
+
test('GET /.env returns 404 (blacklisted even with default: visible)', async () => {
|
|
204
|
+
const res = await supertest(server).get('/.env');
|
|
205
|
+
expect(res.status).toBe(404);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('GET /.gitignore returns 200 (visible, not blacklisted)', async () => {
|
|
209
|
+
const res = await supertest(server).get('/.gitignore');
|
|
210
|
+
expect(res.status).toBe(200);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('.env not in listing when blacklisted', async () => {
|
|
214
|
+
const res = await supertest(server).get('/');
|
|
215
|
+
expect(res.text).not.toContain('.env');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ─── blacklist beats whitelist ────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
describe('dotFiles — blacklist beats whitelist', () => {
|
|
222
|
+
let server;
|
|
223
|
+
beforeAll(() => {
|
|
224
|
+
server = createApp({
|
|
225
|
+
dotFiles: { default: 'visible', whitelist: ['.env'], blacklist: ['.env'] }
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
afterAll(() => server.close());
|
|
229
|
+
|
|
230
|
+
test('GET /.env returns 404 (blacklist wins over whitelist)', async () => {
|
|
231
|
+
const res = await supertest(server).get('/.env');
|
|
232
|
+
expect(res.status).toBe(404);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// ─── dotDirs default: 'visible' (system default) ─────────────────────────────
|
|
237
|
+
|
|
238
|
+
describe('dotDirs — default visible (system default)', () => {
|
|
239
|
+
let server;
|
|
240
|
+
beforeAll(() => { server = createApp(undefined); });
|
|
241
|
+
afterAll(() => server.close());
|
|
242
|
+
|
|
243
|
+
test('.dot-dir appears in root listing by default', async () => {
|
|
244
|
+
const res = await supertest(server).get('/');
|
|
245
|
+
expect(res.text).toContain('.dot-dir');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('GET /.dot-dir/ returns 200 (directory listing) by default', async () => {
|
|
249
|
+
const res = await supertest(server).get('/.dot-dir/');
|
|
250
|
+
expect(res.status).toBe(200);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('GET /.dot-dir/inside.txt returns 200 by default', async () => {
|
|
254
|
+
const res = await supertest(server).get('/.dot-dir/inside.txt');
|
|
255
|
+
expect(res.status).toBe(200);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ─── dotDirs blacklist ────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
describe('dotDirs — blacklist', () => {
|
|
262
|
+
let server;
|
|
263
|
+
beforeAll(() => {
|
|
264
|
+
server = createApp({ dotDirs: { blacklist: ['.dot-dir'] } });
|
|
265
|
+
});
|
|
266
|
+
afterAll(() => server.close());
|
|
267
|
+
|
|
268
|
+
test('GET /.dot-dir/ returns 404 (blacklisted dir)', async () => {
|
|
269
|
+
const res = await supertest(server).get('/.dot-dir/');
|
|
270
|
+
expect(res.status).toBe(404);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('GET /.dot-dir/inside.txt returns 404 (inside blocked dir)', async () => {
|
|
274
|
+
const res = await supertest(server).get('/.dot-dir/inside.txt');
|
|
275
|
+
expect(res.status).toBe(404);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('.dot-dir not in root listing when blacklisted', async () => {
|
|
279
|
+
const res = await supertest(server).get('/');
|
|
280
|
+
expect(res.text).not.toContain('.dot-dir');
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ─── dotDirs blacklist with RegExp ────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
describe('dotDirs — blacklist with RegExp', () => {
|
|
287
|
+
let server;
|
|
288
|
+
beforeAll(() => {
|
|
289
|
+
server = createApp({ dotDirs: { blacklist: [/^\.dot/] } });
|
|
290
|
+
});
|
|
291
|
+
afterAll(() => server.close());
|
|
292
|
+
|
|
293
|
+
test('GET /.dot-dir/ returns 404 (RegExp blacklist match)', async () => {
|
|
294
|
+
const res = await supertest(server).get('/.dot-dir/');
|
|
295
|
+
expect(res.status).toBe(404);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('.well-known accessible (does not match /^\\.dot/)', async () => {
|
|
299
|
+
const res = await supertest(server).get('/.well-known/');
|
|
300
|
+
expect(res.status).toBe(200);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ─── alwaysHide ───────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
describe('alwaysHide — glob and regex patterns', () => {
|
|
307
|
+
let server;
|
|
308
|
+
beforeAll(() => {
|
|
309
|
+
server = createApp({
|
|
310
|
+
dotFiles: { default: 'visible' }, // dot-files visible so we can test alwaysHide separately
|
|
311
|
+
alwaysHide: ['*.secret', /\.key$/]
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
afterAll(() => server.close());
|
|
315
|
+
|
|
316
|
+
test('GET /file.secret returns 404 (glob *.secret)', async () => {
|
|
317
|
+
const res = await supertest(server).get('/file.secret');
|
|
318
|
+
expect(res.status).toBe(404);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('GET /data.key returns 404 (regex /\\.key$/)', async () => {
|
|
322
|
+
const res = await supertest(server).get('/data.key');
|
|
323
|
+
expect(res.status).toBe(404);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('file.secret not in directory listing', async () => {
|
|
327
|
+
const res = await supertest(server).get('/');
|
|
328
|
+
expect(res.text).not.toContain('file.secret');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('data.key not in directory listing', async () => {
|
|
332
|
+
const res = await supertest(server).get('/');
|
|
333
|
+
expect(res.text).not.toContain('data.key');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('regular files remain accessible', async () => {
|
|
337
|
+
const res = await supertest(server).get('/normal.txt');
|
|
338
|
+
expect(res.status).toBe(200);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ─── alwaysHide — path-anchored patterns ─────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
describe('alwaysHide — path-anchored glob (config/secrets/**)', () => {
|
|
345
|
+
let server;
|
|
346
|
+
beforeAll(() => {
|
|
347
|
+
server = createApp({ alwaysHide: ['config/secrets/**'] });
|
|
348
|
+
});
|
|
349
|
+
afterAll(() => server.close());
|
|
350
|
+
|
|
351
|
+
test('GET /config/secrets/password.txt returns 404', async () => {
|
|
352
|
+
const res = await supertest(server).get('/config/secrets/password.txt');
|
|
353
|
+
expect(res.status).toBe(404);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('password.txt not in /config/secrets/ listing', async () => {
|
|
357
|
+
const res = await supertest(server).get('/config/secrets/');
|
|
358
|
+
expect(res.status).toBe(200);
|
|
359
|
+
expect(res.text).not.toContain('password.txt');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('GET /normal.txt remains accessible (not under config/secrets/)', async () => {
|
|
363
|
+
const res = await supertest(server).get('/normal.txt');
|
|
364
|
+
expect(res.status).toBe(200);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ─── alwaysHide secondary to whitelist ───────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
describe('alwaysHide — secondary to dotFiles whitelist', () => {
|
|
371
|
+
let server;
|
|
372
|
+
beforeAll(() => {
|
|
373
|
+
server = createApp({
|
|
374
|
+
dotFiles: { default: 'hidden', whitelist: ['.env'] },
|
|
375
|
+
alwaysHide: ['.env'] // both alwaysHide and whitelist target .env
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
afterAll(() => server.close());
|
|
379
|
+
|
|
380
|
+
test('GET /.env returns 200 (whitelist wins over alwaysHide)', async () => {
|
|
381
|
+
const res = await supertest(server).get('/.env');
|
|
382
|
+
expect(res.status).toBe(200);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ─── deep tree: dot-files hidden at any depth ─────────────────────────────────
|
|
387
|
+
|
|
388
|
+
describe('hidden entries at any depth in directory tree (opt-in dotFiles.default=hidden)', () => {
|
|
389
|
+
let server;
|
|
390
|
+
beforeAll(() => { server = createApp({ dotFiles: { default: 'hidden' } }); });
|
|
391
|
+
afterAll(() => server.close());
|
|
392
|
+
|
|
393
|
+
test('GET /subdir/.env returns 404 (dot-file hidden at any depth)', async () => {
|
|
394
|
+
const res = await supertest(server).get('/subdir/.env');
|
|
395
|
+
expect(res.status).toBe(404);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('/subdir/.env not in /subdir/ listing', async () => {
|
|
399
|
+
const res = await supertest(server).get('/subdir/');
|
|
400
|
+
expect(res.text).not.toContain('.env');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('GET /subdir/regular.txt returns 200 (regular file accessible)', async () => {
|
|
404
|
+
const res = await supertest(server).get('/subdir/regular.txt');
|
|
405
|
+
expect(res.status).toBe(200);
|
|
406
|
+
});
|
|
407
|
+
});
|