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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
2
2
|
//
|
|
3
3
|
// SECURITY & BUG TESTS
|
|
4
|
-
//
|
|
4
|
+
// These tests verify the security properties and bug fixes documented in DEBUG_REPORT.md
|
|
5
5
|
//
|
|
6
6
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
7
7
|
|
|
@@ -13,6 +13,8 @@ const path = require('path');
|
|
|
13
13
|
|
|
14
14
|
const rootDir = path.join(__dirname, 'publicWwwTest');
|
|
15
15
|
|
|
16
|
+
// ─── Path Traversal ───────────────────────────────────────────────────────────
|
|
17
|
+
|
|
16
18
|
describe('Security Tests - Path Traversal', () => {
|
|
17
19
|
let app;
|
|
18
20
|
let server;
|
|
@@ -20,44 +22,56 @@ describe('Security Tests - Path Traversal', () => {
|
|
|
20
22
|
beforeAll(() => {
|
|
21
23
|
app = new Koa();
|
|
22
24
|
app.use(koaClassicServer(rootDir, {
|
|
23
|
-
|
|
25
|
+
dirListing: { enabled: true }
|
|
24
26
|
}));
|
|
25
27
|
server = app.listen();
|
|
26
28
|
});
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
afterAll(() => {
|
|
31
|
+
server.close();
|
|
32
|
+
});
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// On Linux, path.normalize() cannot escape '/' (the OS root), so '/../package.json'
|
|
35
|
+
// resolves to rootDir/package.json — which simply does not exist → 404.
|
|
36
|
+
// On Windows, backslash sequences can escape rootDir and the startsWith() check
|
|
37
|
+
// fires → 403. Both outcomes prove the traversal was blocked (no 200 with file content).
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
test('../ traversal is blocked (403 or 404, never 200 with file content)', async () => {
|
|
40
|
+
const res = await supertest(server).get('/../package.json');
|
|
41
|
+
expect([403, 404]).toContain(res.status);
|
|
42
|
+
expect(res.text).not.toContain('"name"');
|
|
40
43
|
});
|
|
41
44
|
|
|
42
|
-
test('
|
|
43
|
-
// Tenta con encoding URL
|
|
45
|
+
test('URL-encoded ../ traversal (%2e%2e%2f) is blocked (403 or 404)', async () => {
|
|
44
46
|
const res = await supertest(server).get('/%2e%2e%2f%2e%2e%2fpackage.json');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
expect([403, 404]).toContain(res.status);
|
|
48
|
+
expect(res.text).not.toContain('"name"');
|
|
47
49
|
});
|
|
48
50
|
|
|
49
|
-
test('
|
|
50
|
-
// Tenta path assoluto
|
|
51
|
+
test('multi-level traversal (/../../../etc/hosts) is blocked (403 or 404)', async () => {
|
|
51
52
|
const res = await supertest(server).get('/../../../etc/hosts');
|
|
53
|
+
expect([403, 404]).toContain(res.status);
|
|
54
|
+
});
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
test('null byte in path is rejected with 400', async () => {
|
|
57
|
+
// path.normalize() throws ERR_INVALID_ARG_VALUE for paths with \0;
|
|
58
|
+
// the null byte guard returns 400 before reaching fs operations.
|
|
59
|
+
const res = await supertest(server).get('/file%00.txt');
|
|
60
|
+
expect(res.status).toBe(400);
|
|
54
61
|
});
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
test('backslash sequences are not treated as path separators on Linux', async () => {
|
|
64
|
+
// On Linux, \\ is a literal filename character, not a path separator.
|
|
65
|
+
// path.normalize leaves it intact and the resolved path stays within rootDir.
|
|
66
|
+
// The file simply does not exist → 404 (not a traversal escape).
|
|
67
|
+
const res = await supertest(server).get('/..%5C..%5Cetc%5Chosts');
|
|
68
|
+
expect([403, 404]).toContain(res.status);
|
|
69
|
+
// On Windows, path.normalize converts \ to / and the traversal check catches it → 403.
|
|
58
70
|
});
|
|
59
71
|
});
|
|
60
72
|
|
|
73
|
+
// ─── Status Code 404 ─────────────────────────────────────────────────────────
|
|
74
|
+
|
|
61
75
|
describe('Bug Tests - Status Code 404', () => {
|
|
62
76
|
let app;
|
|
63
77
|
let server;
|
|
@@ -65,34 +79,25 @@ describe('Bug Tests - Status Code 404', () => {
|
|
|
65
79
|
beforeAll(() => {
|
|
66
80
|
app = new Koa();
|
|
67
81
|
app.use(koaClassicServer(rootDir, {
|
|
68
|
-
|
|
82
|
+
dirListing: { enabled: true }
|
|
69
83
|
}));
|
|
70
84
|
server = app.listen();
|
|
71
85
|
});
|
|
72
86
|
|
|
73
|
-
test('FIXED:
|
|
87
|
+
test('FIXED: Non-existent file returns 404', async () => {
|
|
74
88
|
const res = await supertest(server).get('/file-che-non-esiste-xyz123.txt');
|
|
75
|
-
|
|
76
|
-
console.log('✅ 404 Status Test - Status Code:', res.status);
|
|
77
|
-
console.log(' Expected: 404, Got:', res.status);
|
|
78
|
-
|
|
79
|
-
// FIXED: Now returns proper 404 status
|
|
80
89
|
expect(res.status).toBe(404);
|
|
81
90
|
expect(res.text).toContain('Not Found');
|
|
82
91
|
});
|
|
83
92
|
|
|
84
|
-
test('FIXED: Directory
|
|
93
|
+
test('FIXED: Directory with dirListing.enabled=false returns 404', async () => {
|
|
85
94
|
const app2 = new Koa();
|
|
86
95
|
app2.use(koaClassicServer(rootDir, {
|
|
87
|
-
|
|
96
|
+
dirListing: { enabled: false }
|
|
88
97
|
}));
|
|
89
98
|
const server2 = app2.listen();
|
|
90
99
|
|
|
91
100
|
const res = await supertest(server2).get('/');
|
|
92
|
-
|
|
93
|
-
console.log('✅ 404 Directory Test - Status Code:', res.status);
|
|
94
|
-
|
|
95
|
-
// FIXED: Now returns proper 404 status
|
|
96
101
|
expect(res.status).toBe(404);
|
|
97
102
|
|
|
98
103
|
server2.close();
|
|
@@ -103,11 +108,12 @@ describe('Bug Tests - Status Code 404', () => {
|
|
|
103
108
|
});
|
|
104
109
|
});
|
|
105
110
|
|
|
111
|
+
// ─── Template Rendering Errors ────────────────────────────────────────────────
|
|
112
|
+
|
|
106
113
|
describe('Bug Tests - Template Rendering Errors', () => {
|
|
107
|
-
test('
|
|
114
|
+
test('FIXED: Template render error returns 500 HTML page, server does not crash', async () => {
|
|
108
115
|
const app = new Koa();
|
|
109
116
|
|
|
110
|
-
// Template che lancia errore
|
|
111
117
|
const brokenRender = async (ctx, next, filePath) => {
|
|
112
118
|
throw new Error('Simulated template rendering error');
|
|
113
119
|
};
|
|
@@ -115,39 +121,29 @@ describe('Bug Tests - Template Rendering Errors', () => {
|
|
|
115
121
|
app.use(koaClassicServer(rootDir, {
|
|
116
122
|
template: {
|
|
117
123
|
render: brokenRender,
|
|
118
|
-
ext: ['txt']
|
|
124
|
+
ext: ['txt']
|
|
119
125
|
}
|
|
120
126
|
}));
|
|
121
127
|
|
|
122
128
|
const server = app.listen();
|
|
123
|
-
|
|
124
|
-
// Crea un file .txt per il test
|
|
125
129
|
const testFile = path.join(rootDir, 'test-template.txt');
|
|
126
130
|
fs.writeFileSync(testFile, 'test content');
|
|
127
131
|
|
|
128
132
|
try {
|
|
129
133
|
const res = await supertest(server).get('/test-template.txt');
|
|
130
134
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// expect(res.status).toBe(500);
|
|
135
|
-
|
|
136
|
-
// ATTUALMENTE POTREBBE CRASHARE IL SERVER
|
|
137
|
-
// Se arriviamo qui senza crash, il test passa
|
|
138
|
-
console.log(' Server did not crash (good)');
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.log('⚠️ Template error caused request to fail:', error.message);
|
|
135
|
+
expect(res.status).toBe(500);
|
|
136
|
+
expect(res.headers['content-type']).toMatch(/html/);
|
|
137
|
+
expect(res.text).toContain('Internal Server Error');
|
|
141
138
|
} finally {
|
|
142
|
-
|
|
143
|
-
if (fs.existsSync(testFile)) {
|
|
144
|
-
fs.unlinkSync(testFile);
|
|
145
|
-
}
|
|
139
|
+
if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
|
|
146
140
|
server.close();
|
|
147
141
|
}
|
|
148
142
|
});
|
|
149
143
|
});
|
|
150
144
|
|
|
145
|
+
// ─── File Extension Extraction ────────────────────────────────────────────────
|
|
146
|
+
|
|
151
147
|
describe('Bug Tests - File Extension Extraction', () => {
|
|
152
148
|
let app;
|
|
153
149
|
let server;
|
|
@@ -155,10 +151,7 @@ describe('Bug Tests - File Extension Extraction', () => {
|
|
|
155
151
|
beforeAll(() => {
|
|
156
152
|
app = new Koa();
|
|
157
153
|
|
|
158
|
-
let renderCalled = false;
|
|
159
154
|
const trackingRender = async (ctx, next, filePath) => {
|
|
160
|
-
renderCalled = true;
|
|
161
|
-
ctx.renderCalled = true;
|
|
162
155
|
ctx.body = 'Rendered: ' + path.basename(filePath);
|
|
163
156
|
};
|
|
164
157
|
|
|
@@ -172,134 +165,96 @@ describe('Bug Tests - File Extension Extraction', () => {
|
|
|
172
165
|
server = app.listen();
|
|
173
166
|
});
|
|
174
167
|
|
|
175
|
-
|
|
176
|
-
|
|
168
|
+
afterAll(() => {
|
|
169
|
+
server.close();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('FIXED: File without extension does not trigger template rendering', async () => {
|
|
177
173
|
const testFile = path.join(rootDir, 'README');
|
|
178
174
|
fs.writeFileSync(testFile, 'readme content');
|
|
179
175
|
|
|
180
176
|
try {
|
|
181
177
|
const res = await supertest(server).get('/README');
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Non dovrebbe essere renderizzato
|
|
186
|
-
expect(res.text).not.toContain('Rendered');
|
|
178
|
+
const responseBody = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
179
|
+
expect(responseBody).not.toContain('Rendered');
|
|
187
180
|
} finally {
|
|
188
|
-
if (fs.existsSync(testFile))
|
|
189
|
-
fs.unlinkSync(testFile);
|
|
190
|
-
}
|
|
181
|
+
if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
|
|
191
182
|
}
|
|
192
183
|
});
|
|
193
184
|
|
|
194
|
-
test('
|
|
195
|
-
//
|
|
185
|
+
test('Unix hidden file (.gitignore) is served as a regular file, not rendered', async () => {
|
|
186
|
+
// dotFiles are hidden by default; make them visible for this test so we can
|
|
187
|
+
// verify that the extension check (not the template renderer) handles the file.
|
|
188
|
+
const appVisible = new Koa();
|
|
189
|
+
appVisible.use(koaClassicServer(rootDir, {
|
|
190
|
+
hidden: { dotFiles: { default: 'visible' } },
|
|
191
|
+
template: {
|
|
192
|
+
render: async (ctx, next, filePath) => { ctx.body = 'Rendered: ' + path.basename(filePath); },
|
|
193
|
+
ext: ['txt']
|
|
194
|
+
}
|
|
195
|
+
}));
|
|
196
|
+
const serverVisible = appVisible.listen();
|
|
197
|
+
|
|
196
198
|
const testFile = path.join(rootDir, '.gitignore');
|
|
197
199
|
fs.writeFileSync(testFile, 'node_modules/');
|
|
198
200
|
|
|
199
201
|
try {
|
|
200
|
-
const res = await supertest(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
const res = await supertest(serverVisible).get('/.gitignore');
|
|
203
|
+
// .gitignore has no .txt extension — template must not be invoked
|
|
204
|
+
expect(res.status).toBe(200);
|
|
205
|
+
const responseBody = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
206
|
+
expect(responseBody).not.toContain('Rendered');
|
|
207
|
+
expect(responseBody).toContain('node_modules/');
|
|
206
208
|
} finally {
|
|
207
|
-
if (fs.existsSync(testFile))
|
|
208
|
-
|
|
209
|
-
}
|
|
209
|
+
if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
|
|
210
|
+
serverVisible.close();
|
|
210
211
|
}
|
|
211
212
|
});
|
|
212
|
-
|
|
213
|
-
afterAll(() => {
|
|
214
|
-
server.close();
|
|
215
|
-
});
|
|
216
213
|
});
|
|
217
214
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// Simula race condition: cancella il file appena dopo la richiesta
|
|
229
|
-
setTimeout(() => {
|
|
230
|
-
if (fs.existsSync(testFile)) {
|
|
231
|
-
fs.unlinkSync(testFile);
|
|
232
|
-
}
|
|
233
|
-
}, 5);
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const res = await supertest(server).get('/temp-race-test.txt');
|
|
237
|
-
|
|
238
|
-
console.log('🐛 Race Condition Test - Status:', res.status);
|
|
215
|
+
// ─── Race Condition File Access ───────────────────────────────────────────────
|
|
216
|
+
//
|
|
217
|
+
// BEHAVIOUR GUARANTEE (not tested — timing is non-deterministic):
|
|
218
|
+
// If a file is deleted between the access check and the stream open,
|
|
219
|
+
// the stream error handler fires and — if headers have not been sent yet —
|
|
220
|
+
// the server returns 500. When rawFile cache is warm the race cannot occur
|
|
221
|
+
// because the file is served entirely from memory.
|
|
222
|
+
//
|
|
223
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
239
224
|
|
|
240
|
-
|
|
241
|
-
// expect(res.status).toBe(404) o 500;
|
|
242
|
-
} catch (error) {
|
|
243
|
-
console.log('⚠️ Race condition caused error:', error.message);
|
|
244
|
-
} finally {
|
|
245
|
-
// Cleanup
|
|
246
|
-
if (fs.existsSync(testFile)) {
|
|
247
|
-
fs.unlinkSync(testFile);
|
|
248
|
-
}
|
|
249
|
-
server.close();
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
});
|
|
225
|
+
// ─── Directory Read Errors ────────────────────────────────────────────────────
|
|
253
226
|
|
|
254
227
|
describe('Bug Tests - Directory Read Errors', () => {
|
|
255
|
-
test('
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (!fs.existsSync(tempDir)) {
|
|
261
|
-
fs.mkdirSync(tempDir);
|
|
262
|
-
}
|
|
228
|
+
test('FIXED: Unreadable directory returns 500, server does not crash', async () => {
|
|
229
|
+
// Skip on Windows (chmod has no effect) and when running as root
|
|
230
|
+
// (root ignores permission bits, so chmod 0o000 has no effect).
|
|
231
|
+
if (process.platform === 'win32') return;
|
|
232
|
+
if (typeof process.getuid === 'function' && process.getuid() === 0) return;
|
|
263
233
|
|
|
264
|
-
app
|
|
265
|
-
|
|
266
|
-
|
|
234
|
+
const app = new Koa();
|
|
235
|
+
const tempDir = path.join(rootDir, 'temp-perm-test-dir');
|
|
236
|
+
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir);
|
|
267
237
|
|
|
238
|
+
app.use(koaClassicServer(rootDir, { dirListing: { enabled: true } }));
|
|
268
239
|
const server = app.listen();
|
|
269
240
|
|
|
270
241
|
try {
|
|
271
|
-
|
|
272
|
-
const res1 = await supertest(server).get('/temp-test-dir');
|
|
242
|
+
const res1 = await supertest(server).get('/temp-perm-test-dir');
|
|
273
243
|
expect(res1.status).toBe(200);
|
|
274
244
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const res2 = await supertest(server).get('/temp-test-dir');
|
|
280
|
-
|
|
281
|
-
console.log('🐛 Directory Permission Test - Status:', res2.status);
|
|
282
|
-
|
|
283
|
-
// Dovrebbe gestire l'errore
|
|
284
|
-
// expect(res2.status).toBe(500) o 403;
|
|
285
|
-
}
|
|
286
|
-
} catch (error) {
|
|
287
|
-
console.log('⚠️ Directory read error:', error.message);
|
|
245
|
+
fs.chmodSync(tempDir, 0o000);
|
|
246
|
+
const res2 = await supertest(server).get('/temp-perm-test-dir');
|
|
247
|
+
expect([403, 500]).toContain(res2.status);
|
|
288
248
|
} finally {
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
try {
|
|
292
|
-
fs.chmodSync(tempDir, 0o755);
|
|
293
|
-
} catch (e) {}
|
|
294
|
-
}
|
|
295
|
-
if (fs.existsSync(tempDir)) {
|
|
296
|
-
fs.rmdirSync(tempDir);
|
|
297
|
-
}
|
|
249
|
+
try { fs.chmodSync(tempDir, 0o755); } catch { /* ignore */ }
|
|
250
|
+
if (fs.existsSync(tempDir)) fs.rmdirSync(tempDir);
|
|
298
251
|
server.close();
|
|
299
252
|
}
|
|
300
253
|
});
|
|
301
254
|
});
|
|
302
255
|
|
|
256
|
+
// ─── Content-Disposition ──────────────────────────────────────────────────────
|
|
257
|
+
|
|
303
258
|
describe('Bug Tests - Content-Disposition', () => {
|
|
304
259
|
let app;
|
|
305
260
|
let server;
|
|
@@ -310,27 +265,58 @@ describe('Bug Tests - Content-Disposition', () => {
|
|
|
310
265
|
server = app.listen();
|
|
311
266
|
});
|
|
312
267
|
|
|
313
|
-
|
|
314
|
-
|
|
268
|
+
afterAll(() => {
|
|
269
|
+
server.close();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('FIXED: Filename with spaces is quoted in Content-Disposition', async () => {
|
|
273
|
+
const testFile = path.join(rootDir, 'file with spaces.txt');
|
|
274
|
+
fs.writeFileSync(testFile, 'test content');
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const res = await supertest(server).get('/file%20with%20spaces.txt');
|
|
278
|
+
const contentDisp = res.headers['content-disposition'];
|
|
279
|
+
// quoted-string form must wrap the filename in double quotes
|
|
280
|
+
expect(contentDisp).toMatch(/filename="file with spaces\.txt"/);
|
|
281
|
+
} finally {
|
|
282
|
+
if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('FIXED: Filename with special chars has RFC 5987 extended form', async () => {
|
|
315
287
|
const testFile = path.join(rootDir, 'file with spaces & special.txt');
|
|
316
288
|
fs.writeFileSync(testFile, 'test content');
|
|
317
289
|
|
|
318
290
|
try {
|
|
319
291
|
const res = await supertest(server).get('/file%20with%20spaces%20%26%20special.txt');
|
|
320
|
-
|
|
321
292
|
const contentDisp = res.headers['content-disposition'];
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
|
|
293
|
+
// RFC 5987 extended form must be present with UTF-8 encoding prefix
|
|
294
|
+
expect(contentDisp).toMatch(/filename\*=UTF-8''/);
|
|
295
|
+
// The & must be percent-encoded as %26 in the extended form
|
|
296
|
+
expect(contentDisp).toContain('%26');
|
|
326
297
|
} finally {
|
|
327
|
-
if (fs.existsSync(testFile))
|
|
328
|
-
fs.unlinkSync(testFile);
|
|
329
|
-
}
|
|
298
|
+
if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
|
|
330
299
|
}
|
|
331
300
|
});
|
|
332
301
|
|
|
333
|
-
|
|
334
|
-
|
|
302
|
+
test('FIXED: Filename with double-quote is safely escaped', async () => {
|
|
303
|
+
// Create file with a double-quote in its name (valid on Linux)
|
|
304
|
+
let testFile;
|
|
305
|
+
try {
|
|
306
|
+
testFile = path.join(rootDir, 'file"name.txt');
|
|
307
|
+
fs.writeFileSync(testFile, 'test');
|
|
308
|
+
} catch {
|
|
309
|
+
// Some filesystems disallow " in filenames — skip gracefully
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const res = await supertest(server).get('/file%22name.txt');
|
|
315
|
+
const contentDisp = res.headers['content-disposition'];
|
|
316
|
+
// The " inside the quoted-string must be escaped as \"
|
|
317
|
+
expect(contentDisp).toMatch(/filename="file\\"name\.txt"/);
|
|
318
|
+
} finally {
|
|
319
|
+
if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
|
|
320
|
+
}
|
|
335
321
|
});
|
|
336
322
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|