kempo-server 1.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/README.md +421 -0
- package/defaultConfig.js +93 -0
- package/docs/.config.json +6 -0
- package/docs/.config.json.example +19 -0
- package/docs/api/user/[id]/GET.js +15 -0
- package/docs/api/user/[id]/[info]/DELETE.js +12 -0
- package/docs/api/user/[id]/[info]/GET.js +17 -0
- package/docs/api/user/[id]/[info]/POST.js +18 -0
- package/docs/api/user/[id]/[info]/PUT.js +19 -0
- package/docs/index.html +343 -0
- package/findFile.js +139 -0
- package/getFiles.js +73 -0
- package/getFlags.js +35 -0
- package/index.js +46 -0
- package/package.json +15 -0
- package/requestWrapper.js +87 -0
- package/responseWrapper.js +204 -0
- package/router.js +159 -0
- package/serveFile.js +71 -0
package/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import router from './router.js';
|
|
3
|
+
import getFlags from './getFlags.js';
|
|
4
|
+
|
|
5
|
+
const flags = getFlags(process.argv.slice(2), {
|
|
6
|
+
port: 3000,
|
|
7
|
+
logging: 2,
|
|
8
|
+
root: './',
|
|
9
|
+
scan: false
|
|
10
|
+
}, {
|
|
11
|
+
p: 'port',
|
|
12
|
+
l: 'logging',
|
|
13
|
+
r: 'root',
|
|
14
|
+
s: 'scan'
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if(typeof(flags.logging) === 'string'){
|
|
18
|
+
switch(flags.logging.toLowerCase()) {
|
|
19
|
+
case 'silent':
|
|
20
|
+
flags.logging = 0;
|
|
21
|
+
break;
|
|
22
|
+
case 'minimal':
|
|
23
|
+
flags.logging = 1;
|
|
24
|
+
break;
|
|
25
|
+
case 'verbose':
|
|
26
|
+
flags.logging = 3;
|
|
27
|
+
break;
|
|
28
|
+
case 'debug':
|
|
29
|
+
flags.logging = 4;
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
flags.logging = 2;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const log = (message, level = 2) => {
|
|
38
|
+
if(level <= flags.logging){
|
|
39
|
+
console.log(message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const server = http.createServer(await router(flags, log));
|
|
44
|
+
server.listen(flags.port);
|
|
45
|
+
log(`Server started at: http://localhost:${flags.port}`);
|
|
46
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kempo-server",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node index.js -r docs"
|
|
9
|
+
},
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"essentialcss": "^2.0.1"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { URL } from 'url';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates an enhanced request object with Express-like functionality
|
|
5
|
+
* @param {IncomingMessage} request - The original Node.js request object
|
|
6
|
+
* @param {Object} params - Route parameters from dynamic routes
|
|
7
|
+
* @returns {Object} Enhanced request object
|
|
8
|
+
*/
|
|
9
|
+
export function createRequestWrapper(request, params = {}) {
|
|
10
|
+
// Parse URL to extract query parameters
|
|
11
|
+
const url = new URL(request.url, `http://${request.headers.host || 'localhost'}`);
|
|
12
|
+
const query = Object.fromEntries(url.searchParams);
|
|
13
|
+
|
|
14
|
+
// Create the enhanced request object
|
|
15
|
+
const enhancedRequest = {
|
|
16
|
+
// Original request properties and methods
|
|
17
|
+
...request,
|
|
18
|
+
method: request.method,
|
|
19
|
+
url: request.url,
|
|
20
|
+
headers: request.headers,
|
|
21
|
+
|
|
22
|
+
// Enhanced properties
|
|
23
|
+
params,
|
|
24
|
+
query,
|
|
25
|
+
path: url.pathname,
|
|
26
|
+
|
|
27
|
+
// Body parsing methods
|
|
28
|
+
async body() {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
let body = '';
|
|
31
|
+
|
|
32
|
+
request.on('data', chunk => {
|
|
33
|
+
body += chunk.toString();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
request.on('end', () => {
|
|
37
|
+
resolve(body);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
request.on('error', reject);
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async json() {
|
|
45
|
+
try {
|
|
46
|
+
const body = await this.body();
|
|
47
|
+
return JSON.parse(body);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error('Invalid JSON in request body');
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async text() {
|
|
54
|
+
return this.body();
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async buffer() {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const chunks = [];
|
|
60
|
+
|
|
61
|
+
request.on('data', chunk => {
|
|
62
|
+
chunks.push(chunk);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
request.on('end', () => {
|
|
66
|
+
resolve(Buffer.concat(chunks));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
request.on('error', reject);
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Utility methods
|
|
74
|
+
get(headerName) {
|
|
75
|
+
return request.headers[headerName.toLowerCase()];
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
is(type) {
|
|
79
|
+
const contentType = this.get('content-type') || '';
|
|
80
|
+
return contentType.includes(type);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return enhancedRequest;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default createRequestWrapper;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an enhanced response object with Express-like functionality
|
|
3
|
+
* @param {ServerResponse} response - The original Node.js response object
|
|
4
|
+
* @returns {Object} Enhanced response object
|
|
5
|
+
*/
|
|
6
|
+
export function createResponseWrapper(response) {
|
|
7
|
+
// Track if response has been sent
|
|
8
|
+
let sent = false;
|
|
9
|
+
|
|
10
|
+
// Create the enhanced response object
|
|
11
|
+
const enhancedResponse = {
|
|
12
|
+
// Original response properties and methods
|
|
13
|
+
...response,
|
|
14
|
+
|
|
15
|
+
// Status code management
|
|
16
|
+
status(code) {
|
|
17
|
+
if (sent) {
|
|
18
|
+
throw new Error('Cannot set status after response has been sent');
|
|
19
|
+
}
|
|
20
|
+
response.statusCode = code;
|
|
21
|
+
return enhancedResponse; // Allow chaining
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Header management
|
|
25
|
+
set(field, value) {
|
|
26
|
+
if (sent) {
|
|
27
|
+
throw new Error('Cannot set headers after response has been sent');
|
|
28
|
+
}
|
|
29
|
+
if (typeof field === 'object') {
|
|
30
|
+
// Set multiple headers: res.set({ 'Content-Type': 'text/html', 'X-Custom': 'value' })
|
|
31
|
+
Object.entries(field).forEach(([key, val]) => {
|
|
32
|
+
response.setHeader(key, val);
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
// Set single header: res.set('Content-Type', 'text/html')
|
|
36
|
+
response.setHeader(field, value);
|
|
37
|
+
}
|
|
38
|
+
return enhancedResponse; // Allow chaining
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
get(field) {
|
|
42
|
+
return response.getHeader(field);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Content type helpers
|
|
46
|
+
type(contentType) {
|
|
47
|
+
if (sent) {
|
|
48
|
+
throw new Error('Cannot set content type after response has been sent');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle common shortcuts
|
|
52
|
+
const typeMap = {
|
|
53
|
+
'html': 'text/html',
|
|
54
|
+
'json': 'application/json',
|
|
55
|
+
'xml': 'application/xml',
|
|
56
|
+
'text': 'text/plain',
|
|
57
|
+
'css': 'text/css',
|
|
58
|
+
'js': 'application/javascript'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const mimeType = typeMap[contentType] || contentType;
|
|
62
|
+
response.setHeader('Content-Type', mimeType);
|
|
63
|
+
return enhancedResponse; // Allow chaining
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// JSON response
|
|
67
|
+
json(obj) {
|
|
68
|
+
if (sent) {
|
|
69
|
+
throw new Error('Cannot send response after it has already been sent');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
sent = true;
|
|
73
|
+
response.setHeader('Content-Type', 'application/json');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const jsonString = JSON.stringify(obj);
|
|
77
|
+
response.end(jsonString);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error('Failed to stringify object to JSON');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return enhancedResponse;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Text response
|
|
86
|
+
send(data) {
|
|
87
|
+
if (sent) {
|
|
88
|
+
throw new Error('Cannot send response after it has already been sent');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
sent = true;
|
|
92
|
+
|
|
93
|
+
if (data === null || data === undefined) {
|
|
94
|
+
response.end();
|
|
95
|
+
return enhancedResponse;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle different data types
|
|
99
|
+
if (typeof data === 'object') {
|
|
100
|
+
// If it's an object, send as JSON
|
|
101
|
+
response.setHeader('Content-Type', 'application/json');
|
|
102
|
+
response.end(JSON.stringify(data));
|
|
103
|
+
} else if (typeof data === 'string') {
|
|
104
|
+
// If Content-Type not set, default to text/html for strings
|
|
105
|
+
if (!response.getHeader('Content-Type')) {
|
|
106
|
+
response.setHeader('Content-Type', 'text/html');
|
|
107
|
+
}
|
|
108
|
+
response.end(data);
|
|
109
|
+
} else if (Buffer.isBuffer(data)) {
|
|
110
|
+
// Handle buffers
|
|
111
|
+
response.end(data);
|
|
112
|
+
} else {
|
|
113
|
+
// Convert to string
|
|
114
|
+
if (!response.getHeader('Content-Type')) {
|
|
115
|
+
response.setHeader('Content-Type', 'text/plain');
|
|
116
|
+
}
|
|
117
|
+
response.end(String(data));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return enhancedResponse;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// HTML response helper
|
|
124
|
+
html(htmlString) {
|
|
125
|
+
if (sent) {
|
|
126
|
+
throw new Error('Cannot send response after it has already been sent');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
sent = true;
|
|
130
|
+
response.setHeader('Content-Type', 'text/html');
|
|
131
|
+
response.end(htmlString);
|
|
132
|
+
return enhancedResponse;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// Text response helper
|
|
136
|
+
text(textString) {
|
|
137
|
+
if (sent) {
|
|
138
|
+
throw new Error('Cannot send response after it has already been sent');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
sent = true;
|
|
142
|
+
response.setHeader('Content-Type', 'text/plain');
|
|
143
|
+
response.end(String(textString));
|
|
144
|
+
return enhancedResponse;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// Redirect helper
|
|
148
|
+
redirect(url, statusCode = 302) {
|
|
149
|
+
if (sent) {
|
|
150
|
+
throw new Error('Cannot redirect after response has been sent');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
sent = true;
|
|
154
|
+
response.statusCode = statusCode;
|
|
155
|
+
response.setHeader('Location', url);
|
|
156
|
+
response.end();
|
|
157
|
+
return enhancedResponse;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// Cookie management (basic)
|
|
161
|
+
cookie(name, value, options = {}) {
|
|
162
|
+
if (sent) {
|
|
163
|
+
throw new Error('Cannot set cookies after response has been sent');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let cookieString = `${name}=${encodeURIComponent(value)}`;
|
|
167
|
+
|
|
168
|
+
if (options.maxAge) {
|
|
169
|
+
cookieString += `; Max-Age=${options.maxAge}`;
|
|
170
|
+
}
|
|
171
|
+
if (options.domain) {
|
|
172
|
+
cookieString += `; Domain=${options.domain}`;
|
|
173
|
+
}
|
|
174
|
+
if (options.path) {
|
|
175
|
+
cookieString += `; Path=${options.path}`;
|
|
176
|
+
}
|
|
177
|
+
if (options.secure) {
|
|
178
|
+
cookieString += '; Secure';
|
|
179
|
+
}
|
|
180
|
+
if (options.httpOnly) {
|
|
181
|
+
cookieString += '; HttpOnly';
|
|
182
|
+
}
|
|
183
|
+
if (options.sameSite) {
|
|
184
|
+
cookieString += `; SameSite=${options.sameSite}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const existingCookies = response.getHeader('Set-Cookie') || [];
|
|
188
|
+
const cookies = Array.isArray(existingCookies) ? existingCookies : [existingCookies];
|
|
189
|
+
cookies.push(cookieString);
|
|
190
|
+
|
|
191
|
+
response.setHeader('Set-Cookie', cookies);
|
|
192
|
+
return enhancedResponse;
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// Clear cookie helper
|
|
196
|
+
clearCookie(name, options = {}) {
|
|
197
|
+
return this.cookie(name, '', { ...options, maxAge: 0 });
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return enhancedResponse;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export default createResponseWrapper;
|
package/router.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import defaultConfig from './defaultConfig.js';
|
|
5
|
+
import getFiles from './getFiles.js';
|
|
6
|
+
import findFile from './findFile.js';
|
|
7
|
+
import serveFile from './serveFile.js';
|
|
8
|
+
|
|
9
|
+
export default async (flags, log) => {
|
|
10
|
+
log('Initializing router', 2);
|
|
11
|
+
const rootPath = path.join(process.cwd(), flags.root);
|
|
12
|
+
log(`Root path: ${rootPath}`, 2);
|
|
13
|
+
|
|
14
|
+
let config = defaultConfig;
|
|
15
|
+
try {
|
|
16
|
+
const configPath = path.join(rootPath, '.config.json');
|
|
17
|
+
log(`Loading config from: ${configPath}`, 2);
|
|
18
|
+
const configContent = await readFile(configPath, 'utf8');
|
|
19
|
+
const userConfig = JSON.parse(configContent);
|
|
20
|
+
config = {
|
|
21
|
+
...defaultConfig,
|
|
22
|
+
...userConfig
|
|
23
|
+
};
|
|
24
|
+
log('User config loaded and merged with defaults', 2);
|
|
25
|
+
} catch (e){
|
|
26
|
+
log('Using default config (no .config.json found)', 2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
Inject mandatory disallowed patterns
|
|
31
|
+
*/
|
|
32
|
+
const dis = new Set(config.disallowedRegex);
|
|
33
|
+
dis.add("^/\\..*");
|
|
34
|
+
dis.add("\\.config$");
|
|
35
|
+
dis.add("\\.git/");
|
|
36
|
+
config.disallowedRegex = [...dis];
|
|
37
|
+
log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`, 2);
|
|
38
|
+
|
|
39
|
+
let files = await getFiles(rootPath, config, log);
|
|
40
|
+
log(`Initial scan found ${files.length} files`, 1);
|
|
41
|
+
|
|
42
|
+
// Process custom routes - resolve paths and validate files exist
|
|
43
|
+
const customRoutes = new Map();
|
|
44
|
+
if (config.customRoutes && Object.keys(config.customRoutes).length > 0) {
|
|
45
|
+
log(`Processing ${Object.keys(config.customRoutes).length} custom routes`, 2);
|
|
46
|
+
|
|
47
|
+
for (const [urlPath, filePath] of Object.entries(config.customRoutes)) {
|
|
48
|
+
try {
|
|
49
|
+
// Resolve the file path relative to the current working directory
|
|
50
|
+
const resolvedPath = path.resolve(filePath);
|
|
51
|
+
|
|
52
|
+
// Check if the file exists (we'll do this async)
|
|
53
|
+
const { stat } = await import('fs/promises');
|
|
54
|
+
await stat(resolvedPath);
|
|
55
|
+
|
|
56
|
+
customRoutes.set(urlPath, resolvedPath);
|
|
57
|
+
log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`, 2);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
log(`Custom route error for ${urlPath} -> ${filePath}: ${error.message}`, 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Track 404 attempts to avoid unnecessary rescans
|
|
65
|
+
const rescanAttempts = new Map(); // path -> attempt count
|
|
66
|
+
const dynamicNoRescanPaths = new Set(); // paths that have exceeded max attempts
|
|
67
|
+
|
|
68
|
+
// Helper function to check if a path should skip rescanning
|
|
69
|
+
const shouldSkipRescan = (requestPath) => {
|
|
70
|
+
// Check static config patterns
|
|
71
|
+
const matchesConfigPattern = config.noRescanPaths.some(pattern => {
|
|
72
|
+
const regex = new RegExp(pattern);
|
|
73
|
+
return regex.test(requestPath);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (matchesConfigPattern) {
|
|
77
|
+
log(`Skipping rescan for configured pattern: ${requestPath}`, 3);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check dynamic blacklist
|
|
82
|
+
if (dynamicNoRescanPaths.has(requestPath)) {
|
|
83
|
+
log(`Skipping rescan for dynamically blacklisted path: ${requestPath}`, 3);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Helper function to track rescan attempts
|
|
91
|
+
const trackRescanAttempt = (requestPath) => {
|
|
92
|
+
const currentAttempts = rescanAttempts.get(requestPath) || 0;
|
|
93
|
+
const newAttempts = currentAttempts + 1;
|
|
94
|
+
rescanAttempts.set(requestPath, newAttempts);
|
|
95
|
+
|
|
96
|
+
if (newAttempts >= config.maxRescanAttempts) {
|
|
97
|
+
dynamicNoRescanPaths.add(requestPath);
|
|
98
|
+
log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`, 1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`, 2);
|
|
102
|
+
return newAttempts;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return async (req, res) => {
|
|
106
|
+
const requestPath = req.url.split('?')[0];
|
|
107
|
+
log(`${req.method} ${requestPath}`, 0);
|
|
108
|
+
|
|
109
|
+
// Check custom routes first
|
|
110
|
+
if (customRoutes.has(requestPath)) {
|
|
111
|
+
const customFilePath = customRoutes.get(requestPath);
|
|
112
|
+
log(`Serving custom route: ${requestPath} -> ${customFilePath}`, 2);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const fileContent = await readFile(customFilePath);
|
|
116
|
+
const fileExtension = path.extname(customFilePath).toLowerCase().slice(1);
|
|
117
|
+
const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
|
|
118
|
+
|
|
119
|
+
log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`, 2);
|
|
120
|
+
res.writeHead(200, { 'Content-Type': mimeType });
|
|
121
|
+
res.end(fileContent);
|
|
122
|
+
return; // Successfully served custom route
|
|
123
|
+
} catch (error) {
|
|
124
|
+
log(`Error serving custom route ${requestPath}: ${error.message}`, 0);
|
|
125
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
126
|
+
res.end('Internal Server Error');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Try to serve the file normally
|
|
132
|
+
const served = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
|
|
133
|
+
|
|
134
|
+
// If not served and scan flag is enabled, try rescanning once (with blacklist check)
|
|
135
|
+
if (!served && flags.scan && !shouldSkipRescan(requestPath)) {
|
|
136
|
+
trackRescanAttempt(requestPath);
|
|
137
|
+
log('File not found, rescanning directory...', 1);
|
|
138
|
+
files = await getFiles(rootPath, config, log);
|
|
139
|
+
log(`Rescan found ${files.length} files`, 2);
|
|
140
|
+
|
|
141
|
+
// Try to serve again after rescan
|
|
142
|
+
const reserved = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
|
|
143
|
+
|
|
144
|
+
if (!reserved) {
|
|
145
|
+
log(`404 - File not found after rescan: ${requestPath}`, 1);
|
|
146
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
147
|
+
res.end('Not Found');
|
|
148
|
+
}
|
|
149
|
+
} else if (!served) {
|
|
150
|
+
if (shouldSkipRescan(requestPath)) {
|
|
151
|
+
log(`404 - Skipped rescan for: ${requestPath}`, 2);
|
|
152
|
+
} else {
|
|
153
|
+
log(`404 - File not found: ${requestPath}`, 1);
|
|
154
|
+
}
|
|
155
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
156
|
+
res.end('Not Found');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
package/serveFile.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import findFile from './findFile.js';
|
|
5
|
+
import createRequestWrapper from './requestWrapper.js';
|
|
6
|
+
import createResponseWrapper from './responseWrapper.js';
|
|
7
|
+
|
|
8
|
+
export default async (files, rootPath, requestPath, method, config, req, res, log) => {
|
|
9
|
+
log(`Attempting to serve: ${requestPath}`, 3);
|
|
10
|
+
const [file, params] = await findFile(files, rootPath, requestPath, method, log);
|
|
11
|
+
|
|
12
|
+
if (!file) {
|
|
13
|
+
log(`No file found for: ${requestPath}`, 3);
|
|
14
|
+
return false; // Could not find file
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fileName = path.basename(file);
|
|
18
|
+
log(`Found file: ${file}`, 2);
|
|
19
|
+
|
|
20
|
+
// Check if this is a route file that should be executed as a module
|
|
21
|
+
if (config.routeFiles.includes(fileName)) {
|
|
22
|
+
log(`Executing route file: ${fileName}`, 2);
|
|
23
|
+
try {
|
|
24
|
+
// Load the file as a module
|
|
25
|
+
const fileUrl = pathToFileURL(file).href;
|
|
26
|
+
log(`Loading module from: ${fileUrl}`, 3);
|
|
27
|
+
const module = await import(fileUrl);
|
|
28
|
+
|
|
29
|
+
// Execute the default export function
|
|
30
|
+
if (typeof module.default === 'function') {
|
|
31
|
+
log(`Executing route function with params: ${JSON.stringify(params)}`, 3);
|
|
32
|
+
|
|
33
|
+
// Create enhanced request and response wrappers
|
|
34
|
+
const enhancedRequest = createRequestWrapper(req, params);
|
|
35
|
+
const enhancedResponse = createResponseWrapper(res);
|
|
36
|
+
|
|
37
|
+
await module.default(enhancedRequest, enhancedResponse);
|
|
38
|
+
log(`Route executed successfully: ${fileName}`, 2);
|
|
39
|
+
return true; // Successfully served
|
|
40
|
+
} else {
|
|
41
|
+
log(`Route file does not export a function: ${fileName}`, 0);
|
|
42
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
43
|
+
res.end('Route file does not export a function');
|
|
44
|
+
return true; // Handled (even though it's an error)
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
log(`Error loading route file ${fileName}: ${error.message}`, 0);
|
|
48
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
49
|
+
res.end('Internal Server Error');
|
|
50
|
+
return true; // Handled (even though it's an error)
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
// Serve the file content with appropriate MIME type
|
|
54
|
+
log(`Serving static file: ${fileName}`, 2);
|
|
55
|
+
try {
|
|
56
|
+
const fileContent = await readFile(file);
|
|
57
|
+
const fileExtension = path.extname(file).toLowerCase().slice(1);
|
|
58
|
+
const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
|
|
59
|
+
|
|
60
|
+
log(`Serving ${file} as ${mimeType} (${fileContent.length} bytes)`, 2);
|
|
61
|
+
res.writeHead(200, { 'Content-Type': mimeType });
|
|
62
|
+
res.end(fileContent);
|
|
63
|
+
return true; // Successfully served
|
|
64
|
+
} catch (error) {
|
|
65
|
+
log(`Error reading file ${file}: ${error.message}`, 0);
|
|
66
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
67
|
+
res.end('Internal Server Error');
|
|
68
|
+
return true; // Handled (even though it's an error)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|