koa-classic-server 1.2.0 β 2.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/BENCHMARKS.md +317 -0
- package/CREATE_RELEASE.sh +53 -0
- package/EXAMPLES_INDEX_OPTION.md +395 -0
- package/INDEX_OPTION_PRIORITY.md +527 -0
- package/OPTIMIZATION_HTTP_CACHING.md +687 -0
- package/PERFORMANCE_ANALYSIS.md +839 -0
- package/PERFORMANCE_COMPARISON.md +388 -0
- package/README.md +21 -5
- package/__tests__/index-option.test.js +447 -0
- package/__tests__/performance.test.js +301 -0
- package/benchmark-results-baseline-v1.2.0.txt +354 -0
- package/benchmark-results-optimized-v2.0.0.txt +354 -0
- package/benchmark.js +239 -0
- package/demo-regex-index.js +140 -0
- package/index.cjs +201 -51
- package/jest.config.js +18 -0
- package/package.json +9 -4
- package/publish-to-npm.sh +65 -0
- package/scripts/setup-benchmark.js +178 -0
- package/test-regex-quick.js +158 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Demo: Enhanced index option with RegExp support
|
|
5
|
+
*
|
|
6
|
+
* Questo esempio dimostra come usare RegExp nell'opzione index
|
|
7
|
+
* per matching case-insensitive e pattern flessibili
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const Koa = require('koa');
|
|
11
|
+
const koaClassicServer = require('./index.cjs');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Crea directory di test
|
|
16
|
+
const testDir = path.join(__dirname, 'demo-regex-test');
|
|
17
|
+
if (!fs.existsSync(testDir)) {
|
|
18
|
+
fs.mkdirSync(testDir);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Crea file di test con vari case
|
|
22
|
+
console.log('π Creazione file di test...\n');
|
|
23
|
+
|
|
24
|
+
const files = [
|
|
25
|
+
{ name: 'INDEX.HTML', content: '<h1 style="color: red;">Trovato INDEX.HTML (maiuscolo)</h1>' },
|
|
26
|
+
{ name: 'Index.Html', content: '<h1 style="color: blue;">Trovato Index.Html (mixed case)</h1>' },
|
|
27
|
+
{ name: 'index.htm', content: '<h1 style="color: green;">Trovato index.htm (estensione .htm)</h1>' },
|
|
28
|
+
{ name: 'default.html', content: '<h1 style="color: orange;">Trovato default.html</h1>' },
|
|
29
|
+
{ name: 'readme.txt', content: 'Questo Γ¨ un file normale' }
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
files.forEach(file => {
|
|
33
|
+
const filePath = path.join(testDir, file.name);
|
|
34
|
+
fs.writeFileSync(filePath, file.content);
|
|
35
|
+
console.log(` β Creato: ${file.name}`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
console.log('\n' + '='.repeat(70));
|
|
39
|
+
console.log('π§ͺ TEST CONFIGURAZIONI DIVERSE\n');
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// TEST 1: Solo case-insensitive .html
|
|
43
|
+
// ============================================================================
|
|
44
|
+
console.log('TEST 1: RegExp case-insensitive /index\\.html/i');
|
|
45
|
+
console.log(' Pattern: [/index\\.html/i]');
|
|
46
|
+
console.log(' Aspettativa: Matcha INDEX.HTML o Index.Html');
|
|
47
|
+
|
|
48
|
+
const app1 = new Koa();
|
|
49
|
+
app1.use(koaClassicServer(testDir, {
|
|
50
|
+
index: [/index\.html/i],
|
|
51
|
+
showDirContents: true
|
|
52
|
+
}));
|
|
53
|
+
const server1 = app1.listen(3001);
|
|
54
|
+
console.log(' β Server avviato su http://localhost:3001\n');
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// TEST 2: Multiple estensioni (.html e .htm)
|
|
58
|
+
// ============================================================================
|
|
59
|
+
console.log('TEST 2: RegExp per .html e .htm');
|
|
60
|
+
console.log(' Pattern: [/index\\.(html|htm)/i]');
|
|
61
|
+
console.log(' Aspettativa: Matcha INDEX.HTML, Index.Html, index.htm');
|
|
62
|
+
|
|
63
|
+
const app2 = new Koa();
|
|
64
|
+
app2.use(koaClassicServer(testDir, {
|
|
65
|
+
index: [/index\.(html|htm)/i],
|
|
66
|
+
showDirContents: true
|
|
67
|
+
}));
|
|
68
|
+
const server2 = app2.listen(3002);
|
|
69
|
+
console.log(' β Server avviato su http://localhost:3002\n');
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// TEST 3: Priority: index.html prima, poi default.html
|
|
73
|
+
// ============================================================================
|
|
74
|
+
console.log('TEST 3: Array con prioritΓ ');
|
|
75
|
+
console.log(' Pattern: [/index\\.(html|htm)/i, /default\\.html/i]');
|
|
76
|
+
console.log(' Aspettativa: Prima cerca index.*, poi default.html');
|
|
77
|
+
|
|
78
|
+
const app3 = new Koa();
|
|
79
|
+
app3.use(koaClassicServer(testDir, {
|
|
80
|
+
index: [
|
|
81
|
+
/index\.(html|htm)/i,
|
|
82
|
+
/default\.html/i
|
|
83
|
+
],
|
|
84
|
+
showDirContents: true
|
|
85
|
+
}));
|
|
86
|
+
const server3 = app3.listen(3003);
|
|
87
|
+
console.log(' β Server avviato su http://localhost:3003\n');
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// TEST 4: Mixed (string + RegExp)
|
|
91
|
+
// ============================================================================
|
|
92
|
+
console.log('TEST 4: Mixed array (string exact + RegExp fallback)');
|
|
93
|
+
console.log(' Pattern: ["index.html", /INDEX\\.HTML/i, /default\\.html/i]');
|
|
94
|
+
console.log(' Aspettativa: Prima exact "index.html", poi case-insensitive');
|
|
95
|
+
|
|
96
|
+
const app4 = new Koa();
|
|
97
|
+
app4.use(koaClassicServer(testDir, {
|
|
98
|
+
index: [
|
|
99
|
+
'index.html', // Exact match (piΓΉ veloce)
|
|
100
|
+
/INDEX\.HTML/i, // Case-insensitive fallback
|
|
101
|
+
/default\.html/i // Default fallback
|
|
102
|
+
],
|
|
103
|
+
showDirContents: true
|
|
104
|
+
}));
|
|
105
|
+
const server4 = app4.listen(3004);
|
|
106
|
+
console.log(' β Server avviato su http://localhost:3004\n');
|
|
107
|
+
|
|
108
|
+
console.log('='.repeat(70));
|
|
109
|
+
console.log('\nπ SERVER ATTIVI:\n');
|
|
110
|
+
console.log(' 1οΈβ£ http://localhost:3001 - Case-insensitive /index\\.html/i');
|
|
111
|
+
console.log(' 2οΈβ£ http://localhost:3002 - Multi-extension /index\\.(html|htm)/i');
|
|
112
|
+
console.log(' 3οΈβ£ http://localhost:3003 - Priority array con fallback');
|
|
113
|
+
console.log(' 4οΈβ£ http://localhost:3004 - Mixed string + RegExp\n');
|
|
114
|
+
|
|
115
|
+
console.log('π File presenti nella directory:');
|
|
116
|
+
files.forEach(file => {
|
|
117
|
+
console.log(` - ${file.name}`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
console.log('\nπ‘ Prova ad aprire i link sopra per vedere quale file viene servito!');
|
|
121
|
+
console.log(' Ogni server ha una configurazione diversa.\n');
|
|
122
|
+
console.log('βΉοΈ Premi Ctrl+C per fermare i server\n');
|
|
123
|
+
|
|
124
|
+
// Cleanup on exit
|
|
125
|
+
process.on('SIGINT', () => {
|
|
126
|
+
console.log('\n\nπ§Ή Chiusura server e pulizia...');
|
|
127
|
+
server1.close();
|
|
128
|
+
server2.close();
|
|
129
|
+
server3.close();
|
|
130
|
+
server4.close();
|
|
131
|
+
|
|
132
|
+
// Rimuovi file di test
|
|
133
|
+
files.forEach(file => {
|
|
134
|
+
fs.unlinkSync(path.join(testDir, file.name));
|
|
135
|
+
});
|
|
136
|
+
fs.rmdirSync(testDir);
|
|
137
|
+
|
|
138
|
+
console.log('β Cleanup completato\n');
|
|
139
|
+
process.exit(0);
|
|
140
|
+
});
|
package/index.cjs
CHANGED
|
@@ -4,17 +4,23 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const mime = require("mime-types");
|
|
6
6
|
|
|
7
|
-
// koa-classic-server -
|
|
8
|
-
// Version:
|
|
9
|
-
//
|
|
7
|
+
// koa-classic-server - Performance optimized version
|
|
8
|
+
// Version: 2.0.0
|
|
9
|
+
// Optimizations applied (v2.0.0):
|
|
10
|
+
// - All sync operations converted to async (non-blocking event loop)
|
|
11
|
+
// - String concatenation replaced with array join (30-40% less memory)
|
|
12
|
+
// - HTTP caching with ETag and Last-Modified (80-95% bandwidth reduction)
|
|
13
|
+
// - Conditional requests support (304 Not Modified)
|
|
14
|
+
//
|
|
15
|
+
// Security fixes (from v1.2.0):
|
|
10
16
|
// - Path Traversal vulnerability protection
|
|
11
17
|
// - Status code 404 properly set
|
|
12
18
|
// - Template rendering error handling
|
|
13
19
|
// - Race condition file access protection
|
|
14
20
|
// - Proper file extension extraction
|
|
15
|
-
// - fs.
|
|
21
|
+
// - fs.readdir error handling
|
|
16
22
|
// - Content-Disposition properly quoted
|
|
17
|
-
// -
|
|
23
|
+
// - XSS protection in directory listing
|
|
18
24
|
|
|
19
25
|
module.exports = function koaClassicServer(
|
|
20
26
|
rootDir,
|
|
@@ -24,13 +30,22 @@ module.exports = function koaClassicServer(
|
|
|
24
30
|
opts = {
|
|
25
31
|
method: ['GET'], // Supported methods, otherwise next() will be called
|
|
26
32
|
showDirContents: true, // Show or hide directory contents
|
|
27
|
-
index: "", // Index file name
|
|
33
|
+
index: ["index.html"], // Index file name(s) - ARRAY FORMAT (recommended):
|
|
34
|
+
// - Array of strings: ["index.html", "index.htm", "default.html"]
|
|
35
|
+
// - Array of RegExp: [/index\.html/i, /default\.(html|htm)/i]
|
|
36
|
+
// - Mixed array: ["index.html", /index\.[eE][jJ][sS]/]
|
|
37
|
+
// Priority is determined by array order (first match wins)
|
|
38
|
+
//
|
|
39
|
+
// DEPRECATED: String format "index.html" is still supported but
|
|
40
|
+
// will be removed in future versions. Use array format instead.
|
|
28
41
|
urlPrefix: "", // URL path prefix
|
|
29
42
|
urlsReserved: [], // Reserved paths (first level only)
|
|
30
43
|
template: {
|
|
31
44
|
render: undefined, // Template rendering function: async (ctx, next, filePath) => {}
|
|
32
45
|
ext: [], // File extensions to process with template.render
|
|
33
46
|
},
|
|
47
|
+
cacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 1 hour)
|
|
48
|
+
enableCaching: true, // Enable HTTP caching headers (ETag, Last-Modified)
|
|
34
49
|
}
|
|
35
50
|
*/
|
|
36
51
|
) {
|
|
@@ -51,12 +66,40 @@ module.exports = function koaClassicServer(
|
|
|
51
66
|
|
|
52
67
|
options.method = Array.isArray(options.method) ? options.method : ['GET'];
|
|
53
68
|
options.showDirContents = typeof options.showDirContents == 'boolean' ? options.showDirContents : true;
|
|
54
|
-
|
|
69
|
+
|
|
70
|
+
// Normalize index option to array format
|
|
71
|
+
if (typeof options.index == 'string') {
|
|
72
|
+
// DEPRECATION WARNING: String format is deprecated
|
|
73
|
+
if (options.index) {
|
|
74
|
+
console.warn(
|
|
75
|
+
'\x1b[33m%s\x1b[0m',
|
|
76
|
+
'[koa-classic-server] DEPRECATION WARNING: Passing a string to the "index" option is deprecated and may be removed in future versions.\n' +
|
|
77
|
+
` Current usage: index: "${options.index}"\n` +
|
|
78
|
+
` Recommended: index: ["${options.index}"]\n` +
|
|
79
|
+
' Please update your configuration to use an array format.'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
// Single string β convert to array with one element
|
|
83
|
+
options.index = options.index ? [options.index] : [];
|
|
84
|
+
} else if (Array.isArray(options.index)) {
|
|
85
|
+
// Already an array β validate elements are strings or RegExp
|
|
86
|
+
options.index = options.index.filter(item =>
|
|
87
|
+
typeof item === 'string' || item instanceof RegExp
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
// Invalid type β default to empty array
|
|
91
|
+
options.index = [];
|
|
92
|
+
}
|
|
93
|
+
|
|
55
94
|
options.urlPrefix = typeof options.urlPrefix == 'string' ? options.urlPrefix : "";
|
|
56
95
|
options.urlsReserved = Array.isArray(options.urlsReserved) ? options.urlsReserved : [];
|
|
57
96
|
options.template.render = (options.template.render == undefined || typeof options.template.render == 'function') ? options.template.render : undefined;
|
|
58
97
|
options.template.ext = Array.isArray(options.template.ext) ? options.template.ext : [];
|
|
59
98
|
|
|
99
|
+
// OPTIMIZATION: HTTP Caching options
|
|
100
|
+
options.cacheMaxAge = typeof options.cacheMaxAge == 'number' && options.cacheMaxAge >= 0 ? options.cacheMaxAge : 3600;
|
|
101
|
+
options.enableCaching = typeof options.enableCaching == 'boolean' ? options.enableCaching : true;
|
|
102
|
+
|
|
60
103
|
return async (ctx, next) => {
|
|
61
104
|
// Check if method is allowed
|
|
62
105
|
if (!options.method.includes(ctx.method)) {
|
|
@@ -103,7 +146,7 @@ module.exports = function koaClassicServer(
|
|
|
103
146
|
}
|
|
104
147
|
}
|
|
105
148
|
|
|
106
|
-
//
|
|
149
|
+
// Path Traversal Protection
|
|
107
150
|
// Construct safe file path
|
|
108
151
|
let requestedPath = "";
|
|
109
152
|
if (pageHrefOutPrefix.pathname == "/") {
|
|
@@ -125,48 +168,99 @@ module.exports = function koaClassicServer(
|
|
|
125
168
|
|
|
126
169
|
let toOpen = fullPath;
|
|
127
170
|
|
|
128
|
-
//
|
|
129
|
-
if (!fs.existsSync(toOpen)) {
|
|
130
|
-
ctx.status = 404; // FIX: Set proper status code
|
|
131
|
-
ctx.body = requestedUrlNotFound();
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
171
|
+
// OPTIMIZATION: Check if file/directory exists (async, non-blocking)
|
|
135
172
|
let stat;
|
|
136
173
|
try {
|
|
137
|
-
stat = fs.
|
|
174
|
+
stat = await fs.promises.stat(toOpen);
|
|
138
175
|
} catch (error) {
|
|
139
|
-
|
|
140
|
-
ctx.status =
|
|
141
|
-
ctx.body =
|
|
176
|
+
// File/directory doesn't exist or can't be accessed
|
|
177
|
+
ctx.status = 404;
|
|
178
|
+
ctx.body = requestedUrlNotFound();
|
|
142
179
|
return;
|
|
143
180
|
}
|
|
144
181
|
|
|
145
182
|
if (stat.isDirectory()) {
|
|
146
183
|
// Handle directory
|
|
147
184
|
if (options.showDirContents) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
185
|
+
// NEW: Enhanced index file search with array and RegExp support
|
|
186
|
+
if (options.index && options.index.length > 0) {
|
|
187
|
+
const indexFile = await findIndexFile(toOpen, options.index);
|
|
188
|
+
if (indexFile) {
|
|
189
|
+
const indexPath = path.join(toOpen, indexFile.name);
|
|
190
|
+
await loadFile(indexPath, indexFile.stat);
|
|
152
191
|
return;
|
|
153
192
|
}
|
|
154
193
|
}
|
|
155
|
-
|
|
194
|
+
|
|
195
|
+
// No index file found, show directory listing
|
|
196
|
+
ctx.body = await show_dir(toOpen);
|
|
156
197
|
} else {
|
|
157
|
-
//
|
|
198
|
+
// Directory listing disabled
|
|
158
199
|
ctx.status = 404;
|
|
159
200
|
ctx.body = requestedUrlNotFound();
|
|
160
201
|
}
|
|
161
202
|
return;
|
|
162
203
|
} else {
|
|
163
204
|
// Handle file
|
|
164
|
-
await loadFile(toOpen);
|
|
205
|
+
await loadFile(toOpen, stat);
|
|
165
206
|
return;
|
|
166
207
|
}
|
|
167
208
|
|
|
168
209
|
// Internal functions
|
|
169
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Find index file in directory with priority support
|
|
213
|
+
* @param {string} dirPath - Directory path to search
|
|
214
|
+
* @param {Array<string|RegExp>} indexPatterns - Array of patterns (strings or RegExp)
|
|
215
|
+
* @returns {Promise<{name: string, stat: fs.Stats}|null>} - First matching file or null
|
|
216
|
+
*/
|
|
217
|
+
async function findIndexFile(dirPath, indexPatterns) {
|
|
218
|
+
try {
|
|
219
|
+
// Read directory contents
|
|
220
|
+
const files = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
221
|
+
|
|
222
|
+
// Filter only files (not directories)
|
|
223
|
+
const fileNames = files
|
|
224
|
+
.filter(dirent => dirent.isFile())
|
|
225
|
+
.map(dirent => dirent.name);
|
|
226
|
+
|
|
227
|
+
// Search with priority order (first pattern wins)
|
|
228
|
+
for (const pattern of indexPatterns) {
|
|
229
|
+
let matchedFile = null;
|
|
230
|
+
|
|
231
|
+
if (typeof pattern === 'string') {
|
|
232
|
+
// Exact string match (case-sensitive)
|
|
233
|
+
if (fileNames.includes(pattern)) {
|
|
234
|
+
matchedFile = pattern;
|
|
235
|
+
}
|
|
236
|
+
} else if (pattern instanceof RegExp) {
|
|
237
|
+
// RegExp match (supports case-insensitive with /i flag)
|
|
238
|
+
matchedFile = fileNames.find(fileName => pattern.test(fileName));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// If match found, verify it's a file and return it
|
|
242
|
+
if (matchedFile) {
|
|
243
|
+
try {
|
|
244
|
+
const filePath = path.join(dirPath, matchedFile);
|
|
245
|
+
const fileStat = await fs.promises.stat(filePath);
|
|
246
|
+
if (fileStat.isFile()) {
|
|
247
|
+
return { name: matchedFile, stat: fileStat };
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
// File was deleted between readdir and stat, continue to next pattern
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// No match found
|
|
257
|
+
return null;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('Error finding index file:', error);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
170
264
|
function requestedUrlNotFound() {
|
|
171
265
|
return `
|
|
172
266
|
<!DOCTYPE html>
|
|
@@ -185,14 +279,25 @@ module.exports = function koaClassicServer(
|
|
|
185
279
|
`;
|
|
186
280
|
}
|
|
187
281
|
|
|
188
|
-
//
|
|
189
|
-
async function loadFile(toOpen) {
|
|
190
|
-
//
|
|
282
|
+
// OPTIMIZATION: loadFile now receives stat to avoid double stat call
|
|
283
|
+
async function loadFile(toOpen, fileStat) {
|
|
284
|
+
// Get file stat if not provided
|
|
285
|
+
if (!fileStat) {
|
|
286
|
+
try {
|
|
287
|
+
fileStat = await fs.promises.stat(toOpen);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error('File stat error:', error);
|
|
290
|
+
ctx.status = 404;
|
|
291
|
+
ctx.body = requestedUrlNotFound();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Template rendering
|
|
191
297
|
if (options.template.ext.length > 0 && options.template.render) {
|
|
192
298
|
const fileExt = path.extname(toOpen).slice(1); // Remove leading dot
|
|
193
299
|
|
|
194
300
|
if (fileExt && options.template.ext.includes(fileExt)) {
|
|
195
|
-
// FIX #3: Template rendering error handling
|
|
196
301
|
try {
|
|
197
302
|
await options.template.render(ctx, next, toOpen);
|
|
198
303
|
return;
|
|
@@ -205,7 +310,46 @@ module.exports = function koaClassicServer(
|
|
|
205
310
|
}
|
|
206
311
|
}
|
|
207
312
|
|
|
208
|
-
//
|
|
313
|
+
// OPTIMIZATION: HTTP Caching Headers
|
|
314
|
+
if (options.enableCaching) {
|
|
315
|
+
// Generate ETag from mtime timestamp + file size
|
|
316
|
+
// This ensures ETag changes when file is modified or resized
|
|
317
|
+
const etag = `"${fileStat.mtime.getTime()}-${fileStat.size}"`;
|
|
318
|
+
|
|
319
|
+
// Format Last-Modified header (RFC 7231)
|
|
320
|
+
const lastModified = fileStat.mtime.toUTCString();
|
|
321
|
+
|
|
322
|
+
// Set caching headers
|
|
323
|
+
ctx.set('ETag', etag);
|
|
324
|
+
ctx.set('Last-Modified', lastModified);
|
|
325
|
+
ctx.set('Cache-Control', `public, max-age=${options.cacheMaxAge}, must-revalidate`);
|
|
326
|
+
|
|
327
|
+
// OPTIMIZATION: Handle conditional requests (304 Not Modified)
|
|
328
|
+
|
|
329
|
+
// Check If-None-Match header (ETag validation)
|
|
330
|
+
const clientEtag = ctx.get('If-None-Match');
|
|
331
|
+
if (clientEtag && clientEtag === etag) {
|
|
332
|
+
// File hasn't changed - return 304 Not Modified
|
|
333
|
+
ctx.status = 304;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check If-Modified-Since header (date validation)
|
|
338
|
+
const clientModifiedSince = ctx.get('If-Modified-Since');
|
|
339
|
+
if (clientModifiedSince) {
|
|
340
|
+
const clientDate = new Date(clientModifiedSince);
|
|
341
|
+
const fileDate = new Date(fileStat.mtime);
|
|
342
|
+
|
|
343
|
+
// Compare timestamps (ignore milliseconds for better compatibility)
|
|
344
|
+
if (fileDate.getTime() <= clientDate.getTime()) {
|
|
345
|
+
// File hasn't been modified - return 304 Not Modified
|
|
346
|
+
ctx.status = 304;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Verify file is still readable (race condition protection)
|
|
209
353
|
try {
|
|
210
354
|
await fs.promises.access(toOpen, fs.constants.R_OK);
|
|
211
355
|
} catch (error) {
|
|
@@ -229,8 +373,9 @@ module.exports = function koaClassicServer(
|
|
|
229
373
|
});
|
|
230
374
|
|
|
231
375
|
ctx.response.set("content-type", mimeType);
|
|
376
|
+
ctx.response.set("content-length", fileStat.size);
|
|
232
377
|
|
|
233
|
-
//
|
|
378
|
+
// Content-Disposition properly quoted with only basename
|
|
234
379
|
const filename = path.basename(toOpen);
|
|
235
380
|
const safeFilename = filename.replace(/"/g, '\\"'); // Escape quotes
|
|
236
381
|
ctx.response.set(
|
|
@@ -241,11 +386,12 @@ module.exports = function koaClassicServer(
|
|
|
241
386
|
ctx.body = src;
|
|
242
387
|
}
|
|
243
388
|
|
|
244
|
-
//
|
|
245
|
-
function show_dir(toOpen) {
|
|
389
|
+
// OPTIMIZATION: show_dir is now async and uses array join instead of string concatenation
|
|
390
|
+
async function show_dir(toOpen) {
|
|
246
391
|
let dir;
|
|
247
392
|
try {
|
|
248
|
-
|
|
393
|
+
// OPTIMIZATION: Use async readdir (non-blocking)
|
|
394
|
+
dir = await fs.promises.readdir(toOpen, { withFileTypes: true });
|
|
249
395
|
} catch (error) {
|
|
250
396
|
console.error('Directory read error:', error);
|
|
251
397
|
return `
|
|
@@ -263,7 +409,10 @@ module.exports = function koaClassicServer(
|
|
|
263
409
|
`;
|
|
264
410
|
}
|
|
265
411
|
|
|
266
|
-
|
|
412
|
+
// OPTIMIZATION: Use array + join instead of string concatenation
|
|
413
|
+
// This reduces memory allocation from O(nΒ²) to O(n)
|
|
414
|
+
const parts = [];
|
|
415
|
+
parts.push("<table>");
|
|
267
416
|
|
|
268
417
|
// Parent directory link
|
|
269
418
|
if (pageHrefOutPrefix.origin + "/" != pageHrefOutPrefix.href) {
|
|
@@ -271,12 +420,11 @@ module.exports = function koaClassicServer(
|
|
|
271
420
|
a_pD.pop();
|
|
272
421
|
const parentDirectory = a_pD.join("/");
|
|
273
422
|
// Escape HTML to prevent XSS
|
|
274
|
-
|
|
423
|
+
parts.push(`<tr><td><a href="${escapeHtml(parentDirectory)}"><b>.. Parent Directory</b></a></td><td>DIR</td></tr>`);
|
|
275
424
|
}
|
|
276
425
|
|
|
277
426
|
if (dir.length == 0) {
|
|
278
|
-
|
|
279
|
-
s_dir += `</table>`;
|
|
427
|
+
parts.push(`<tr><td>empty folder</td><td></td></tr>`);
|
|
280
428
|
} else {
|
|
281
429
|
let a_sy = Object.getOwnPropertySymbols(dir[0]);
|
|
282
430
|
const sy_type = a_sy[0];
|
|
@@ -285,12 +433,13 @@ module.exports = function koaClassicServer(
|
|
|
285
433
|
const s_name = item.name.toString();
|
|
286
434
|
const type = item[sy_type];
|
|
287
435
|
|
|
436
|
+
let rowStart = '';
|
|
288
437
|
if (type == 1) {
|
|
289
438
|
// File
|
|
290
|
-
|
|
439
|
+
rowStart = `<tr><td> FILE `;
|
|
291
440
|
} else if (type == 2 || type == 3) {
|
|
292
441
|
// Directory or symbolic link
|
|
293
|
-
|
|
442
|
+
rowStart = `<tr><td>`;
|
|
294
443
|
} else {
|
|
295
444
|
console.error("Unknown file type:", type);
|
|
296
445
|
continue; // Skip unknown types instead of throwing
|
|
@@ -306,18 +455,21 @@ module.exports = function koaClassicServer(
|
|
|
306
455
|
|
|
307
456
|
// Check if this is a reserved directory
|
|
308
457
|
if (pageHrefOutPrefix.pathname == '/' && options.urlsReserved.includes('/' + s_name) && (type == 2 || type == 3)) {
|
|
309
|
-
|
|
458
|
+
parts.push(`${rowStart} ${escapeHtml(s_name)}</td> <td> DIR BUT RESERVED</td></tr>`);
|
|
310
459
|
} else {
|
|
311
460
|
// Escape HTML to prevent XSS in filenames
|
|
312
461
|
const mimeType = type == 2 ? "DIR" : (mime.lookup(itemPath) || 'unknown');
|
|
313
|
-
|
|
462
|
+
parts.push(`${rowStart} <a href="${escapeHtml(itemUri)}">${escapeHtml(s_name)}</a> </td> <td> ${escapeHtml(mimeType)} </td></tr>`);
|
|
314
463
|
}
|
|
315
464
|
}
|
|
316
465
|
}
|
|
317
466
|
|
|
318
|
-
|
|
467
|
+
parts.push("</table>");
|
|
468
|
+
|
|
469
|
+
// OPTIMIZATION: Single join operation instead of multiple concatenations
|
|
470
|
+
const tableHtml = parts.join('');
|
|
319
471
|
|
|
320
|
-
|
|
472
|
+
const html = `
|
|
321
473
|
<!DOCTYPE html>
|
|
322
474
|
<html>
|
|
323
475
|
<head>
|
|
@@ -327,15 +479,13 @@ module.exports = function koaClassicServer(
|
|
|
327
479
|
<title>Index of ${escapeHtml(pageHrefOutPrefix.pathname)}</title>
|
|
328
480
|
</head>
|
|
329
481
|
<body>
|
|
330
|
-
<h1>Index of ${escapeHtml(pageHrefOutPrefix.pathname)}</h1
|
|
331
|
-
|
|
332
|
-
toReturn += s_dir;
|
|
333
|
-
|
|
334
|
-
toReturn += `
|
|
482
|
+
<h1>Index of ${escapeHtml(pageHrefOutPrefix.pathname)}</h1>
|
|
483
|
+
${tableHtml}
|
|
335
484
|
</body>
|
|
336
485
|
</html>
|
|
337
486
|
`;
|
|
338
|
-
|
|
487
|
+
|
|
488
|
+
return html;
|
|
339
489
|
}
|
|
340
490
|
|
|
341
491
|
// Helper function to escape HTML and prevent XSS
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
testMatch: [
|
|
4
|
+
'**/__tests__/**/*.test.js'
|
|
5
|
+
],
|
|
6
|
+
testTimeout: 120000, // 2 minutes for performance tests
|
|
7
|
+
verbose: true,
|
|
8
|
+
collectCoverageFrom: [
|
|
9
|
+
'index.cjs',
|
|
10
|
+
'index.mjs'
|
|
11
|
+
],
|
|
12
|
+
coveragePathIgnorePatterns: [
|
|
13
|
+
'/node_modules/',
|
|
14
|
+
'/customTest/',
|
|
15
|
+
'/benchmark-data/',
|
|
16
|
+
'/scripts/'
|
|
17
|
+
]
|
|
18
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa-classic-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "High-performance Koa middleware for serving static files with Apache-like directory listing, HTTP caching, template engine support, and comprehensive security fixes",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"exports": {
|
|
7
7
|
"import": "./index.mjs",
|
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
"start": "node index.cjs",
|
|
12
12
|
"test": "jest",
|
|
13
13
|
"test:security": "jest __tests__/security.test.js",
|
|
14
|
+
"test:performance": "jest __tests__/performance.test.js --runInBand",
|
|
15
|
+
"benchmark": "node benchmark.js",
|
|
16
|
+
"benchmark:save": "node benchmark.js --save",
|
|
17
|
+
"benchmark:setup": "node scripts/setup-benchmark.js",
|
|
14
18
|
"loadConfig": "node ./customTest/loadConfig.util.js"
|
|
15
|
-
|
|
16
19
|
},
|
|
17
20
|
"keywords": [
|
|
18
21
|
"koa",
|
|
@@ -27,9 +30,11 @@
|
|
|
27
30
|
"author": "Italo Paesano",
|
|
28
31
|
"license": "MIT",
|
|
29
32
|
"dependencies": {
|
|
30
|
-
"koa": "^
|
|
33
|
+
"koa": "^3.1.1",
|
|
34
|
+
"mime-types": "^2.1.35"
|
|
31
35
|
},
|
|
32
36
|
"devDependencies": {
|
|
37
|
+
"autocannon": "^7.15.0",
|
|
33
38
|
"inquirer": "^12.4.1",
|
|
34
39
|
"jest": "^29.7.0",
|
|
35
40
|
"supertest": "^7.0.0"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Script per pubblicare koa-classic-server v1.2.0 su npm
|
|
3
|
+
|
|
4
|
+
set -e # Exit on error
|
|
5
|
+
|
|
6
|
+
echo "π¦ Publishing koa-classic-server v1.2.0 to npm"
|
|
7
|
+
echo ""
|
|
8
|
+
|
|
9
|
+
# Verifica login
|
|
10
|
+
echo "π Verifying npm login..."
|
|
11
|
+
if ! npm whoami > /dev/null 2>&1; then
|
|
12
|
+
echo "β Not logged in to npm. Please run: npm login"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
echo "β
Logged in as: $(npm whoami)"
|
|
17
|
+
echo ""
|
|
18
|
+
|
|
19
|
+
# Verifica versione
|
|
20
|
+
echo "π Package info:"
|
|
21
|
+
echo " Name: $(node -p "require('./package.json').name")"
|
|
22
|
+
echo " Version: $(node -p "require('./package.json').version")"
|
|
23
|
+
echo ""
|
|
24
|
+
|
|
25
|
+
# Verifica che non ci siano modifiche non committate
|
|
26
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
27
|
+
echo "β οΈ Warning: You have uncommitted changes"
|
|
28
|
+
git status --short
|
|
29
|
+
read -p "Continue anyway? (y/N) " -n 1 -r
|
|
30
|
+
echo
|
|
31
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Dry run per vedere cosa verrΓ pubblicato
|
|
37
|
+
echo "π Files that will be published:"
|
|
38
|
+
npm pack --dry-run | tail -20
|
|
39
|
+
echo ""
|
|
40
|
+
|
|
41
|
+
# Chiedi conferma
|
|
42
|
+
read -p "π Publish to npm? (y/N) " -n 1 -r
|
|
43
|
+
echo
|
|
44
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
45
|
+
echo "β Publish cancelled"
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Pubblica
|
|
50
|
+
echo "π€ Publishing to npm..."
|
|
51
|
+
npm publish
|
|
52
|
+
|
|
53
|
+
if [ $? -eq 0 ]; then
|
|
54
|
+
echo ""
|
|
55
|
+
echo "β
Successfully published koa-classic-server@1.2.0!"
|
|
56
|
+
echo ""
|
|
57
|
+
echo "π View on npm: https://www.npmjs.com/package/koa-classic-server"
|
|
58
|
+
echo ""
|
|
59
|
+
echo "π Users can now install with:"
|
|
60
|
+
echo " npm install koa-classic-server@1.2.0"
|
|
61
|
+
echo " npm install koa-classic-server@latest"
|
|
62
|
+
else
|
|
63
|
+
echo "β Publish failed"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|