koa-classic-server 1.1.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/CHANGELOG.md +181 -0
- package/CREATE_RELEASE.sh +53 -0
- package/DEBUG_REPORT.md +593 -0
- package/DOCUMENTATION.md +1585 -0
- package/EXAMPLES_INDEX_OPTION.md +395 -0
- package/INDEX_OPTION_PRIORITY.md +527 -0
- package/LICENSE +21 -0
- package/OPTIMIZATION_HTTP_CACHING.md +687 -0
- package/PERFORMANCE_ANALYSIS.md +839 -0
- package/PERFORMANCE_COMPARISON.md +388 -0
- package/README.md +278 -103
- package/__tests__/index-option.test.js +447 -0
- package/__tests__/index.test.js +15 -11
- package/__tests__/performance.test.js +301 -0
- package/__tests__/publicWwwTest/cartella vuota con spazi nel nome/file con spazio nel nome .txt +1 -0
- package/__tests__/security.test.js +336 -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 +386 -156
- package/jest.config.js +18 -0
- package/package.json +18 -5
- package/publish-to-npm.sh +65 -0
- package/scripts/setup-benchmark.js +178 -0
- package/test-regex-quick.js +158 -0
package/index.cjs
CHANGED
|
@@ -1,140 +1,265 @@
|
|
|
1
1
|
|
|
2
2
|
const { URL } = require("url");
|
|
3
3
|
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
4
5
|
const mime = require("mime-types");
|
|
5
|
-
const { throws } = require("assert");
|
|
6
|
-
const { error } = require("console");
|
|
7
6
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
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):
|
|
16
|
+
// - Path Traversal vulnerability protection
|
|
17
|
+
// - Status code 404 properly set
|
|
18
|
+
// - Template rendering error handling
|
|
19
|
+
// - Race condition file access protection
|
|
20
|
+
// - Proper file extension extraction
|
|
21
|
+
// - fs.readdir error handling
|
|
22
|
+
// - Content-Disposition properly quoted
|
|
23
|
+
// - XSS protection in directory listing
|
|
24
|
+
|
|
25
|
+
module.exports = function koaClassicServer(
|
|
11
26
|
rootDir,
|
|
12
27
|
opts = {}
|
|
13
|
-
|
|
14
28
|
/*
|
|
15
|
-
opts
|
|
29
|
+
opts STRUCTURE
|
|
16
30
|
opts = {
|
|
17
|
-
method:
|
|
18
|
-
showDirContents: true, //
|
|
19
|
-
index: "", //
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
method: ['GET'], // Supported methods, otherwise next() will be called
|
|
32
|
+
showDirContents: true, // Show or hide directory contents
|
|
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.
|
|
41
|
+
urlPrefix: "", // URL path prefix
|
|
42
|
+
urlsReserved: [], // Reserved paths (first level only)
|
|
24
43
|
template: {
|
|
25
|
-
render: undefined, //
|
|
26
|
-
ext:
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
|
|
44
|
+
render: undefined, // Template rendering function: async (ctx, next, filePath) => {}
|
|
45
|
+
ext: [], // File extensions to process with template.render
|
|
46
|
+
},
|
|
47
|
+
cacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 1 hour)
|
|
48
|
+
enableCaching: true, // Enable HTTP caching headers (ETag, Last-Modified)
|
|
49
|
+
}
|
|
50
|
+
*/
|
|
51
|
+
) {
|
|
52
|
+
// Validate rootDir
|
|
53
|
+
if (!rootDir || typeof rootDir !== 'string') {
|
|
54
|
+
throw new TypeError('rootDir must be a non-empty string');
|
|
55
|
+
}
|
|
56
|
+
if (!path.isAbsolute(rootDir)) {
|
|
57
|
+
throw new Error('rootDir must be an absolute path');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Normalize rootDir to prevent issues
|
|
61
|
+
const normalizedRootDir = path.resolve(rootDir);
|
|
62
|
+
|
|
63
|
+
// Set default options
|
|
31
64
|
const options = opts || {};
|
|
32
|
-
options.template = opts.template || {
|
|
65
|
+
options.template = opts.template || {};
|
|
66
|
+
|
|
67
|
+
options.method = Array.isArray(options.method) ? options.method : ['GET'];
|
|
68
|
+
options.showDirContents = typeof options.showDirContents == 'boolean' ? options.showDirContents : true;
|
|
33
69
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
}
|
|
41
93
|
|
|
94
|
+
options.urlPrefix = typeof options.urlPrefix == 'string' ? options.urlPrefix : "";
|
|
95
|
+
options.urlsReserved = Array.isArray(options.urlsReserved) ? options.urlsReserved : [];
|
|
96
|
+
options.template.render = (options.template.render == undefined || typeof options.template.render == 'function') ? options.template.render : undefined;
|
|
97
|
+
options.template.ext = Array.isArray(options.template.ext) ? options.template.ext : [];
|
|
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;
|
|
42
102
|
|
|
43
103
|
return async (ctx, next) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// controlla se il metodo richiesto è presente nella lista di quelli ammessi
|
|
104
|
+
// Check if method is allowed
|
|
47
105
|
if (!options.method.includes(ctx.method)) {
|
|
48
|
-
next();
|
|
106
|
+
await next();
|
|
49
107
|
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}else{
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Normalize URL (remove trailing slash)
|
|
111
|
+
let pageHref = '';
|
|
112
|
+
if (ctx.href.charAt(ctx.href.length - 1) == '/') {
|
|
113
|
+
pageHref = new URL(ctx.href.slice(0, -1));
|
|
114
|
+
} else {
|
|
58
115
|
pageHref = new URL(ctx.href);
|
|
59
116
|
}
|
|
60
|
-
|
|
61
|
-
//console.log( "rootDir="+rootDir+" UrlPrefix="+options.urlPrefix+" pageHref.pathname="+pageHref.pathname );
|
|
62
117
|
|
|
63
|
-
//
|
|
64
|
-
const a_pathname = pageHref.pathname.split("/")
|
|
118
|
+
// Check URL prefix
|
|
119
|
+
const a_pathname = pageHref.pathname.split("/");
|
|
65
120
|
const a_urlPrefix = options.urlPrefix.split("/");
|
|
66
121
|
|
|
67
|
-
//controllo urlPrefix
|
|
68
122
|
for (const key in a_urlPrefix) {
|
|
69
123
|
if (a_urlPrefix[key] != a_pathname[key]) {
|
|
70
|
-
next();
|
|
124
|
+
await next();
|
|
71
125
|
return;
|
|
72
126
|
}
|
|
73
127
|
}
|
|
74
|
-
// superato questotolgo tutti gli urlprefix dai PageHref
|
|
75
128
|
|
|
76
|
-
//
|
|
129
|
+
// Create pageHrefOutPrefix without URL prefix
|
|
77
130
|
let pageHrefOutPrefix = pageHref;
|
|
78
|
-
if (options.urlPrefix != "") {
|
|
79
|
-
let
|
|
80
|
-
let
|
|
81
|
-
let hrefOutPrefix = pageHref.origin + '/' +
|
|
82
|
-
pageHrefOutPrefix = new URL(hrefOutPrefix)
|
|
131
|
+
if (options.urlPrefix != "") {
|
|
132
|
+
let a_pathnameOutPrefix = a_pathname.slice(a_urlPrefix.length);
|
|
133
|
+
let s_pathnameOutPrefix = a_pathnameOutPrefix.join("/");
|
|
134
|
+
let hrefOutPrefix = pageHref.origin + '/' + s_pathnameOutPrefix;
|
|
135
|
+
pageHrefOutPrefix = new URL(hrefOutPrefix);
|
|
83
136
|
}
|
|
84
137
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
if (Array.isArray(options.urlsReserved)) {
|
|
138
|
+
// Check reserved URLs (first level only)
|
|
139
|
+
if (Array.isArray(options.urlsReserved) && options.urlsReserved.length > 0) {
|
|
88
140
|
const a_pathnameOutPrefix = pageHrefOutPrefix.pathname.split("/");
|
|
89
141
|
for (const value of options.urlsReserved) {
|
|
90
142
|
if (a_pathnameOutPrefix[1] == value.substring(1)) {
|
|
91
|
-
|
|
92
|
-
next();
|
|
143
|
+
await next();
|
|
93
144
|
return;
|
|
94
145
|
}
|
|
95
146
|
}
|
|
96
147
|
}
|
|
97
|
-
//controllo urlReserved
|
|
98
148
|
|
|
99
|
-
//
|
|
100
|
-
|
|
149
|
+
// Path Traversal Protection
|
|
150
|
+
// Construct safe file path
|
|
151
|
+
let requestedPath = "";
|
|
101
152
|
if (pageHrefOutPrefix.pathname == "/") {
|
|
102
|
-
|
|
153
|
+
requestedPath = "";
|
|
103
154
|
} else {
|
|
104
|
-
|
|
155
|
+
requestedPath = decodeURIComponent(pageHrefOutPrefix.pathname);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Normalize path and prevent path traversal
|
|
159
|
+
const normalizedPath = path.normalize(requestedPath);
|
|
160
|
+
const fullPath = path.join(normalizedRootDir, normalizedPath);
|
|
161
|
+
|
|
162
|
+
// Security check: ensure resolved path is within rootDir
|
|
163
|
+
if (!fullPath.startsWith(normalizedRootDir)) {
|
|
164
|
+
ctx.status = 403;
|
|
165
|
+
ctx.body = 'Forbidden';
|
|
166
|
+
return;
|
|
105
167
|
}
|
|
106
168
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
169
|
+
let toOpen = fullPath;
|
|
170
|
+
|
|
171
|
+
// OPTIMIZATION: Check if file/directory exists (async, non-blocking)
|
|
172
|
+
let stat;
|
|
173
|
+
try {
|
|
174
|
+
stat = await fs.promises.stat(toOpen);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// File/directory doesn't exist or can't be accessed
|
|
177
|
+
ctx.status = 404;
|
|
110
178
|
ctx.body = requestedUrlNotFound();
|
|
111
179
|
return;
|
|
112
180
|
}
|
|
113
|
-
|
|
114
|
-
let dir = ""; //solo segnaposto da migliorare
|
|
181
|
+
|
|
115
182
|
if (stat.isDirectory()) {
|
|
116
|
-
//
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
183
|
+
// Handle directory
|
|
184
|
+
if (options.showDirContents) {
|
|
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);
|
|
122
191
|
return;
|
|
123
192
|
}
|
|
124
193
|
}
|
|
125
|
-
|
|
194
|
+
|
|
195
|
+
// No index file found, show directory listing
|
|
196
|
+
ctx.body = await show_dir(toOpen);
|
|
126
197
|
} else {
|
|
127
|
-
//
|
|
198
|
+
// Directory listing disabled
|
|
199
|
+
ctx.status = 404;
|
|
128
200
|
ctx.body = requestedUrlNotFound();
|
|
129
201
|
}
|
|
130
202
|
return;
|
|
131
203
|
} else {
|
|
132
|
-
//
|
|
133
|
-
loadFile(toOpen);
|
|
204
|
+
// Handle file
|
|
205
|
+
await loadFile(toOpen, stat);
|
|
134
206
|
return;
|
|
135
207
|
}
|
|
136
208
|
|
|
137
|
-
//
|
|
209
|
+
// Internal functions
|
|
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
|
+
}
|
|
138
263
|
|
|
139
264
|
function requestedUrlNotFound() {
|
|
140
265
|
return `
|
|
@@ -142,133 +267,238 @@ module.exports = function koaClassicServer(
|
|
|
142
267
|
<html>
|
|
143
268
|
<head>
|
|
144
269
|
<meta charset="UTF-8">
|
|
145
|
-
<meta http-equiv="X-UA-Compatible">
|
|
270
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
146
271
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
147
272
|
<title>URL not found</title>
|
|
148
273
|
</head>
|
|
149
274
|
<body>
|
|
150
275
|
<h1>Not Found</h1>
|
|
151
|
-
|
|
152
276
|
<h3>The requested URL was not found on this server.</h3>
|
|
153
|
-
|
|
154
277
|
</body>
|
|
155
278
|
</html>
|
|
156
279
|
`;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
280
|
+
}
|
|
281
|
+
|
|
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
|
|
297
|
+
if (options.template.ext.length > 0 && options.template.render) {
|
|
298
|
+
const fileExt = path.extname(toOpen).slice(1); // Remove leading dot
|
|
299
|
+
|
|
300
|
+
if (fileExt && options.template.ext.includes(fileExt)) {
|
|
301
|
+
try {
|
|
302
|
+
await options.template.render(ctx, next, toOpen);
|
|
303
|
+
return;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('Template rendering error:', error);
|
|
306
|
+
ctx.status = 500;
|
|
307
|
+
ctx.body = 'Internal Server Error - Template Rendering Failed';
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
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;
|
|
168
334
|
return;
|
|
169
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)
|
|
353
|
+
try {
|
|
354
|
+
await fs.promises.access(toOpen, fs.constants.R_OK);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error('File access error:', error);
|
|
357
|
+
ctx.status = 404;
|
|
358
|
+
ctx.body = requestedUrlNotFound();
|
|
359
|
+
return;
|
|
170
360
|
}
|
|
361
|
+
|
|
362
|
+
// Serve static file
|
|
171
363
|
let mimeType = mime.lookup(toOpen);
|
|
172
364
|
const src = fs.createReadStream(toOpen);
|
|
365
|
+
|
|
366
|
+
// Handle stream errors
|
|
367
|
+
src.on('error', (err) => {
|
|
368
|
+
console.error('Stream error:', err);
|
|
369
|
+
if (!ctx.headerSent) {
|
|
370
|
+
ctx.status = 500;
|
|
371
|
+
ctx.body = 'Error reading file';
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
173
375
|
ctx.response.set("content-type", mimeType);
|
|
376
|
+
ctx.response.set("content-length", fileStat.size);
|
|
377
|
+
|
|
378
|
+
// Content-Disposition properly quoted with only basename
|
|
379
|
+
const filename = path.basename(toOpen);
|
|
380
|
+
const safeFilename = filename.replace(/"/g, '\\"'); // Escape quotes
|
|
174
381
|
ctx.response.set(
|
|
175
382
|
"content-disposition",
|
|
176
|
-
`inline; filename
|
|
177
|
-
);
|
|
383
|
+
`inline; filename="${safeFilename}"`
|
|
384
|
+
);
|
|
385
|
+
|
|
178
386
|
ctx.body = src;
|
|
179
387
|
}
|
|
180
388
|
|
|
181
|
-
//
|
|
182
|
-
function show_dir(toOpen) {
|
|
183
|
-
dir
|
|
184
|
-
|
|
389
|
+
// OPTIMIZATION: show_dir is now async and uses array join instead of string concatenation
|
|
390
|
+
async function show_dir(toOpen) {
|
|
391
|
+
let dir;
|
|
392
|
+
try {
|
|
393
|
+
// OPTIMIZATION: Use async readdir (non-blocking)
|
|
394
|
+
dir = await fs.promises.readdir(toOpen, { withFileTypes: true });
|
|
395
|
+
} catch (error) {
|
|
396
|
+
console.error('Directory read error:', error);
|
|
397
|
+
return `
|
|
398
|
+
<!DOCTYPE html>
|
|
399
|
+
<html>
|
|
400
|
+
<head>
|
|
401
|
+
<meta charset="UTF-8">
|
|
402
|
+
<title>Error</title>
|
|
403
|
+
</head>
|
|
404
|
+
<body>
|
|
405
|
+
<h1>Error Reading Directory</h1>
|
|
406
|
+
<p>Unable to access directory contents.</p>
|
|
407
|
+
</body>
|
|
408
|
+
</html>
|
|
409
|
+
`;
|
|
410
|
+
}
|
|
185
411
|
|
|
186
|
-
//
|
|
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>");
|
|
416
|
+
|
|
417
|
+
// Parent directory link
|
|
187
418
|
if (pageHrefOutPrefix.origin + "/" != pageHrefOutPrefix.href) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
a_pD.pop(); // rimuovo l'ultimo elemento per trasormarla dell parent directory
|
|
419
|
+
const a_pD = pageHref.href.split("/");
|
|
420
|
+
a_pD.pop();
|
|
191
421
|
const parentDirectory = a_pD.join("/");
|
|
192
|
-
|
|
422
|
+
// Escape HTML to prevent XSS
|
|
423
|
+
parts.push(`<tr><td><a href="${escapeHtml(parentDirectory)}"><b>.. Parent Directory</b></a></td><td>DIR</td></tr>`);
|
|
193
424
|
}
|
|
194
|
-
// END PARENT directory
|
|
195
425
|
|
|
196
426
|
if (dir.length == 0) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
s_dir += `<tr><td>empty folder</td><td></td></tr>`;
|
|
200
|
-
s_dir += `</table>`;
|
|
427
|
+
parts.push(`<tr><td>empty folder</td><td></td></tr>`);
|
|
201
428
|
} else {
|
|
202
|
-
|
|
429
|
+
let a_sy = Object.getOwnPropertySymbols(dir[0]);
|
|
430
|
+
const sy_type = a_sy[0];
|
|
203
431
|
|
|
204
|
-
let a_sy = Object.getOwnPropertySymbols(dir[0]); // recupero l'array dei symbol
|
|
205
|
-
const sy_type = a_sy[0]; // recupero il symbol Symbol(type)
|
|
206
|
-
//let test = sy_type.description; test == 'type
|
|
207
432
|
for (const item of dir) {
|
|
208
433
|
const s_name = item.name.toString();
|
|
209
434
|
const type = item[sy_type];
|
|
210
435
|
|
|
211
|
-
|
|
436
|
+
let rowStart = '';
|
|
212
437
|
if (type == 1) {
|
|
213
|
-
//
|
|
214
|
-
|
|
438
|
+
// File
|
|
439
|
+
rowStart = `<tr><td> FILE `;
|
|
215
440
|
} else if (type == 2 || type == 3) {
|
|
216
|
-
//
|
|
217
|
-
|
|
441
|
+
// Directory or symbolic link
|
|
442
|
+
rowStart = `<tr><td>`;
|
|
218
443
|
} else {
|
|
219
|
-
|
|
220
|
-
|
|
444
|
+
console.error("Unknown file type:", type);
|
|
445
|
+
continue; // Skip unknown types instead of throwing
|
|
221
446
|
}
|
|
222
447
|
|
|
223
|
-
const itemPath =
|
|
448
|
+
const itemPath = path.join(toOpen, s_name);
|
|
224
449
|
let itemUri = "";
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
itemUri = `${
|
|
228
|
-
pageHref.origin + options.urlPrefix
|
|
229
|
-
}/${encodeURIComponent(s_name)}`;
|
|
450
|
+
if (pageHref.href == pageHref.origin + options.urlPrefix + "/") {
|
|
451
|
+
itemUri = `${pageHref.origin + options.urlPrefix}/${encodeURIComponent(s_name)}`;
|
|
230
452
|
} else {
|
|
231
|
-
itemUri = `${pageHref.href}/${encodeURIComponent(
|
|
232
|
-
s_name
|
|
233
|
-
)}`;
|
|
234
|
-
//in questo caso non mi trovo nella root ed
|
|
453
|
+
itemUri = `${pageHref.href}/${encodeURIComponent(s_name)}`;
|
|
235
454
|
}
|
|
236
455
|
|
|
237
|
-
//
|
|
238
|
-
if(pageHrefOutPrefix.pathname == '/' &&
|
|
239
|
-
|
|
240
|
-
}else{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
: ( mime.lookup(itemPath) == false ) ? 'unknow' : mime.lookup(itemPath)
|
|
245
|
-
} </td></tr>`;
|
|
456
|
+
// Check if this is a reserved directory
|
|
457
|
+
if (pageHrefOutPrefix.pathname == '/' && options.urlsReserved.includes('/' + s_name) && (type == 2 || type == 3)) {
|
|
458
|
+
parts.push(`${rowStart} ${escapeHtml(s_name)}</td> <td> DIR BUT RESERVED</td></tr>`);
|
|
459
|
+
} else {
|
|
460
|
+
// Escape HTML to prevent XSS in filenames
|
|
461
|
+
const mimeType = type == 2 ? "DIR" : (mime.lookup(itemPath) || 'unknown');
|
|
462
|
+
parts.push(`${rowStart} <a href="${escapeHtml(itemUri)}">${escapeHtml(s_name)}</a> </td> <td> ${escapeHtml(mimeType)} </td></tr>`);
|
|
246
463
|
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
249
466
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
467
|
+
parts.push("</table>");
|
|
468
|
+
|
|
469
|
+
// OPTIMIZATION: Single join operation instead of multiple concatenations
|
|
470
|
+
const tableHtml = parts.join('');
|
|
471
|
+
|
|
472
|
+
const html = `
|
|
253
473
|
<!DOCTYPE html>
|
|
254
474
|
<html>
|
|
255
475
|
<head>
|
|
256
476
|
<meta charset="UTF-8">
|
|
257
|
-
<meta http-equiv="X-UA-Compatible">
|
|
477
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
258
478
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
259
|
-
<title>
|
|
479
|
+
<title>Index of ${escapeHtml(pageHrefOutPrefix.pathname)}</title>
|
|
260
480
|
</head>
|
|
261
|
-
<body
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// for test
|
|
265
|
-
//toReturn += s_dir + " \n <br> rootDir="+rootDir+" UrlPrefix="+options.urlPrefix+" pageHref.pathname="+pageHref.pathname ;
|
|
266
|
-
|
|
267
|
-
toReturn += `
|
|
481
|
+
<body>
|
|
482
|
+
<h1>Index of ${escapeHtml(pageHrefOutPrefix.pathname)}</h1>
|
|
483
|
+
${tableHtml}
|
|
268
484
|
</body>
|
|
269
485
|
</html>
|
|
270
486
|
`;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
487
|
+
|
|
488
|
+
return html;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Helper function to escape HTML and prevent XSS
|
|
492
|
+
function escapeHtml(unsafe) {
|
|
493
|
+
if (typeof unsafe !== 'string') {
|
|
494
|
+
return unsafe;
|
|
495
|
+
}
|
|
496
|
+
return unsafe
|
|
497
|
+
.replace(/&/g, "&")
|
|
498
|
+
.replace(/</g, "<")
|
|
499
|
+
.replace(/>/g, ">")
|
|
500
|
+
.replace(/"/g, """)
|
|
501
|
+
.replace(/'/g, "'");
|
|
502
|
+
}
|
|
503
|
+
};
|
|
274
504
|
};
|