kempo-server 1.2.1 → 1.4.1
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/README.md +209 -4
- package/builtinMiddleware.js +136 -0
- package/defaultConfig.js +36 -0
- package/docs/configuration.html +117 -0
- package/docs/examples.html +199 -0
- package/docs/getting-started.html +70 -0
- package/docs/index.html +51 -330
- package/docs/middleware.html +145 -0
- package/docs/request-response.html +93 -0
- package/docs/routing.html +75 -0
- package/example-middleware.js +23 -0
- package/example.config.json +50 -0
- package/middlewareRunner.js +25 -0
- package/package.json +1 -1
- package/router.js +125 -64
package/router.js
CHANGED
|
@@ -5,6 +5,14 @@ import defaultConfig from './defaultConfig.js';
|
|
|
5
5
|
import getFiles from './getFiles.js';
|
|
6
6
|
import findFile from './findFile.js';
|
|
7
7
|
import serveFile from './serveFile.js';
|
|
8
|
+
import MiddlewareRunner from './middlewareRunner.js';
|
|
9
|
+
import {
|
|
10
|
+
corsMiddleware,
|
|
11
|
+
compressionMiddleware,
|
|
12
|
+
rateLimitMiddleware,
|
|
13
|
+
securityMiddleware,
|
|
14
|
+
loggingMiddleware
|
|
15
|
+
} from './builtinMiddleware.js';
|
|
8
16
|
|
|
9
17
|
export default async (flags, log) => {
|
|
10
18
|
log('Initializing router', 2);
|
|
@@ -39,6 +47,57 @@ export default async (flags, log) => {
|
|
|
39
47
|
let files = await getFiles(rootPath, config, log);
|
|
40
48
|
log(`Initial scan found ${files.length} files`, 1);
|
|
41
49
|
|
|
50
|
+
// Initialize middleware runner
|
|
51
|
+
const middlewareRunner = new MiddlewareRunner();
|
|
52
|
+
|
|
53
|
+
// Load built-in middleware based on config
|
|
54
|
+
if (config.middleware?.cors?.enabled) {
|
|
55
|
+
middlewareRunner.use(corsMiddleware(config.middleware.cors));
|
|
56
|
+
log('CORS middleware enabled', 2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (config.middleware?.compression?.enabled) {
|
|
60
|
+
middlewareRunner.use(compressionMiddleware(config.middleware.compression));
|
|
61
|
+
log('Compression middleware enabled', 2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (config.middleware?.rateLimit?.enabled) {
|
|
65
|
+
middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit));
|
|
66
|
+
log('Rate limit middleware enabled', 2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (config.middleware?.security?.enabled) {
|
|
70
|
+
middlewareRunner.use(securityMiddleware(config.middleware.security));
|
|
71
|
+
log('Security middleware enabled', 2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (config.middleware?.logging?.enabled) {
|
|
75
|
+
middlewareRunner.use(loggingMiddleware(config.middleware.logging, log));
|
|
76
|
+
log('Logging middleware enabled', 2);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Load custom middleware files
|
|
80
|
+
if (config.middleware?.custom && config.middleware.custom.length > 0) {
|
|
81
|
+
log(`Loading ${config.middleware.custom.length} custom middleware files`, 2);
|
|
82
|
+
|
|
83
|
+
for (const middlewarePath of config.middleware.custom) {
|
|
84
|
+
try {
|
|
85
|
+
const resolvedPath = path.resolve(middlewarePath);
|
|
86
|
+
const middlewareModule = await import(pathToFileURL(resolvedPath));
|
|
87
|
+
const customMiddleware = middlewareModule.default;
|
|
88
|
+
|
|
89
|
+
if (typeof customMiddleware === 'function') {
|
|
90
|
+
middlewareRunner.use(customMiddleware(config.middleware));
|
|
91
|
+
log(`Custom middleware loaded: ${middlewarePath}`, 2);
|
|
92
|
+
} else {
|
|
93
|
+
log(`Custom middleware error: ${middlewarePath} does not export a default function`, 1);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
log(`Custom middleware error for ${middlewarePath}: ${error.message}`, 1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
42
101
|
// Process custom routes - resolve paths and validate files exist
|
|
43
102
|
const customRoutes = new Map();
|
|
44
103
|
const wildcardRoutes = new Map();
|
|
@@ -146,80 +205,82 @@ export default async (flags, log) => {
|
|
|
146
205
|
};
|
|
147
206
|
|
|
148
207
|
return async (req, res) => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// Check custom routes first
|
|
153
|
-
if (customRoutes.has(requestPath)) {
|
|
154
|
-
const customFilePath = customRoutes.get(requestPath);
|
|
155
|
-
log(`Serving custom route: ${requestPath} -> ${customFilePath}`, 2);
|
|
208
|
+
await middlewareRunner.run(req, res, async () => {
|
|
209
|
+
const requestPath = req.url.split('?')[0];
|
|
210
|
+
log(`${req.method} ${requestPath}`, 0);
|
|
156
211
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
212
|
+
// Check custom routes first
|
|
213
|
+
if (customRoutes.has(requestPath)) {
|
|
214
|
+
const customFilePath = customRoutes.get(requestPath);
|
|
215
|
+
log(`Serving custom route: ${requestPath} -> ${customFilePath}`, 2);
|
|
161
216
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
217
|
+
try {
|
|
218
|
+
const fileContent = await readFile(customFilePath);
|
|
219
|
+
const fileExtension = path.extname(customFilePath).toLowerCase().slice(1);
|
|
220
|
+
const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
|
|
221
|
+
|
|
222
|
+
log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`, 2);
|
|
223
|
+
res.writeHead(200, { 'Content-Type': mimeType });
|
|
224
|
+
res.end(fileContent);
|
|
225
|
+
return; // Successfully served custom route
|
|
226
|
+
} catch (error) {
|
|
227
|
+
log(`Error serving custom route ${requestPath}: ${error.message}`, 0);
|
|
228
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
229
|
+
res.end('Internal Server Error');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
171
232
|
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Check wildcard routes
|
|
175
|
-
const wildcardMatch = findWildcardRoute(requestPath);
|
|
176
|
-
if (wildcardMatch) {
|
|
177
|
-
const resolvedFilePath = resolveWildcardPath(wildcardMatch.filePath, wildcardMatch.matches);
|
|
178
|
-
log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`, 2);
|
|
179
233
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
234
|
+
// Check wildcard routes
|
|
235
|
+
const wildcardMatch = findWildcardRoute(requestPath);
|
|
236
|
+
if (wildcardMatch) {
|
|
237
|
+
const resolvedFilePath = resolveWildcardPath(wildcardMatch.filePath, wildcardMatch.matches);
|
|
238
|
+
log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`, 2);
|
|
184
239
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
240
|
+
try {
|
|
241
|
+
const fileContent = await readFile(resolvedFilePath);
|
|
242
|
+
const fileExtension = path.extname(resolvedFilePath).toLowerCase().slice(1);
|
|
243
|
+
const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
|
|
244
|
+
|
|
245
|
+
log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`, 2);
|
|
246
|
+
res.writeHead(200, { 'Content-Type': mimeType });
|
|
247
|
+
res.end(fileContent);
|
|
248
|
+
return; // Successfully served wildcard route
|
|
249
|
+
} catch (error) {
|
|
250
|
+
log(`Error serving wildcard route ${requestPath}: ${error.message}`, 0);
|
|
251
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
252
|
+
res.end('Internal Server Error');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
194
255
|
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Try to serve the file normally
|
|
198
|
-
const served = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
|
|
199
|
-
|
|
200
|
-
// If not served and scan flag is enabled, try rescanning once (with blacklist check)
|
|
201
|
-
if (!served && flags.scan && !shouldSkipRescan(requestPath)) {
|
|
202
|
-
trackRescanAttempt(requestPath);
|
|
203
|
-
log('File not found, rescanning directory...', 1);
|
|
204
|
-
files = await getFiles(rootPath, config, log);
|
|
205
|
-
log(`Rescan found ${files.length} files`, 2);
|
|
206
256
|
|
|
207
|
-
// Try to serve
|
|
208
|
-
const
|
|
257
|
+
// Try to serve the file normally
|
|
258
|
+
const served = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
|
|
209
259
|
|
|
210
|
-
|
|
211
|
-
|
|
260
|
+
// If not served and scan flag is enabled, try rescanning once (with blacklist check)
|
|
261
|
+
if (!served && flags.scan && !shouldSkipRescan(requestPath)) {
|
|
262
|
+
trackRescanAttempt(requestPath);
|
|
263
|
+
log('File not found, rescanning directory...', 1);
|
|
264
|
+
files = await getFiles(rootPath, config, log);
|
|
265
|
+
log(`Rescan found ${files.length} files`, 2);
|
|
266
|
+
|
|
267
|
+
// Try to serve again after rescan
|
|
268
|
+
const reserved = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
|
|
269
|
+
|
|
270
|
+
if (!reserved) {
|
|
271
|
+
log(`404 - File not found after rescan: ${requestPath}`, 1);
|
|
272
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
273
|
+
res.end('Not Found');
|
|
274
|
+
}
|
|
275
|
+
} else if (!served) {
|
|
276
|
+
if (shouldSkipRescan(requestPath)) {
|
|
277
|
+
log(`404 - Skipped rescan for: ${requestPath}`, 2);
|
|
278
|
+
} else {
|
|
279
|
+
log(`404 - File not found: ${requestPath}`, 1);
|
|
280
|
+
}
|
|
212
281
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
213
282
|
res.end('Not Found');
|
|
214
283
|
}
|
|
215
|
-
}
|
|
216
|
-
if (shouldSkipRescan(requestPath)) {
|
|
217
|
-
log(`404 - Skipped rescan for: ${requestPath}`, 2);
|
|
218
|
-
} else {
|
|
219
|
-
log(`404 - File not found: ${requestPath}`, 1);
|
|
220
|
-
}
|
|
221
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
222
|
-
res.end('Not Found');
|
|
223
|
-
}
|
|
284
|
+
});
|
|
224
285
|
}
|
|
225
286
|
}
|