lieko-express 0.0.8 → 0.0.10
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 +121 -355
- 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
|
@@ -61,127 +61,53 @@ class LiekoExpress {
|
|
|
61
61
|
exposedHeaders: [],
|
|
62
62
|
debug: false
|
|
63
63
|
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
cors(options = {}) {
|
|
67
|
-
this.corsOptions = {
|
|
68
|
-
...this.corsOptions,
|
|
69
|
-
enabled: true,
|
|
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
64
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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;
|
|
65
|
+
this.excludedPatterns = [
|
|
66
|
+
/^\/\.well-known\/.*/i // Chrome DevTools, Apple, etc.
|
|
67
|
+
]
|
|
93
68
|
}
|
|
94
69
|
|
|
95
|
-
|
|
96
|
-
if (!
|
|
97
|
-
|
|
98
|
-
const requestOrigin = req.headers.origin || "";
|
|
70
|
+
excludeUrl(patterns) {
|
|
71
|
+
if (!Array.isArray(patterns)) patterns = [patterns];
|
|
72
|
+
this.excludedPatterns = this.excludedPatterns || [];
|
|
99
73
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
}));
|
|
74
|
+
patterns.forEach(pattern => {
|
|
75
|
+
if (pattern instanceof RegExp) {
|
|
76
|
+
this.excludedPatterns.push(pattern);
|
|
77
|
+
return;
|
|
112
78
|
}
|
|
113
|
-
}
|
|
114
79
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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;
|
|
124
|
-
}
|
|
80
|
+
let regexStr = pattern
|
|
81
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
82
|
+
.replace(/\\\*/g, '.*');
|
|
125
83
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
origin: finalOrigin
|
|
84
|
+
regexStr = '^' + regexStr + '$';
|
|
85
|
+
this.excludedPatterns.push(new RegExp(regexStr, 'i'));
|
|
129
86
|
});
|
|
130
87
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (opts.credentials) {
|
|
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(", "));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Chrome Private Network Access
|
|
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
|
-
}
|
|
88
|
+
return this;
|
|
158
89
|
}
|
|
159
90
|
|
|
160
|
-
|
|
161
|
-
if (!
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
}
|
|
91
|
+
_isExcluded(url) {
|
|
92
|
+
if (!this.excludedPatterns?.length) return false;
|
|
93
|
+
return this.excludedPatterns.some(re => re.test(url));
|
|
94
|
+
}
|
|
175
95
|
|
|
176
|
-
|
|
177
|
-
|
|
96
|
+
cors(options = {}) {
|
|
97
|
+
if (options === false) {
|
|
98
|
+
this._corsMiddleware = null;
|
|
99
|
+
return this;
|
|
178
100
|
}
|
|
179
101
|
|
|
180
|
-
|
|
102
|
+
const middleware = require('./lib/cors')(options);
|
|
103
|
+
this._corsMiddleware = middleware;
|
|
181
104
|
|
|
182
|
-
|
|
183
|
-
|
|
105
|
+
const stack = new Error().stack;
|
|
106
|
+
if (!stack.includes('at LiekoExpress.use')) {
|
|
107
|
+
this.use(middleware);
|
|
184
108
|
}
|
|
109
|
+
|
|
110
|
+
return middleware;
|
|
185
111
|
}
|
|
186
112
|
|
|
187
113
|
debug(value = true) {
|
|
@@ -708,248 +634,7 @@ ${cyan} (req, res, next) => {
|
|
|
708
634
|
}
|
|
709
635
|
|
|
710
636
|
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
|
-
};
|
|
637
|
+
return require('./lib/static')(root, options);
|
|
953
638
|
}
|
|
954
639
|
|
|
955
640
|
_mountRouter(basePath, router) {
|
|
@@ -1226,6 +911,10 @@ ${cyan} (req, res, next) => {
|
|
|
1226
911
|
}
|
|
1227
912
|
|
|
1228
913
|
async _handleRequest(req, res) {
|
|
914
|
+
if (this._isExcluded(req.url.split('?')[0])) {
|
|
915
|
+
return res.status(404).end();
|
|
916
|
+
}
|
|
917
|
+
|
|
1229
918
|
this._enhanceRequest(req);
|
|
1230
919
|
|
|
1231
920
|
const url = req.url;
|
|
@@ -1426,6 +1115,90 @@ ${cyan} (req, res, next) => {
|
|
|
1426
1115
|
return undefined;
|
|
1427
1116
|
};
|
|
1428
1117
|
req.header = req.get;
|
|
1118
|
+
|
|
1119
|
+
const parseAccept = (header) => {
|
|
1120
|
+
if (!header) return [];
|
|
1121
|
+
return header
|
|
1122
|
+
.split(',')
|
|
1123
|
+
.map(part => {
|
|
1124
|
+
const [type, ...rest] = part.trim().split(';');
|
|
1125
|
+
const q = rest
|
|
1126
|
+
.find(p => p.trim().startsWith('q='))
|
|
1127
|
+
?.split('=')[1];
|
|
1128
|
+
const quality = q ? parseFloat(q) : 1.0;
|
|
1129
|
+
return { type: type.trim().toLowerCase(), quality };
|
|
1130
|
+
})
|
|
1131
|
+
.filter(item => item.quality > 0)
|
|
1132
|
+
.sort((a, b) => b.quality - a.quality)
|
|
1133
|
+
.map(item => item.type);
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
const accepts = (types) => {
|
|
1137
|
+
if (!Array.isArray(types)) types = [types];
|
|
1138
|
+
const accepted = parseAccept(req.headers['accept']);
|
|
1139
|
+
|
|
1140
|
+
for (const type of types) {
|
|
1141
|
+
const t = type.toLowerCase();
|
|
1142
|
+
|
|
1143
|
+
if (accepted.includes(t)) return type;
|
|
1144
|
+
|
|
1145
|
+
if (accepted.some(a => {
|
|
1146
|
+
if (a === '*/*') return true;
|
|
1147
|
+
if (a.endsWith('/*')) {
|
|
1148
|
+
const prefix = a.slice(0, -1);
|
|
1149
|
+
return t.startsWith(prefix);
|
|
1150
|
+
}
|
|
1151
|
+
return false;
|
|
1152
|
+
})) {
|
|
1153
|
+
return type;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return false;
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
req.accepts = function (types) {
|
|
1161
|
+
return accepts(types);
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
req.acceptsLanguages = function (langs) {
|
|
1165
|
+
if (!Array.isArray(langs)) langs = [langs];
|
|
1166
|
+
const accepted = parseAccept(req.headers['accept-language'] || '');
|
|
1167
|
+
for (const lang of langs) {
|
|
1168
|
+
const l = lang.toLowerCase();
|
|
1169
|
+
if (accepted.some(a => a === l || a.startsWith(l + '-'))) return lang;
|
|
1170
|
+
}
|
|
1171
|
+
return false;
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
req.acceptsEncodings = function (encodings) {
|
|
1175
|
+
if (!Array.isArray(encodings)) encodings = [encodings];
|
|
1176
|
+
const accepted = parseAccept(req.headers['accept-encoding'] || '');
|
|
1177
|
+
for (const enc of encodings) {
|
|
1178
|
+
if (accepted.includes(enc.toLowerCase())) return enc;
|
|
1179
|
+
}
|
|
1180
|
+
return false;
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
req.acceptsCharsets = function (charsets) {
|
|
1184
|
+
if (!Array.isArray(charsets)) charsets = [charsets];
|
|
1185
|
+
const accepted = parseAccept(req.headers['accept-charset'] || '');
|
|
1186
|
+
for (const charset of charsets) {
|
|
1187
|
+
if (accepted.includes(charset.toLowerCase())) return charset;
|
|
1188
|
+
}
|
|
1189
|
+
return false;
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
req.is = function (type) {
|
|
1193
|
+
const ct = (req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
1194
|
+
if (!type) return ct;
|
|
1195
|
+
const t = type.toLowerCase();
|
|
1196
|
+
if (t.includes('/')) return ct === t;
|
|
1197
|
+
if (t === 'json') return ct.includes('json');
|
|
1198
|
+
if (t === 'urlencoded') return ct.includes('x-www-form-urlencoded');
|
|
1199
|
+
if (t === 'multipart') return ct.includes('multipart');
|
|
1200
|
+
return false;
|
|
1201
|
+
};
|
|
1429
1202
|
}
|
|
1430
1203
|
|
|
1431
1204
|
_enhanceResponse(req, res) {
|
|
@@ -1572,12 +1345,6 @@ ${cyan} (req, res, next) => {
|
|
|
1572
1345
|
callback(null, html);
|
|
1573
1346
|
resolve();
|
|
1574
1347
|
} else {
|
|
1575
|
-
/*
|
|
1576
|
-
HBS cause error header already sent here ??
|
|
1577
|
-
*/
|
|
1578
|
-
//res.statusCode = statusCode || 200;
|
|
1579
|
-
//res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1580
|
-
//responseSent = true;
|
|
1581
1348
|
res.html(html);
|
|
1582
1349
|
resolve();
|
|
1583
1350
|
}
|
|
@@ -1841,7 +1608,7 @@ ${cyan} (req, res, next) => {
|
|
|
1841
1608
|
logLines.push('---------------------------------------------');
|
|
1842
1609
|
console.log('\n' + logLines.join('\n') + '\n');
|
|
1843
1610
|
}
|
|
1844
|
-
|
|
1611
|
+
|
|
1845
1612
|
listRoutes() {
|
|
1846
1613
|
const routeEntries = [];
|
|
1847
1614
|
|
|
@@ -1916,7 +1683,6 @@ ${cyan} (req, res, next) => {
|
|
|
1916
1683
|
listen() {
|
|
1917
1684
|
const args = Array.from(arguments);
|
|
1918
1685
|
const server = createServer(this._handleRequest.bind(this));
|
|
1919
|
-
|
|
1920
1686
|
server.listen.apply(server, args);
|
|
1921
1687
|
this.server = server;
|
|
1922
1688
|
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.10",
|
|
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
|
}
|