kempo-server 1.2.1 → 1.3.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.
@@ -0,0 +1,136 @@
1
+ // Built-in middleware functions for Kempo Server
2
+ import zlib from 'zlib';
3
+
4
+ // CORS Middleware
5
+ export const corsMiddleware = (config) => {
6
+ return async (req, res, next) => {
7
+ const origin = req.headers.origin;
8
+ const allowedOrigins = Array.isArray(config.origin) ? config.origin : [config.origin];
9
+
10
+ if (config.origin === '*' || allowedOrigins.includes(origin)) {
11
+ res.setHeader('Access-Control-Allow-Origin', origin || '*');
12
+ }
13
+
14
+ res.setHeader('Access-Control-Allow-Methods', config.methods.join(', '));
15
+ res.setHeader('Access-Control-Allow-Headers', config.headers.join(', '));
16
+
17
+ // Handle preflight requests
18
+ if (req.method === 'OPTIONS') {
19
+ res.writeHead(200);
20
+ res.end();
21
+ return;
22
+ }
23
+
24
+ await next();
25
+ };
26
+ };
27
+
28
+ // Compression Middleware
29
+ export const compressionMiddleware = (config) => {
30
+ return async (req, res, next) => {
31
+ const acceptEncoding = req.headers['accept-encoding'] || '';
32
+
33
+ if (!acceptEncoding.includes('gzip')) {
34
+ return await next();
35
+ }
36
+
37
+ const originalEnd = res.end;
38
+ const originalWrite = res.write;
39
+ const chunks = [];
40
+
41
+ res.write = function(chunk) {
42
+ if (chunk) chunks.push(Buffer.from(chunk));
43
+ return true;
44
+ };
45
+
46
+ res.end = function(chunk) {
47
+ if (chunk) chunks.push(Buffer.from(chunk));
48
+
49
+ const buffer = Buffer.concat(chunks);
50
+
51
+ // Only compress if above threshold
52
+ if (buffer.length >= config.threshold) {
53
+ zlib.gzip(buffer, (err, compressed) => {
54
+ if (!err && compressed.length < buffer.length) {
55
+ res.setHeader('Content-Encoding', 'gzip');
56
+ res.setHeader('Content-Length', compressed.length);
57
+ originalEnd.call(res, compressed);
58
+ } else {
59
+ originalEnd.call(res, buffer);
60
+ }
61
+ });
62
+ } else {
63
+ originalEnd.call(res, buffer);
64
+ }
65
+ };
66
+
67
+ await next();
68
+ };
69
+ };
70
+
71
+ // Rate Limiting Middleware
72
+ export const rateLimitMiddleware = (config) => {
73
+ const requestCounts = new Map();
74
+
75
+ return async (req, res, next) => {
76
+ const clientId = req.socket.remoteAddress;
77
+ const now = Date.now();
78
+ const windowStart = now - config.windowMs;
79
+
80
+ if (!requestCounts.has(clientId)) {
81
+ requestCounts.set(clientId, []);
82
+ }
83
+
84
+ const requests = requestCounts.get(clientId);
85
+ const recentRequests = requests.filter(time => time > windowStart);
86
+
87
+ if (recentRequests.length >= config.maxRequests) {
88
+ res.writeHead(429, { 'Content-Type': 'text/plain' });
89
+ res.end(config.message);
90
+ return;
91
+ }
92
+
93
+ recentRequests.push(now);
94
+ requestCounts.set(clientId, recentRequests);
95
+
96
+ await next();
97
+ };
98
+ };
99
+
100
+ // Security Headers Middleware
101
+ export const securityMiddleware = (config) => {
102
+ return async (req, res, next) => {
103
+ for (const [header, value] of Object.entries(config.headers)) {
104
+ res.setHeader(header, value);
105
+ }
106
+ await next();
107
+ };
108
+ };
109
+
110
+ // Logging Middleware
111
+ export const loggingMiddleware = (config, log) => {
112
+ return async (req, res, next) => {
113
+ const startTime = Date.now();
114
+ const userAgent = config.includeUserAgent ? req.headers['user-agent'] : '';
115
+
116
+ // Store original end to capture response
117
+ const originalEnd = res.end;
118
+ res.end = function(...args) {
119
+ const responseTime = Date.now() - startTime;
120
+ let logMessage = `${req.method} ${req.url}`;
121
+
122
+ if (config.includeResponseTime) {
123
+ logMessage += ` - ${responseTime}ms`;
124
+ }
125
+
126
+ if (config.includeUserAgent && userAgent) {
127
+ logMessage += ` - ${userAgent}`;
128
+ }
129
+
130
+ log(logMessage, 1);
131
+ originalEnd.apply(res, args);
132
+ };
133
+
134
+ await next();
135
+ };
136
+ };
package/defaultConfig.js CHANGED
@@ -90,5 +90,41 @@ export default {
90
90
  customRoutes: {
91
91
  // Example: "/vendor/bootstrap.css": "./node_modules/bootstrap/dist/css/bootstrap.min.css"
92
92
  // Wildcard example: "kempo/*": "./node_modules/kempo/dust/*"
93
+ },
94
+ middleware: {
95
+ // Built-in middleware configuration
96
+ cors: {
97
+ enabled: false,
98
+ origin: "*",
99
+ methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
100
+ headers: ["Content-Type", "Authorization"]
101
+ },
102
+ compression: {
103
+ enabled: false,
104
+ threshold: 1024 // Only compress files larger than 1KB
105
+ },
106
+ rateLimit: {
107
+ enabled: false,
108
+ maxRequests: 100,
109
+ windowMs: 60000, // 1 minute
110
+ message: "Too many requests"
111
+ },
112
+ security: {
113
+ enabled: true,
114
+ headers: {
115
+ "X-Content-Type-Options": "nosniff",
116
+ "X-Frame-Options": "DENY",
117
+ "X-XSS-Protection": "1; mode=block"
118
+ }
119
+ },
120
+ logging: {
121
+ enabled: true,
122
+ includeUserAgent: false,
123
+ includeResponseTime: true
124
+ },
125
+ // Custom middleware files
126
+ custom: [
127
+ // Example: "./middleware/auth.js"
128
+ ]
93
129
  }
94
130
  }
@@ -0,0 +1,23 @@
1
+ // Example custom middleware file
2
+ // This would be placed in your project directory and referenced in config
3
+
4
+ export default async function authMiddleware(req, res, next) {
5
+ // Example: Check for API key in headers
6
+ const apiKey = req.headers['x-api-key'];
7
+
8
+ // Skip auth for public routes
9
+ if (req.url.startsWith('/public/')) {
10
+ return await next();
11
+ }
12
+
13
+ if (!apiKey || apiKey !== process.env.API_KEY) {
14
+ res.writeHead(401, { 'Content-Type': 'application/json' });
15
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
16
+ return;
17
+ }
18
+
19
+ // Add user info to request for downstream use
20
+ req.user = { authenticated: true, apiKey };
21
+
22
+ await next();
23
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "allowedMimes": {
3
+ "html": "text/html",
4
+ "css": "text/css",
5
+ "js": "application/javascript",
6
+ "json": "application/json",
7
+ "png": "image/png",
8
+ "jpg": "image/jpeg"
9
+ },
10
+ "middleware": {
11
+ "cors": {
12
+ "enabled": true,
13
+ "origin": ["http://localhost:3000", "https://mydomain.com"],
14
+ "methods": ["GET", "POST", "PUT", "DELETE"],
15
+ "headers": ["Content-Type", "Authorization", "X-API-Key"]
16
+ },
17
+ "compression": {
18
+ "enabled": true,
19
+ "threshold": 512
20
+ },
21
+ "rateLimit": {
22
+ "enabled": true,
23
+ "maxRequests": 50,
24
+ "windowMs": 60000,
25
+ "message": "Rate limit exceeded. Please try again later."
26
+ },
27
+ "security": {
28
+ "enabled": true,
29
+ "headers": {
30
+ "X-Content-Type-Options": "nosniff",
31
+ "X-Frame-Options": "SAMEORIGIN",
32
+ "X-XSS-Protection": "1; mode=block",
33
+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains"
34
+ }
35
+ },
36
+ "logging": {
37
+ "enabled": true,
38
+ "includeUserAgent": true,
39
+ "includeResponseTime": true
40
+ },
41
+ "custom": [
42
+ "./middleware/auth.js",
43
+ "./middleware/analytics.js"
44
+ ]
45
+ },
46
+ "customRoutes": {
47
+ "api/*": "./api-handlers/*",
48
+ "/vendor/bootstrap.css": "./node_modules/bootstrap/dist/css/bootstrap.min.css"
49
+ }
50
+ }
@@ -0,0 +1,25 @@
1
+ // Middleware runner for Kempo Server
2
+ export default class MiddlewareRunner {
3
+ constructor() {
4
+ this.middlewares = [];
5
+ }
6
+
7
+ use(middleware) {
8
+ this.middlewares.push(middleware);
9
+ }
10
+
11
+ async run(req, res, finalHandler) {
12
+ let index = 0;
13
+
14
+ const next = async () => {
15
+ if (index >= this.middlewares.length) {
16
+ return await finalHandler();
17
+ }
18
+
19
+ const middleware = this.middlewares[index++];
20
+ await middleware(req, res, next);
21
+ };
22
+
23
+ await next();
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kempo-server",
3
3
  "type": "module",
4
- "version": "1.2.1",
4
+ "version": "1.3.0",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "main": "index.js",
7
7
  "bin": {
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
- const requestPath = req.url.split('?')[0];
150
- log(`${req.method} ${requestPath}`, 0);
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
- try {
158
- const fileContent = await readFile(customFilePath);
159
- const fileExtension = path.extname(customFilePath).toLowerCase().slice(1);
160
- const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
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
- log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`, 2);
163
- res.writeHead(200, { 'Content-Type': mimeType });
164
- res.end(fileContent);
165
- return; // Successfully served custom route
166
- } catch (error) {
167
- log(`Error serving custom route ${requestPath}: ${error.message}`, 0);
168
- res.writeHead(500, { 'Content-Type': 'text/plain' });
169
- res.end('Internal Server Error');
170
- return;
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
- try {
181
- const fileContent = await readFile(resolvedFilePath);
182
- const fileExtension = path.extname(resolvedFilePath).toLowerCase().slice(1);
183
- const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
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
- log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`, 2);
186
- res.writeHead(200, { 'Content-Type': mimeType });
187
- res.end(fileContent);
188
- return; // Successfully served wildcard route
189
- } catch (error) {
190
- log(`Error serving wildcard route ${requestPath}: ${error.message}`, 0);
191
- res.writeHead(500, { 'Content-Type': 'text/plain' });
192
- res.end('Internal Server Error');
193
- return;
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 again after rescan
208
- const reserved = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
257
+ // Try to serve the file normally
258
+ const served = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
209
259
 
210
- if (!reserved) {
211
- log(`404 - File not found after rescan: ${requestPath}`, 1);
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
- } else if (!served) {
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
  }