lieko-express 0.0.8 → 0.0.9
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/lib/cors.js +100 -0
- package/lib/schema.js +418 -0
- package/lib/static.js +247 -0
- package/lieko-express.js +98 -357
- package/package.json +3 -2
package/lib/cors.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module.exports = function cors(userOptions = {}) {
|
|
2
|
+
const defaultOptions = {
|
|
3
|
+
enabled: true,
|
|
4
|
+
origin: "*",
|
|
5
|
+
strictOrigin: false,
|
|
6
|
+
allowPrivateNetwork: false,
|
|
7
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
8
|
+
headers: ["Content-Type", "Authorization"],
|
|
9
|
+
credentials: false,
|
|
10
|
+
maxAge: 86400,
|
|
11
|
+
exposedHeaders: [],
|
|
12
|
+
debug: false
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const opts = { ...defaultOptions, ...userOptions, enabled: true };
|
|
16
|
+
|
|
17
|
+
const _matchOrigin = (origin, allowedOrigin) => {
|
|
18
|
+
if (!origin || !allowedOrigin) return false;
|
|
19
|
+
if (Array.isArray(allowedOrigin)) {
|
|
20
|
+
return allowedOrigin.some(o => _matchOrigin(origin, o));
|
|
21
|
+
}
|
|
22
|
+
if (allowedOrigin === "*") return true;
|
|
23
|
+
if (allowedOrigin.includes("*")) {
|
|
24
|
+
const regex = new RegExp("^" + allowedOrigin
|
|
25
|
+
.replace(/\./g, "\\.")
|
|
26
|
+
.replace(/\*/g, ".*") + "$");
|
|
27
|
+
return regex.test(origin);
|
|
28
|
+
}
|
|
29
|
+
return origin === allowedOrigin;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const _logDebug = (req, finalOpts) => {
|
|
33
|
+
if (!finalOpts.debug) return;
|
|
34
|
+
console.log("\n[CORS DEBUG]");
|
|
35
|
+
console.log("Request:", req.method, req.url);
|
|
36
|
+
console.log("Origin:", req.headers.origin || "none");
|
|
37
|
+
console.log("Applied CORS Policy:");
|
|
38
|
+
console.log(" - Access-Control-Allow-Origin:", finalOpts.origin);
|
|
39
|
+
console.log(" - Methods:", finalOpts.methods.join(", "));
|
|
40
|
+
console.log(" - Headers:", finalOpts.headers.join(", "));
|
|
41
|
+
if (finalOpts.credentials) console.log(" - Credentials: true");
|
|
42
|
+
if (finalOpts.exposedHeaders?.length) console.log(" - Exposed:", finalOpts.exposedHeaders.join(", "));
|
|
43
|
+
console.log(" - Max-Age:", finalOpts.maxAge);
|
|
44
|
+
if (req.method === "OPTIONS") console.log("Preflight handled → 204\n");
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return function corsMiddleware(req, res, next) {
|
|
48
|
+
if (!opts.enabled) return next();
|
|
49
|
+
|
|
50
|
+
const requestOrigin = req.headers.origin || "";
|
|
51
|
+
|
|
52
|
+
let finalOrigin = "*";
|
|
53
|
+
|
|
54
|
+
if (opts.strictOrigin && requestOrigin) {
|
|
55
|
+
const allowed = _matchOrigin(requestOrigin, opts.origin);
|
|
56
|
+
if (!allowed) {
|
|
57
|
+
res.statusCode = 403;
|
|
58
|
+
return res.end(JSON.stringify({
|
|
59
|
+
success: false,
|
|
60
|
+
error: "Origin Forbidden",
|
|
61
|
+
message: `Origin "${requestOrigin}" is not allowed`
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (opts.origin === "*") {
|
|
67
|
+
finalOrigin = "*";
|
|
68
|
+
} else if (Array.isArray(opts.origin)) {
|
|
69
|
+
const match = opts.origin.find(o => _matchOrigin(requestOrigin, o));
|
|
70
|
+
finalOrigin = match || opts.origin[0];
|
|
71
|
+
} else {
|
|
72
|
+
finalOrigin = _matchOrigin(requestOrigin, opts.origin) ? requestOrigin : opts.origin;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_logDebug(req, { ...opts, origin: finalOrigin });
|
|
76
|
+
|
|
77
|
+
res.setHeader("Access-Control-Allow-Origin", finalOrigin);
|
|
78
|
+
|
|
79
|
+
if (opts.credentials) {
|
|
80
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (opts.exposedHeaders?.length) {
|
|
84
|
+
res.setHeader("Access-Control-Expose-Headers", opts.exposedHeaders.join(", "));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (opts.allowPrivateNetwork && req.headers["access-control-request-private-network"] === "true") {
|
|
88
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (req.method === "OPTIONS") {
|
|
92
|
+
res.setHeader("Access-Control-Allow-Methods", opts.methods.join(", "));
|
|
93
|
+
res.setHeader("Access-Control-Allow-Headers", opts.headers.join(", "));
|
|
94
|
+
res.setHeader("Access-Control-Max-Age", opts.maxAge);
|
|
95
|
+
res.statusCode = 204;
|
|
96
|
+
return res.end();
|
|
97
|
+
}
|
|
98
|
+
next();
|
|
99
|
+
};
|
|
100
|
+
};
|
package/lib/schema.js
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
class ValidationError extends Error {
|
|
2
|
+
constructor(errors) {
|
|
3
|
+
super('Validation failed');
|
|
4
|
+
this.name = 'ValidationError';
|
|
5
|
+
this.errors = errors;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class Schema {
|
|
10
|
+
constructor(rules) {
|
|
11
|
+
this.rules = rules;
|
|
12
|
+
this.fields = rules;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
validate(data) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
for (const [field, validators] of Object.entries(this.rules)) {
|
|
18
|
+
const value = data[field];
|
|
19
|
+
for (const validator of validators) {
|
|
20
|
+
const error = validator(value, field, data);
|
|
21
|
+
if (error) {
|
|
22
|
+
errors.push(error);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (errors.length > 0) throw new ValidationError(errors);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const validators = {
|
|
33
|
+
required: (message = 'Field is required') => {
|
|
34
|
+
return (value, field) => {
|
|
35
|
+
if (value === undefined || value === null || value === '') {
|
|
36
|
+
return { field, message, type: 'required' };
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
requiredTrue: (message = 'Field must be true') => {
|
|
43
|
+
return (value, field) => {
|
|
44
|
+
const normalized = value === true || value === 'true' || value === '1' || value === 1;
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
return { field, message, type: 'requiredTrue' };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
optional: () => {
|
|
53
|
+
return () => null;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
string: (message = 'Field must be a string') => {
|
|
57
|
+
return (value, field) => {
|
|
58
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
59
|
+
return { field, message, type: 'string' };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
number: (message = 'Field must be a number') => {
|
|
66
|
+
return (value, field) => {
|
|
67
|
+
if (value !== undefined && typeof value !== 'number') {
|
|
68
|
+
return { field, message, type: 'number' };
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
boolean: (message = 'Field must be a boolean') => {
|
|
75
|
+
return (value, field) => {
|
|
76
|
+
if (value === undefined || value === null || value === '') return null;
|
|
77
|
+
|
|
78
|
+
const validTrue = ['true', true, 1, '1'];
|
|
79
|
+
const validFalse = ['false', false, 0, '0'];
|
|
80
|
+
|
|
81
|
+
const isValid = validTrue.includes(value) || validFalse.includes(value);
|
|
82
|
+
|
|
83
|
+
if (!isValid) {
|
|
84
|
+
return { field, message, type: 'boolean' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
integer: (message = 'Field must be an integer') => {
|
|
92
|
+
return (value, field) => {
|
|
93
|
+
if (value !== undefined && !Number.isInteger(value)) {
|
|
94
|
+
return { field, message, type: 'integer' };
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
positive: (message = 'Field must be positive') => {
|
|
101
|
+
return (value, field) => {
|
|
102
|
+
if (value !== undefined && value <= 0) {
|
|
103
|
+
return { field, message, type: 'positive' };
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
negative: (message = 'Field must be negative') => {
|
|
110
|
+
return (value, field) => {
|
|
111
|
+
if (value !== undefined && value >= 0) {
|
|
112
|
+
return { field, message, type: 'negative' };
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
email: (message = 'Invalid email format') => {
|
|
119
|
+
return (value, field) => {
|
|
120
|
+
if (value !== undefined && value !== null) {
|
|
121
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
122
|
+
if (!emailRegex.test(value)) {
|
|
123
|
+
return { field, message, type: 'email' };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
min: (minValue, message) => {
|
|
131
|
+
return (value, field) => {
|
|
132
|
+
if (value !== undefined && value !== null) {
|
|
133
|
+
if (typeof value === 'string' && value.length < minValue) {
|
|
134
|
+
return {
|
|
135
|
+
field,
|
|
136
|
+
message: message || `Field must be at least ${minValue} characters`,
|
|
137
|
+
type: 'min'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === 'number' && value < minValue) {
|
|
141
|
+
return {
|
|
142
|
+
field,
|
|
143
|
+
message: message || `Field must be at least ${minValue}`,
|
|
144
|
+
type: 'min'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
max: (maxValue, message) => {
|
|
153
|
+
return (value, field) => {
|
|
154
|
+
if (value !== undefined && value !== null) {
|
|
155
|
+
if (typeof value === 'string' && value.length > maxValue) {
|
|
156
|
+
return {
|
|
157
|
+
field,
|
|
158
|
+
message: message || `Field must be at most ${maxValue} characters`,
|
|
159
|
+
type: 'max'
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (typeof value === 'number' && value > maxValue) {
|
|
163
|
+
return {
|
|
164
|
+
field,
|
|
165
|
+
message: message || `Field must be at most ${maxValue}`,
|
|
166
|
+
type: 'max'
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
length: (n, message) => {
|
|
175
|
+
return (value, field) => {
|
|
176
|
+
if (typeof value === 'string' && value.length !== n) {
|
|
177
|
+
return {
|
|
178
|
+
field,
|
|
179
|
+
message: message || `Field must be exactly ${n} characters`,
|
|
180
|
+
type: 'length'
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
minLength: (minLength, message) => {
|
|
188
|
+
return (value, field) => {
|
|
189
|
+
if (value !== undefined && value !== null && typeof value === 'string') {
|
|
190
|
+
if (value.length < minLength) {
|
|
191
|
+
return {
|
|
192
|
+
field,
|
|
193
|
+
message: message || `Field must be at least ${minLength} characters`,
|
|
194
|
+
type: 'minLength'
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
maxLength: (maxLength, message) => {
|
|
203
|
+
return (value, field) => {
|
|
204
|
+
if (value !== undefined && value !== null && typeof value === 'string') {
|
|
205
|
+
if (value.length > maxLength) {
|
|
206
|
+
return {
|
|
207
|
+
field,
|
|
208
|
+
message: message || `Field must be at most ${maxLength} characters`,
|
|
209
|
+
type: 'maxLength'
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
pattern: (regex, message = 'Invalid format') => {
|
|
218
|
+
return (value, field) => {
|
|
219
|
+
if (value !== undefined && value !== null && typeof value === 'string') {
|
|
220
|
+
if (!regex.test(value)) {
|
|
221
|
+
return { field, message, type: 'pattern' };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
oneOf: (allowedValues, message) => {
|
|
229
|
+
return (value, field) => {
|
|
230
|
+
if (value !== undefined && value !== null) {
|
|
231
|
+
if (!allowedValues.includes(value)) {
|
|
232
|
+
return {
|
|
233
|
+
field,
|
|
234
|
+
message: message || `Field must be one of: ${allowedValues.join(', ')}`,
|
|
235
|
+
type: 'oneOf'
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
notOneOf: (values, message) => {
|
|
244
|
+
return (value, field) => {
|
|
245
|
+
if (values.includes(value)) {
|
|
246
|
+
return {
|
|
247
|
+
field,
|
|
248
|
+
message: message || `Field cannot be one of: ${values.join(', ')}`,
|
|
249
|
+
type: 'notOneOf'
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
custom: (validatorFn, message = 'Validation failed') => {
|
|
257
|
+
return (value, field, data) => {
|
|
258
|
+
const isValid = validatorFn(value, data);
|
|
259
|
+
if (!isValid) {
|
|
260
|
+
return { field, message, type: 'custom' };
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
equal: (expectedValue, message) => {
|
|
267
|
+
return (value, field) => {
|
|
268
|
+
if (value !== expectedValue) {
|
|
269
|
+
return {
|
|
270
|
+
field,
|
|
271
|
+
message: message || `Field must be equal to ${expectedValue}`,
|
|
272
|
+
type: 'equal'
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
mustBeTrue: (message = 'This field must be accepted') => {
|
|
280
|
+
return (value, field) => {
|
|
281
|
+
const normalized = value === true || value === 'true' || value === '1' || value === 1;
|
|
282
|
+
if (!normalized) {
|
|
283
|
+
return { field, message, type: 'mustBeTrue' };
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
mustBeFalse: (message = 'This field must be declined') => {
|
|
290
|
+
return (value, field) => {
|
|
291
|
+
const normalized = value === false || value === 'false' || value === '0' || value === 0;
|
|
292
|
+
if (!normalized) {
|
|
293
|
+
return { field, message, type: 'mustBeFalse' };
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
date: (message = 'Invalid date') => {
|
|
300
|
+
return (value, field) => {
|
|
301
|
+
if (!value) return null;
|
|
302
|
+
const date = new Date(value);
|
|
303
|
+
if (isNaN(date.getTime())) {
|
|
304
|
+
return { field, message, type: 'date' };
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
before: (limit, message) => {
|
|
311
|
+
return (value, field) => {
|
|
312
|
+
if (!value) return null;
|
|
313
|
+
const d1 = new Date(value);
|
|
314
|
+
const d2 = new Date(limit);
|
|
315
|
+
if (isNaN(d1) || d1 >= d2) {
|
|
316
|
+
return {
|
|
317
|
+
field,
|
|
318
|
+
message: message || `Date must be before ${limit}`,
|
|
319
|
+
type: 'before'
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
after: (limit, message) => {
|
|
327
|
+
return (value, field) => {
|
|
328
|
+
if (!value) return null;
|
|
329
|
+
const d1 = new Date(value);
|
|
330
|
+
const d2 = new Date(limit);
|
|
331
|
+
if (isNaN(d1) || d1 <= d2) {
|
|
332
|
+
return {
|
|
333
|
+
field,
|
|
334
|
+
message: message || `Date must be after ${limit}`,
|
|
335
|
+
type: 'after'
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
startsWith: (prefix, message) => {
|
|
343
|
+
return (value, field) => {
|
|
344
|
+
if (typeof value === 'string' && !value.startsWith(prefix)) {
|
|
345
|
+
return {
|
|
346
|
+
field,
|
|
347
|
+
message: message || `Field must start with "${prefix}"`,
|
|
348
|
+
type: 'startsWith'
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
endsWith: (suffix, message) => {
|
|
356
|
+
return (value, field) => {
|
|
357
|
+
if (typeof value === 'string' && !value.endsWith(suffix)) {
|
|
358
|
+
return {
|
|
359
|
+
field,
|
|
360
|
+
message: message || `Field must end with "${suffix}"`,
|
|
361
|
+
type: 'endsWith'
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
function validate(schema) {
|
|
370
|
+
return (req, res, next) => {
|
|
371
|
+
try {
|
|
372
|
+
schema.validate(req.body);
|
|
373
|
+
next();
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (error instanceof ValidationError) {
|
|
376
|
+
return res.status(400).json({
|
|
377
|
+
success: false,
|
|
378
|
+
message: 'Validation failed',
|
|
379
|
+
errors: error.errors
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function validatePartial(schema) {
|
|
388
|
+
const partial = {};
|
|
389
|
+
|
|
390
|
+
for (const field in schema.fields) {
|
|
391
|
+
const rules = schema.fields[field];
|
|
392
|
+
|
|
393
|
+
const filtered = rules.filter(v =>
|
|
394
|
+
v.name !== 'required' &&
|
|
395
|
+
v.name !== 'requiredTrue' &&
|
|
396
|
+
v.name !== 'mustBeTrue'
|
|
397
|
+
);
|
|
398
|
+
partial[field] = [validators.optional(), ...filtered];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return new Schema(partial);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
Schema,
|
|
406
|
+
ValidationError,
|
|
407
|
+
validators,
|
|
408
|
+
validate,
|
|
409
|
+
validatePartial,
|
|
410
|
+
|
|
411
|
+
schema: (...args) => new Schema(...args),
|
|
412
|
+
v: validators,
|
|
413
|
+
validator: validators,
|
|
414
|
+
middleware: {
|
|
415
|
+
validate,
|
|
416
|
+
validatePartial
|
|
417
|
+
}
|
|
418
|
+
};
|
package/lib/static.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
module.exports = function serveStatic(root, options = {}) {
|
|
5
|
+
const opts = {
|
|
6
|
+
maxAge: options.maxAge || 0,
|
|
7
|
+
index: options.index !== undefined ? options.index : 'index.html',
|
|
8
|
+
dotfiles: options.dotfiles || 'ignore',
|
|
9
|
+
etag: options.etag !== undefined ? options.etag : true,
|
|
10
|
+
extensions: options.extensions || false,
|
|
11
|
+
fallthrough: options.fallthrough !== undefined ? options.fallthrough : true,
|
|
12
|
+
immutable: options.immutable || false,
|
|
13
|
+
lastModified: options.lastModified !== undefined ? options.lastModified : true,
|
|
14
|
+
redirect: options.redirect !== undefined ? options.redirect : true,
|
|
15
|
+
setHeaders: options.setHeaders || null,
|
|
16
|
+
cacheControl: options.cacheControl !== undefined ? options.cacheControl : true
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const mimeTypes = {
|
|
20
|
+
'.html': 'text/html; charset=utf-8',
|
|
21
|
+
'.htm': 'text/html; charset=utf-8',
|
|
22
|
+
'.css': 'text/css; charset=utf-8',
|
|
23
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
24
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
25
|
+
'.json': 'application/json; charset=utf-8',
|
|
26
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
27
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
28
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
29
|
+
'.jpg': 'image/jpeg',
|
|
30
|
+
'.jpeg': 'image/jpeg',
|
|
31
|
+
'.png': 'image/png',
|
|
32
|
+
'.gif': 'image/gif',
|
|
33
|
+
'.svg': 'image/svg+xml',
|
|
34
|
+
'.webp': 'image/webp',
|
|
35
|
+
'.ico': 'image/x-icon',
|
|
36
|
+
'.bmp': 'image/bmp',
|
|
37
|
+
'.tiff': 'image/tiff',
|
|
38
|
+
'.tif': 'image/tiff',
|
|
39
|
+
'.mp3': 'audio/mpeg',
|
|
40
|
+
'.wav': 'audio/wav',
|
|
41
|
+
'.ogg': 'audio/ogg',
|
|
42
|
+
'.m4a': 'audio/mp4',
|
|
43
|
+
'.aac': 'audio/aac',
|
|
44
|
+
'.flac': 'audio/flac',
|
|
45
|
+
'.mp4': 'video/mp4',
|
|
46
|
+
'.webm': 'video/webm',
|
|
47
|
+
'.ogv': 'video/ogg',
|
|
48
|
+
'.avi': 'video/x-msvideo',
|
|
49
|
+
'.mov': 'video/quicktime',
|
|
50
|
+
'.wmv': 'video/x-ms-wmv',
|
|
51
|
+
'.flv': 'video/x-flv',
|
|
52
|
+
'.mkv': 'video/x-matroska',
|
|
53
|
+
'.woff': 'font/woff',
|
|
54
|
+
'.woff2': 'font/woff2',
|
|
55
|
+
'.ttf': 'font/ttf',
|
|
56
|
+
'.otf': 'font/otf',
|
|
57
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
58
|
+
'.zip': 'application/zip',
|
|
59
|
+
'.rar': 'application/x-rar-compressed',
|
|
60
|
+
'.tar': 'application/x-tar',
|
|
61
|
+
'.gz': 'application/gzip',
|
|
62
|
+
'.7z': 'application/x-7z-compressed',
|
|
63
|
+
'.pdf': 'application/pdf',
|
|
64
|
+
'.doc': 'application/msword',
|
|
65
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
66
|
+
'.xls': 'application/vnd.ms-excel',
|
|
67
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
68
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
69
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
70
|
+
'.wasm': 'application/wasm',
|
|
71
|
+
'.csv': 'text/csv; charset=utf-8'
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getMimeType = (filePath) => {
|
|
75
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
76
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const generateETag = (stats) => {
|
|
80
|
+
const mtime = stats.mtime.getTime().toString(16);
|
|
81
|
+
const size = stats.size.toString(16);
|
|
82
|
+
return `W/"${size}-${mtime}"`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return async (req, res, next) => {
|
|
86
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
87
|
+
return next();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
let pathname = req.url;
|
|
92
|
+
const qIndex = pathname.indexOf('?');
|
|
93
|
+
if (qIndex !== -1) {
|
|
94
|
+
pathname = pathname.substring(0, qIndex);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (pathname === '') {
|
|
98
|
+
pathname = '/';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
pathname = decodeURIComponent(pathname);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
if (opts.fallthrough) return next();
|
|
105
|
+
return res.status(400).send('Bad Request');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let filePath = pathname === '/' ? root : path.join(root, pathname);
|
|
109
|
+
|
|
110
|
+
const resolvedPath = path.resolve(filePath);
|
|
111
|
+
const resolvedRoot = path.resolve(root);
|
|
112
|
+
|
|
113
|
+
if (!resolvedPath.startsWith(resolvedRoot)) {
|
|
114
|
+
if (opts.fallthrough) return next();
|
|
115
|
+
return res.status(403).send('Forbidden');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let stats;
|
|
119
|
+
try {
|
|
120
|
+
stats = await fs.promises.stat(filePath);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (pathname === '/' && opts.index) {
|
|
123
|
+
const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
|
|
124
|
+
for (const indexFile of indexes) {
|
|
125
|
+
const indexPath = path.join(root, indexFile);
|
|
126
|
+
try {
|
|
127
|
+
stats = await fs.promises.stat(indexPath);
|
|
128
|
+
if (stats.isFile()) {
|
|
129
|
+
filePath = indexPath;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
} catch (e) { }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!stats && opts.extensions && Array.isArray(opts.extensions)) {
|
|
137
|
+
let found = false;
|
|
138
|
+
for (const ext of opts.extensions) {
|
|
139
|
+
const testPath = filePath + (ext.startsWith('.') ? ext : '.' + ext);
|
|
140
|
+
try {
|
|
141
|
+
stats = await fs.promises.stat(testPath);
|
|
142
|
+
filePath = testPath;
|
|
143
|
+
found = true;
|
|
144
|
+
break;
|
|
145
|
+
} catch (e) { }
|
|
146
|
+
}
|
|
147
|
+
if (!found) return next();
|
|
148
|
+
} else if (!stats) {
|
|
149
|
+
return next();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (stats.isDirectory()) {
|
|
154
|
+
if (opts.redirect && !pathname.endsWith('/')) {
|
|
155
|
+
const query = qIndex !== -1 ? req.url.substring(qIndex) : '';
|
|
156
|
+
const redirectUrl = pathname + '/' + query;
|
|
157
|
+
return res.redirect(redirectUrl, 301);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (opts.index) {
|
|
161
|
+
const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
|
|
162
|
+
|
|
163
|
+
for (const indexFile of indexes) {
|
|
164
|
+
const indexPath = path.join(filePath, indexFile);
|
|
165
|
+
try {
|
|
166
|
+
const indexStats = await fs.promises.stat(indexPath);
|
|
167
|
+
if (indexStats.isFile()) {
|
|
168
|
+
filePath = indexPath;
|
|
169
|
+
stats = indexStats;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
} catch (e) { }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (stats.isDirectory()) {
|
|
176
|
+
if (opts.fallthrough) return next();
|
|
177
|
+
return res.status(404).send('Not Found');
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
if (opts.fallthrough) return next();
|
|
181
|
+
return res.status(404).send('Not Found');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (opts.etag) {
|
|
186
|
+
const etag = generateETag(stats);
|
|
187
|
+
const ifNoneMatch = req.headers['if-none-match'];
|
|
188
|
+
|
|
189
|
+
if (ifNoneMatch === etag) {
|
|
190
|
+
res.statusCode = 304;
|
|
191
|
+
res.end();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
res.setHeader('ETag', etag);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (opts.lastModified) {
|
|
199
|
+
const lastModified = stats.mtime.toUTCString();
|
|
200
|
+
const ifModifiedSince = req.headers['if-modified-since'];
|
|
201
|
+
|
|
202
|
+
if (ifModifiedSince === lastModified) {
|
|
203
|
+
res.statusCode = 304;
|
|
204
|
+
res.end();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
res.setHeader('Last-Modified', lastModified);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (opts.cacheControl) {
|
|
212
|
+
let cacheControl = 'public';
|
|
213
|
+
|
|
214
|
+
if (opts.maxAge > 0) {
|
|
215
|
+
cacheControl += `, max-age=${opts.maxAge}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (opts.immutable) {
|
|
219
|
+
cacheControl += ', immutable';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
res.setHeader('Cache-Control', cacheControl);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const mimeType = getMimeType(filePath);
|
|
226
|
+
res.setHeader('Content-Type', mimeType);
|
|
227
|
+
res.setHeader('Content-Length', stats.size);
|
|
228
|
+
|
|
229
|
+
if (typeof opts.setHeaders === 'function') {
|
|
230
|
+
opts.setHeaders(res, filePath, stats);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (req.method === 'HEAD') {
|
|
234
|
+
return res.end();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const data = await fs.promises.readFile(filePath);
|
|
238
|
+
res.end(data);
|
|
239
|
+
return;
|
|
240
|
+
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error('Static middleware error:', error);
|
|
243
|
+
if (opts.fallthrough) return next();
|
|
244
|
+
res.status(500).send('Internal Server Error');
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
};
|
package/lieko-express.js
CHANGED
|
@@ -64,124 +64,20 @@ class LiekoExpress {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
cors(options = {}) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
...options
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
_matchOrigin(origin, allowedOrigin) {
|
|
75
|
-
if (!origin || !allowedOrigin) return false;
|
|
76
|
-
|
|
77
|
-
if (Array.isArray(allowedOrigin)) {
|
|
78
|
-
return allowedOrigin.some(o => this._matchOrigin(origin, o));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (allowedOrigin === "*") return true;
|
|
82
|
-
|
|
83
|
-
// Wildcard https://*.example.com
|
|
84
|
-
if (allowedOrigin.includes("*")) {
|
|
85
|
-
const regex = new RegExp("^" + allowedOrigin
|
|
86
|
-
.replace(/\./g, "\\.")
|
|
87
|
-
.replace(/\*/g, ".*") + "$");
|
|
88
|
-
|
|
89
|
-
return regex.test(origin);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return origin === allowedOrigin;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
_applyCors(req, res, opts) {
|
|
96
|
-
if (!opts || !opts.enabled) return;
|
|
97
|
-
|
|
98
|
-
const requestOrigin = req.headers.origin || "";
|
|
99
|
-
|
|
100
|
-
let finalOrigin = "*";
|
|
101
|
-
|
|
102
|
-
if (opts.strictOrigin && requestOrigin) {
|
|
103
|
-
const allowed = this._matchOrigin(requestOrigin, opts.origin);
|
|
104
|
-
|
|
105
|
-
if (!allowed) {
|
|
106
|
-
res.statusCode = 403;
|
|
107
|
-
return res.end(JSON.stringify({
|
|
108
|
-
success: false,
|
|
109
|
-
error: "Origin Forbidden",
|
|
110
|
-
message: `Origin "${requestOrigin}" is not allowed`
|
|
111
|
-
}));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (opts.origin === "*") {
|
|
116
|
-
finalOrigin = "*";
|
|
117
|
-
} else if (Array.isArray(opts.origin)) {
|
|
118
|
-
const match = opts.origin.find(o => this._matchOrigin(requestOrigin, o));
|
|
119
|
-
finalOrigin = match || opts.origin[0];
|
|
120
|
-
} else {
|
|
121
|
-
finalOrigin = this._matchOrigin(requestOrigin, opts.origin)
|
|
122
|
-
? requestOrigin
|
|
123
|
-
: opts.origin;
|
|
67
|
+
if (options === false) {
|
|
68
|
+
this._corsMiddleware = null;
|
|
69
|
+
return this;
|
|
124
70
|
}
|
|
125
71
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
origin: finalOrigin
|
|
129
|
-
});
|
|
72
|
+
const middleware = require('./lib/cors')(options);
|
|
73
|
+
this._corsMiddleware = middleware;
|
|
130
74
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (opts.exposedHeaders?.length) {
|
|
138
|
-
res.setHeader("Access-Control-Expose-Headers",
|
|
139
|
-
opts.exposedHeaders.join(", "));
|
|
75
|
+
const stack = new Error().stack;
|
|
76
|
+
if (!stack.includes('at LiekoExpress.use')) {
|
|
77
|
+
this.use(middleware);
|
|
140
78
|
}
|
|
141
79
|
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
opts.allowPrivateNetwork &&
|
|
145
|
-
req.headers["access-control-request-private-network"] === "true"
|
|
146
|
-
) {
|
|
147
|
-
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (req.method === "OPTIONS") {
|
|
151
|
-
res.setHeader("Access-Control-Allow-Methods", opts.methods.join(", "));
|
|
152
|
-
res.setHeader("Access-Control-Allow-Headers", opts.headers.join(", "));
|
|
153
|
-
res.setHeader("Access-Control-Max-Age", opts.maxAge);
|
|
154
|
-
|
|
155
|
-
res.statusCode = 204;
|
|
156
|
-
return res.end();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
_logCorsDebug(req, opts) {
|
|
161
|
-
if (!opts.debug) return;
|
|
162
|
-
|
|
163
|
-
console.log("\n[CORS DEBUG]");
|
|
164
|
-
console.log("Request:", req.method, req.url);
|
|
165
|
-
console.log("Origin:", req.headers.origin || "none");
|
|
166
|
-
|
|
167
|
-
console.log("Applied CORS Policy:");
|
|
168
|
-
console.log(" - Access-Control-Allow-Origin:", opts.origin);
|
|
169
|
-
console.log(" - Access-Control-Allow-Methods:", opts.methods.join(", "));
|
|
170
|
-
console.log(" - Access-Control-Allow-Headers:", opts.headers.join(", "));
|
|
171
|
-
|
|
172
|
-
if (opts.credentials) {
|
|
173
|
-
console.log(" - Access-Control-Allow-Credentials: true");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (opts.exposedHeaders?.length) {
|
|
177
|
-
console.log(" - Access-Control-Expose-Headers:", opts.exposedHeaders.join(", "));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
console.log(" - Max-Age:", opts.maxAge);
|
|
181
|
-
|
|
182
|
-
if (req.method === "OPTIONS") {
|
|
183
|
-
console.log("Preflight request handled with status 204\n");
|
|
184
|
-
}
|
|
80
|
+
return middleware;
|
|
185
81
|
}
|
|
186
82
|
|
|
187
83
|
debug(value = true) {
|
|
@@ -708,248 +604,7 @@ ${cyan} (req, res, next) => {
|
|
|
708
604
|
}
|
|
709
605
|
|
|
710
606
|
static(root, options = {}) {
|
|
711
|
-
|
|
712
|
-
maxAge: options.maxAge || 0,
|
|
713
|
-
index: options.index !== undefined ? options.index : 'index.html',
|
|
714
|
-
dotfiles: options.dotfiles || 'ignore',
|
|
715
|
-
etag: options.etag !== undefined ? options.etag : true,
|
|
716
|
-
extensions: options.extensions || false,
|
|
717
|
-
fallthrough: options.fallthrough !== undefined ? options.fallthrough : true,
|
|
718
|
-
immutable: options.immutable || false,
|
|
719
|
-
lastModified: options.lastModified !== undefined ? options.lastModified : true,
|
|
720
|
-
redirect: options.redirect !== undefined ? options.redirect : true,
|
|
721
|
-
setHeaders: options.setHeaders || null,
|
|
722
|
-
cacheControl: options.cacheControl !== undefined ? options.cacheControl : true
|
|
723
|
-
};
|
|
724
|
-
|
|
725
|
-
const mimeTypes = {
|
|
726
|
-
'.html': 'text/html; charset=utf-8',
|
|
727
|
-
'.htm': 'text/html; charset=utf-8',
|
|
728
|
-
'.css': 'text/css; charset=utf-8',
|
|
729
|
-
'.js': 'application/javascript; charset=utf-8',
|
|
730
|
-
'.mjs': 'application/javascript; charset=utf-8',
|
|
731
|
-
'.json': 'application/json; charset=utf-8',
|
|
732
|
-
'.xml': 'application/xml; charset=utf-8',
|
|
733
|
-
'.txt': 'text/plain; charset=utf-8',
|
|
734
|
-
'.md': 'text/markdown; charset=utf-8',
|
|
735
|
-
'.jpg': 'image/jpeg',
|
|
736
|
-
'.jpeg': 'image/jpeg',
|
|
737
|
-
'.png': 'image/png',
|
|
738
|
-
'.gif': 'image/gif',
|
|
739
|
-
'.svg': 'image/svg+xml',
|
|
740
|
-
'.webp': 'image/webp',
|
|
741
|
-
'.ico': 'image/x-icon',
|
|
742
|
-
'.bmp': 'image/bmp',
|
|
743
|
-
'.tiff': 'image/tiff',
|
|
744
|
-
'.tif': 'image/tiff',
|
|
745
|
-
'.mp3': 'audio/mpeg',
|
|
746
|
-
'.wav': 'audio/wav',
|
|
747
|
-
'.ogg': 'audio/ogg',
|
|
748
|
-
'.m4a': 'audio/mp4',
|
|
749
|
-
'.aac': 'audio/aac',
|
|
750
|
-
'.flac': 'audio/flac',
|
|
751
|
-
'.mp4': 'video/mp4',
|
|
752
|
-
'.webm': 'video/webm',
|
|
753
|
-
'.ogv': 'video/ogg',
|
|
754
|
-
'.avi': 'video/x-msvideo',
|
|
755
|
-
'.mov': 'video/quicktime',
|
|
756
|
-
'.wmv': 'video/x-ms-wmv',
|
|
757
|
-
'.flv': 'video/x-flv',
|
|
758
|
-
'.mkv': 'video/x-matroska',
|
|
759
|
-
'.woff': 'font/woff',
|
|
760
|
-
'.woff2': 'font/woff2',
|
|
761
|
-
'.ttf': 'font/ttf',
|
|
762
|
-
'.otf': 'font/otf',
|
|
763
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
764
|
-
'.zip': 'application/zip',
|
|
765
|
-
'.rar': 'application/x-rar-compressed',
|
|
766
|
-
'.tar': 'application/x-tar',
|
|
767
|
-
'.gz': 'application/gzip',
|
|
768
|
-
'.7z': 'application/x-7z-compressed',
|
|
769
|
-
'.pdf': 'application/pdf',
|
|
770
|
-
'.doc': 'application/msword',
|
|
771
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
772
|
-
'.xls': 'application/vnd.ms-excel',
|
|
773
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
774
|
-
'.ppt': 'application/vnd.ms-powerpoint',
|
|
775
|
-
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
776
|
-
'.wasm': 'application/wasm',
|
|
777
|
-
'.csv': 'text/csv; charset=utf-8'
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
const getMimeType = (filePath) => {
|
|
781
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
782
|
-
return mimeTypes[ext] || 'application/octet-stream';
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
const generateETag = (stats) => {
|
|
786
|
-
const mtime = stats.mtime.getTime().toString(16);
|
|
787
|
-
const size = stats.size.toString(16);
|
|
788
|
-
return `W/"${size}-${mtime}"`;
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
return async (req, res, next) => {
|
|
792
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
793
|
-
return next();
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
try {
|
|
797
|
-
let pathname = req.url;
|
|
798
|
-
const qIndex = pathname.indexOf('?');
|
|
799
|
-
if (qIndex !== -1) {
|
|
800
|
-
pathname = pathname.substring(0, qIndex);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
if (pathname === '') {
|
|
804
|
-
pathname = '/';
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
try {
|
|
808
|
-
pathname = decodeURIComponent(pathname);
|
|
809
|
-
} catch (e) {
|
|
810
|
-
if (opts.fallthrough) return next();
|
|
811
|
-
return res.status(400).send('Bad Request');
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
let filePath = pathname === '/' ? root : path.join(root, pathname);
|
|
815
|
-
|
|
816
|
-
const resolvedPath = path.resolve(filePath);
|
|
817
|
-
const resolvedRoot = path.resolve(root);
|
|
818
|
-
|
|
819
|
-
if (!resolvedPath.startsWith(resolvedRoot)) {
|
|
820
|
-
if (opts.fallthrough) return next();
|
|
821
|
-
return res.status(403).send('Forbidden');
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
let stats;
|
|
825
|
-
try {
|
|
826
|
-
stats = await fs.promises.stat(filePath);
|
|
827
|
-
} catch (err) {
|
|
828
|
-
if (pathname === '/' && opts.index) {
|
|
829
|
-
const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
|
|
830
|
-
for (const indexFile of indexes) {
|
|
831
|
-
const indexPath = path.join(root, indexFile);
|
|
832
|
-
try {
|
|
833
|
-
stats = await fs.promises.stat(indexPath);
|
|
834
|
-
if (stats.isFile()) {
|
|
835
|
-
filePath = indexPath;
|
|
836
|
-
break;
|
|
837
|
-
}
|
|
838
|
-
} catch (e) { }
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (!stats && opts.extensions && Array.isArray(opts.extensions)) {
|
|
843
|
-
let found = false;
|
|
844
|
-
for (const ext of opts.extensions) {
|
|
845
|
-
const testPath = filePath + (ext.startsWith('.') ? ext : '.' + ext);
|
|
846
|
-
try {
|
|
847
|
-
stats = await fs.promises.stat(testPath);
|
|
848
|
-
filePath = testPath;
|
|
849
|
-
found = true;
|
|
850
|
-
break;
|
|
851
|
-
} catch (e) { }
|
|
852
|
-
}
|
|
853
|
-
if (!found) return next();
|
|
854
|
-
} else if (!stats) {
|
|
855
|
-
return next();
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
if (stats.isDirectory()) {
|
|
860
|
-
if (opts.redirect && !pathname.endsWith('/')) {
|
|
861
|
-
const query = qIndex !== -1 ? req.url.substring(qIndex) : '';
|
|
862
|
-
const redirectUrl = pathname + '/' + query;
|
|
863
|
-
return res.redirect(redirectUrl, 301);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
if (opts.index) {
|
|
867
|
-
const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
|
|
868
|
-
|
|
869
|
-
for (const indexFile of indexes) {
|
|
870
|
-
const indexPath = path.join(filePath, indexFile);
|
|
871
|
-
try {
|
|
872
|
-
const indexStats = await fs.promises.stat(indexPath);
|
|
873
|
-
if (indexStats.isFile()) {
|
|
874
|
-
filePath = indexPath;
|
|
875
|
-
stats = indexStats;
|
|
876
|
-
break;
|
|
877
|
-
}
|
|
878
|
-
} catch (e) { }
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
if (stats.isDirectory()) {
|
|
882
|
-
if (opts.fallthrough) return next();
|
|
883
|
-
return res.status(404).send('Not Found');
|
|
884
|
-
}
|
|
885
|
-
} else {
|
|
886
|
-
if (opts.fallthrough) return next();
|
|
887
|
-
return res.status(404).send('Not Found');
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
if (opts.etag) {
|
|
892
|
-
const etag = generateETag(stats);
|
|
893
|
-
const ifNoneMatch = req.headers['if-none-match'];
|
|
894
|
-
|
|
895
|
-
if (ifNoneMatch === etag) {
|
|
896
|
-
res.statusCode = 304;
|
|
897
|
-
res.end();
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
res.setHeader('ETag', etag);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
if (opts.lastModified) {
|
|
905
|
-
const lastModified = stats.mtime.toUTCString();
|
|
906
|
-
const ifModifiedSince = req.headers['if-modified-since'];
|
|
907
|
-
|
|
908
|
-
if (ifModifiedSince === lastModified) {
|
|
909
|
-
res.statusCode = 304;
|
|
910
|
-
res.end();
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
res.setHeader('Last-Modified', lastModified);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
if (opts.cacheControl) {
|
|
918
|
-
let cacheControl = 'public';
|
|
919
|
-
|
|
920
|
-
if (opts.maxAge > 0) {
|
|
921
|
-
cacheControl += `, max-age=${opts.maxAge}`;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (opts.immutable) {
|
|
925
|
-
cacheControl += ', immutable';
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
res.setHeader('Cache-Control', cacheControl);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const mimeType = getMimeType(filePath);
|
|
932
|
-
res.setHeader('Content-Type', mimeType);
|
|
933
|
-
res.setHeader('Content-Length', stats.size);
|
|
934
|
-
|
|
935
|
-
if (typeof opts.setHeaders === 'function') {
|
|
936
|
-
opts.setHeaders(res, filePath, stats);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
if (req.method === 'HEAD') {
|
|
940
|
-
return res.end();
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
const data = await fs.promises.readFile(filePath);
|
|
944
|
-
res.end(data);
|
|
945
|
-
return;
|
|
946
|
-
|
|
947
|
-
} catch (error) {
|
|
948
|
-
console.error('Static middleware error:', error);
|
|
949
|
-
if (opts.fallthrough) return next();
|
|
950
|
-
res.status(500).send('Internal Server Error');
|
|
951
|
-
}
|
|
952
|
-
};
|
|
607
|
+
return require('./lib/static')(root, options);
|
|
953
608
|
}
|
|
954
609
|
|
|
955
610
|
_mountRouter(basePath, router) {
|
|
@@ -1426,6 +1081,90 @@ ${cyan} (req, res, next) => {
|
|
|
1426
1081
|
return undefined;
|
|
1427
1082
|
};
|
|
1428
1083
|
req.header = req.get;
|
|
1084
|
+
|
|
1085
|
+
const parseAccept = (header) => {
|
|
1086
|
+
if (!header) return [];
|
|
1087
|
+
return header
|
|
1088
|
+
.split(',')
|
|
1089
|
+
.map(part => {
|
|
1090
|
+
const [type, ...rest] = part.trim().split(';');
|
|
1091
|
+
const q = rest
|
|
1092
|
+
.find(p => p.trim().startsWith('q='))
|
|
1093
|
+
?.split('=')[1];
|
|
1094
|
+
const quality = q ? parseFloat(q) : 1.0;
|
|
1095
|
+
return { type: type.trim().toLowerCase(), quality };
|
|
1096
|
+
})
|
|
1097
|
+
.filter(item => item.quality > 0)
|
|
1098
|
+
.sort((a, b) => b.quality - a.quality)
|
|
1099
|
+
.map(item => item.type);
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
const accepts = (types) => {
|
|
1103
|
+
if (!Array.isArray(types)) types = [types];
|
|
1104
|
+
const accepted = parseAccept(req.headers['accept']);
|
|
1105
|
+
|
|
1106
|
+
for (const type of types) {
|
|
1107
|
+
const t = type.toLowerCase();
|
|
1108
|
+
|
|
1109
|
+
if (accepted.includes(t)) return type;
|
|
1110
|
+
|
|
1111
|
+
if (accepted.some(a => {
|
|
1112
|
+
if (a === '*/*') return true;
|
|
1113
|
+
if (a.endsWith('/*')) {
|
|
1114
|
+
const prefix = a.slice(0, -1);
|
|
1115
|
+
return t.startsWith(prefix);
|
|
1116
|
+
}
|
|
1117
|
+
return false;
|
|
1118
|
+
})) {
|
|
1119
|
+
return type;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
return false;
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
req.accepts = function (types) {
|
|
1127
|
+
return accepts(types);
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
req.acceptsLanguages = function (langs) {
|
|
1131
|
+
if (!Array.isArray(langs)) langs = [langs];
|
|
1132
|
+
const accepted = parseAccept(req.headers['accept-language'] || '');
|
|
1133
|
+
for (const lang of langs) {
|
|
1134
|
+
const l = lang.toLowerCase();
|
|
1135
|
+
if (accepted.some(a => a === l || a.startsWith(l + '-'))) return lang;
|
|
1136
|
+
}
|
|
1137
|
+
return false;
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
req.acceptsEncodings = function (encodings) {
|
|
1141
|
+
if (!Array.isArray(encodings)) encodings = [encodings];
|
|
1142
|
+
const accepted = parseAccept(req.headers['accept-encoding'] || '');
|
|
1143
|
+
for (const enc of encodings) {
|
|
1144
|
+
if (accepted.includes(enc.toLowerCase())) return enc;
|
|
1145
|
+
}
|
|
1146
|
+
return false;
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
req.acceptsCharsets = function (charsets) {
|
|
1150
|
+
if (!Array.isArray(charsets)) charsets = [charsets];
|
|
1151
|
+
const accepted = parseAccept(req.headers['accept-charset'] || '');
|
|
1152
|
+
for (const charset of charsets) {
|
|
1153
|
+
if (accepted.includes(charset.toLowerCase())) return charset;
|
|
1154
|
+
}
|
|
1155
|
+
return false;
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
req.is = function (type) {
|
|
1159
|
+
const ct = (req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
1160
|
+
if (!type) return ct;
|
|
1161
|
+
const t = type.toLowerCase();
|
|
1162
|
+
if (t.includes('/')) return ct === t;
|
|
1163
|
+
if (t === 'json') return ct.includes('json');
|
|
1164
|
+
if (t === 'urlencoded') return ct.includes('x-www-form-urlencoded');
|
|
1165
|
+
if (t === 'multipart') return ct.includes('multipart');
|
|
1166
|
+
return false;
|
|
1167
|
+
};
|
|
1429
1168
|
}
|
|
1430
1169
|
|
|
1431
1170
|
_enhanceResponse(req, res) {
|
|
@@ -1504,6 +1243,7 @@ ${cyan} (req, res, next) => {
|
|
|
1504
1243
|
const locals = { ...res.locals, ...options };
|
|
1505
1244
|
let viewPath = view;
|
|
1506
1245
|
let ext = path.extname(view);
|
|
1246
|
+
console.log("EXT: ", ext)
|
|
1507
1247
|
|
|
1508
1248
|
if (!ext) {
|
|
1509
1249
|
ext = this.settings['view engine'];
|
|
@@ -1518,11 +1258,13 @@ ${cyan} (req, res, next) => {
|
|
|
1518
1258
|
|
|
1519
1259
|
const viewsDir = this.settings.views || path.join(process.cwd(), 'views');
|
|
1520
1260
|
let fullPath = path.join(viewsDir, viewPath);
|
|
1261
|
+
console.log(fullPath)
|
|
1521
1262
|
let fileExists = false;
|
|
1522
1263
|
try {
|
|
1523
1264
|
await fs.promises.access(fullPath);
|
|
1524
1265
|
fileExists = true;
|
|
1525
1266
|
} catch (err) {
|
|
1267
|
+
console.log(err)
|
|
1526
1268
|
const extensions = ['.html', '.ejs', '.pug', '.hbs'];
|
|
1527
1269
|
for (const tryExt of extensions) {
|
|
1528
1270
|
if (tryExt === ext) continue;
|
|
@@ -1841,7 +1583,7 @@ ${cyan} (req, res, next) => {
|
|
|
1841
1583
|
logLines.push('---------------------------------------------');
|
|
1842
1584
|
console.log('\n' + logLines.join('\n') + '\n');
|
|
1843
1585
|
}
|
|
1844
|
-
|
|
1586
|
+
|
|
1845
1587
|
listRoutes() {
|
|
1846
1588
|
const routeEntries = [];
|
|
1847
1589
|
|
|
@@ -1916,7 +1658,6 @@ ${cyan} (req, res, next) => {
|
|
|
1916
1658
|
listen() {
|
|
1917
1659
|
const args = Array.from(arguments);
|
|
1918
1660
|
const server = createServer(this._handleRequest.bind(this));
|
|
1919
|
-
|
|
1920
1661
|
server.listen.apply(server, args);
|
|
1921
1662
|
this.server = server;
|
|
1922
1663
|
return server;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lieko-express",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/eiwSrvt/lieko-express"
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"lieko-express.js",
|
|
20
20
|
"lieko-express.d.ts",
|
|
21
|
-
"README.md"
|
|
21
|
+
"README.md",
|
|
22
|
+
"lib"
|
|
22
23
|
]
|
|
23
24
|
}
|