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
|
@@ -31,7 +31,7 @@ describe('hideExtension option tests', () => {
|
|
|
31
31
|
beforeAll(() => {
|
|
32
32
|
app = new Koa();
|
|
33
33
|
app.use(koaClassicServer(rootDir, {
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
162
|
+
// Directory/file conflict (dirListing: { enabled: true })
|
|
163
163
|
// ==========================================
|
|
164
|
-
describe('Directory/file conflict with
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
43
|
-
test('
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
expect(
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 () => {
|
package/__tests__/index.test.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) || '
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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>');
|