ultimate-express 2.0.9 → 2.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/src/utils.js CHANGED
@@ -1,372 +1,372 @@
1
- /*
2
- Copyright 2024 dimden.dev
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- const mime = require("mime-types");
18
- const path = require("path");
19
- const proxyaddr = require("proxy-addr");
20
- const qs = require("qs");
21
- const querystring = require("fast-querystring");
22
- const etag = require("etag");
23
- const { Stats } = require("fs");
24
-
25
- const EMPTY_REGEX = new RegExp(``);
26
-
27
- function fastQueryParse(query, options) {
28
- const len = query.length;
29
- if(len === 0){
30
- return new NullObject();
31
- }
32
- if(len <= 128) {
33
- if(!query.includes('[') && !query.includes('%5B') && !query.includes('.') && !query.includes('%2E')) {
34
- // [Object: null prototype] issue
35
- return {...querystring.parse(query)};
36
- }
37
- }
38
- // [Object: null prototype] issue
39
- return {...qs.parse(query, options)};
40
- }
41
-
42
- function removeDuplicateSlashes(path) {
43
- return path.replace(/\/{2,}/g, '/');
44
- }
45
-
46
- function patternToRegex(pattern, isPrefix = false) {
47
- if(pattern instanceof RegExp) {
48
- return pattern;
49
- }
50
- if(isPrefix && pattern === '') {
51
- return EMPTY_REGEX;
52
- }
53
-
54
- let regexPattern = pattern
55
- .replaceAll('.', '\\.')
56
- .replaceAll('-', '\\-')
57
- .replaceAll('*', '(.*)') // Convert * to .*
58
- .replace(/\/:(\w+)(\(.+?\))?\??/g, (match, param, regex) => {
59
- const optional = match.endsWith('?');
60
- return `\\/${optional ? '?' : ''}(?<${param}>${regex ? regex + '($|\\/)' : '[^/]+'})${optional ? '?' : ''}`;
61
- }); // Convert :param to capture group
62
-
63
- return new RegExp(`^${regexPattern}${isPrefix ? '(?=$|\/)' : '$'}`);
64
- }
65
-
66
- function needsConversionToRegex(pattern) {
67
- if(pattern instanceof RegExp) {
68
- return false;
69
- }
70
- if(pattern === '/*') {
71
- return false;
72
- }
73
-
74
- return pattern.includes('*') ||
75
- pattern.includes('?') ||
76
- pattern.includes('+') ||
77
- pattern.includes('(') ||
78
- pattern.includes(')') ||
79
- pattern.includes(':') ||
80
- pattern.includes('{') ||
81
- pattern.includes('}') ||
82
- pattern.includes('[') ||
83
- pattern.includes(']');
84
- }
85
-
86
- function canBeOptimized(pattern) {
87
- if(pattern === '/*') {
88
- return false;
89
- }
90
- if(pattern instanceof RegExp) {
91
- return false;
92
- }
93
- if(
94
- pattern.includes('*') ||
95
- pattern.includes('?') ||
96
- pattern.includes('+') ||
97
- pattern.includes('(') ||
98
- pattern.includes(')') ||
99
- pattern.includes('{') ||
100
- pattern.includes('}') ||
101
- pattern.includes('[') ||
102
- pattern.includes(']')
103
- ) {
104
- return false;
105
- }
106
- return true;
107
- }
108
-
109
- function acceptParams(str) {
110
- const length = str.length;
111
- const colonIndex = str.indexOf(';');
112
- let index = colonIndex === -1 ? length : colonIndex;
113
- const ret = { value: str.slice(0, index).trim(), quality: 1, params: {} };
114
-
115
- while (index < length) {
116
- const splitIndex = str.indexOf('=', index);
117
- if (splitIndex === -1) break;
118
-
119
- const colonIndex = str.indexOf(';', index);
120
- const endIndex = colonIndex === -1 ? length : colonIndex;
121
-
122
- if (splitIndex > endIndex) {
123
- index = str.lastIndexOf(';', splitIndex - 1) + 1;
124
- continue;
125
- }
126
-
127
- const key = str.slice(index, splitIndex).trim();
128
- const value = str.slice(splitIndex + 1, endIndex).trim();
129
-
130
- if (key === 'q') {
131
- ret.quality = parseFloat(value);
132
- } else {
133
- ret.params[key] = value;
134
- }
135
-
136
- index = endIndex + 1;
137
- }
138
-
139
- return ret;
140
- }
141
-
142
- function normalizeType(type) {
143
- return ~type.indexOf('/') ?
144
- acceptParams(type) :
145
- { value: (mime.lookup(type) || 'application/octet-stream'), params: {} };
146
- }
147
-
148
- function stringify(value, replacer, spaces, escape) {
149
- let json = replacer || spaces
150
- ? JSON.stringify(value, replacer, spaces)
151
- : JSON.stringify(value);
152
-
153
- if (escape && typeof json === 'string') {
154
- json = json.replace(/[<>&]/g, function (c) {
155
- switch (c.charCodeAt(0)) {
156
- case 0x3c:
157
- return '\\u003c'
158
- case 0x3e:
159
- return '\\u003e'
160
- case 0x26:
161
- return '\\u0026'
162
- default:
163
- return c
164
- }
165
- });
166
- }
167
-
168
- return json;
169
- }
170
-
171
- const defaultSettings = {
172
- 'jsonp callback name': 'callback',
173
- 'env': () => process.env.NODE_ENV ?? 'development',
174
- 'etag': 'weak',
175
- 'etag fn': () => createETagGenerator({ weak: true }),
176
- 'query parser': 'extended',
177
- 'query parser fn': () => fastQueryParse,
178
- 'subdomain offset': 2,
179
- 'trust proxy': false,
180
- 'views': () => path.join(process.cwd(), 'views'),
181
- 'view cache': () => process.env.NODE_ENV === 'production',
182
- 'x-powered-by': true,
183
- 'case sensitive routing': true,
184
- 'declarative responses': true
185
- };
186
-
187
- function compileTrust(val) {
188
- if (typeof val === 'function') return val;
189
-
190
- if (val === true) {
191
- // Support plain true/false
192
- return function(){ return true };
193
- }
194
-
195
- if (typeof val === 'number') {
196
- // Support trusting hop count
197
- return function(a, i){ return i < val };
198
- }
199
-
200
- if (typeof val === 'string') {
201
- // Support comma-separated values
202
- val = val.split(',')
203
- .map(function (v) { return v.trim() })
204
- }
205
-
206
- return proxyaddr.compile(val || []);
207
- }
208
-
209
- const shownWarnings = new Set();
210
- function deprecated(oldMethod, newMethod, full = false) {
211
- const err = new Error();
212
- const pos = full ? err.stack.split('\n').slice(1).join('\n') : err.stack.split('\n')[3].trim().split('(').slice(1).join('(').split(')').slice(0, -1).join(')');
213
- if(shownWarnings.has(pos)) return;
214
- shownWarnings.add(pos);
215
- console.warn(`${new Date().toLocaleString('en-UK', {
216
- weekday: 'short',
217
- year: 'numeric',
218
- month: 'short',
219
- day: 'numeric',
220
- hour: 'numeric',
221
- minute: 'numeric',
222
- second: 'numeric',
223
- timeZone: 'GMT',
224
- timeZoneName: 'short'
225
- })} u-express deprecated ${oldMethod}: Use ${newMethod} instead at ${pos}`);
226
- }
227
-
228
- function findIndexStartingFrom(arr, fn, index = 0) {
229
- for(let i = index, end = arr.length; i < end; i++) {
230
- if(fn(arr[i], i, arr)) {
231
- return i;
232
- }
233
- }
234
- return -1;
235
- };
236
-
237
- function decode (path) {
238
- try {
239
- return decodeURIComponent(path)
240
- } catch (err) {
241
- return -1
242
- }
243
- }
244
-
245
- const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
246
-
247
- function containsDotFile(parts) {
248
- for(let i = 0, len = parts.length; i < len; i++) {
249
- const part = parts[i];
250
- if(part.length > 1 && part[0] === '.') {
251
- return true;
252
- }
253
- }
254
-
255
- return false;
256
- }
257
-
258
- function parseTokenList(str) {
259
- let end = 0;
260
- const list = [];
261
- let start = 0;
262
-
263
- // gather tokens
264
- for (let i = 0, len = str.length; i < len; i++) {
265
- switch(str.charCodeAt(i)) {
266
- case 0x20: /* */
267
- if (start === end) {
268
- start = end = i + 1;
269
- }
270
- break;
271
- case 0x2c: /* , */
272
- if (start !== end) {
273
- list.push(str.substring(start, end));
274
- }
275
- start = end = i + 1;
276
- break;
277
- default:
278
- end = i + 1;
279
- break;
280
- }
281
- }
282
-
283
- // final token
284
- if (start !== end) {
285
- list.push(str.substring(start, end));
286
- }
287
-
288
- return list;
289
- }
290
-
291
-
292
- function parseHttpDate(date) {
293
- const timestamp = date && Date.parse(date);
294
- return typeof timestamp === 'number' ? timestamp : NaN;
295
- }
296
-
297
- function isPreconditionFailure(req, res) {
298
- const match = req.headers['if-match'];
299
-
300
- // if-match
301
- if(match) {
302
- const etag = res.get('etag');
303
- return !etag || (match !== '*' && parseTokenList(match).every(match => {
304
- return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag;
305
- }));
306
- }
307
-
308
- // if-unmodified-since
309
- const unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']);
310
- if(!isNaN(unmodifiedSince)) {
311
- const lastModified = parseHttpDate(res.get('Last-Modified'));
312
- return isNaN(lastModified) || lastModified > unmodifiedSince;
313
- }
314
-
315
- return false;
316
- }
317
-
318
- function createETagGenerator(options) {
319
- return function generateETag (body, encoding) {
320
- if(body instanceof Stats) {
321
- return etag(body, options);
322
- }
323
- const buf = !Buffer.isBuffer(body) ? Buffer.from(body, encoding) : body;
324
- return etag(buf, options);
325
- }
326
- }
327
-
328
- function isRangeFresh(req, res) {
329
- const ifRange = req.headers['if-range'];
330
- if(!ifRange) {
331
- return true;
332
- }
333
-
334
- // if-range as etag
335
- if(ifRange.indexOf('"') !== -1) {
336
- const etag = res.get('etag');
337
- return Boolean(etag && ifRange.indexOf(etag) !== -1);
338
- }
339
-
340
- // if-range as modified date
341
- const lastModified = res.get('Last-Modified');
342
- return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
343
- }
344
-
345
- // fast null object
346
- const NullObject = function() {};
347
- NullObject.prototype = Object.create(null);
348
-
349
- module.exports = {
350
- removeDuplicateSlashes,
351
- patternToRegex,
352
- needsConversionToRegex,
353
- acceptParams,
354
- normalizeType,
355
- stringify,
356
- defaultSettings,
357
- compileTrust,
358
- deprecated,
359
- UP_PATH_REGEXP,
360
- NullObject,
361
- decode,
362
- containsDotFile,
363
- parseTokenList,
364
- parseHttpDate,
365
- isPreconditionFailure,
366
- createETagGenerator,
367
- isRangeFresh,
368
- findIndexStartingFrom,
369
- fastQueryParse,
370
- canBeOptimized,
371
- EMPTY_REGEX
372
- };
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const mime = require("mime-types");
18
+ const path = require("path");
19
+ const proxyaddr = require("proxy-addr");
20
+ const qs = require("qs");
21
+ const querystring = require("fast-querystring");
22
+ const etag = require("etag");
23
+ const { Stats } = require("fs");
24
+
25
+ const EMPTY_REGEX = new RegExp(``);
26
+
27
+ function fastQueryParse(query, options) {
28
+ const len = query.length;
29
+ if(len === 0){
30
+ return new NullObject();
31
+ }
32
+ if(len <= 128) {
33
+ if(!query.includes('[') && !query.includes('%5B') && !query.includes('.') && !query.includes('%2E')) {
34
+ // [Object: null prototype] issue
35
+ return {...querystring.parse(query)};
36
+ }
37
+ }
38
+ // [Object: null prototype] issue
39
+ return {...qs.parse(query, options)};
40
+ }
41
+
42
+ function removeDuplicateSlashes(path) {
43
+ return path.replace(/\/{2,}/g, '/');
44
+ }
45
+
46
+ function patternToRegex(pattern, isPrefix = false) {
47
+ if(pattern instanceof RegExp) {
48
+ return pattern;
49
+ }
50
+ if(isPrefix && pattern === '') {
51
+ return EMPTY_REGEX;
52
+ }
53
+
54
+ let regexPattern = pattern
55
+ .replaceAll('.', '\\.')
56
+ .replaceAll('-', '\\-')
57
+ .replaceAll('*', '(.*)') // Convert * to .*
58
+ .replace(/\/:(\w+)(\(.+?\))?\??/g, (match, param, regex) => {
59
+ const optional = match.endsWith('?');
60
+ return `\\/${optional ? '?' : ''}(?<${param}>${regex ? regex + '($|\\/)' : '[^/]+'})${optional ? '?' : ''}`;
61
+ }); // Convert :param to capture group
62
+
63
+ return new RegExp(`^${regexPattern}${isPrefix ? '(?=$|\/)' : '$'}`);
64
+ }
65
+
66
+ function needsConversionToRegex(pattern) {
67
+ if(pattern instanceof RegExp) {
68
+ return false;
69
+ }
70
+ if(pattern === '/*') {
71
+ return false;
72
+ }
73
+
74
+ return pattern.includes('*') ||
75
+ pattern.includes('?') ||
76
+ pattern.includes('+') ||
77
+ pattern.includes('(') ||
78
+ pattern.includes(')') ||
79
+ pattern.includes(':') ||
80
+ pattern.includes('{') ||
81
+ pattern.includes('}') ||
82
+ pattern.includes('[') ||
83
+ pattern.includes(']');
84
+ }
85
+
86
+ function canBeOptimized(pattern) {
87
+ if(pattern === '/*') {
88
+ return false;
89
+ }
90
+ if(pattern instanceof RegExp) {
91
+ return false;
92
+ }
93
+ if(
94
+ pattern.includes('*') ||
95
+ pattern.includes('?') ||
96
+ pattern.includes('+') ||
97
+ pattern.includes('(') ||
98
+ pattern.includes(')') ||
99
+ pattern.includes('{') ||
100
+ pattern.includes('}') ||
101
+ pattern.includes('[') ||
102
+ pattern.includes(']')
103
+ ) {
104
+ return false;
105
+ }
106
+ return true;
107
+ }
108
+
109
+ function acceptParams(str) {
110
+ const length = str.length;
111
+ const colonIndex = str.indexOf(';');
112
+ let index = colonIndex === -1 ? length : colonIndex;
113
+ const ret = { value: str.slice(0, index).trim(), quality: 1, params: {} };
114
+
115
+ while (index < length) {
116
+ const splitIndex = str.indexOf('=', index);
117
+ if (splitIndex === -1) break;
118
+
119
+ const colonIndex = str.indexOf(';', index);
120
+ const endIndex = colonIndex === -1 ? length : colonIndex;
121
+
122
+ if (splitIndex > endIndex) {
123
+ index = str.lastIndexOf(';', splitIndex - 1) + 1;
124
+ continue;
125
+ }
126
+
127
+ const key = str.slice(index, splitIndex).trim();
128
+ const value = str.slice(splitIndex + 1, endIndex).trim();
129
+
130
+ if (key === 'q') {
131
+ ret.quality = parseFloat(value);
132
+ } else {
133
+ ret.params[key] = value;
134
+ }
135
+
136
+ index = endIndex + 1;
137
+ }
138
+
139
+ return ret;
140
+ }
141
+
142
+ function normalizeType(type) {
143
+ return ~type.indexOf('/') ?
144
+ acceptParams(type) :
145
+ { value: (mime.lookup(type) || 'application/octet-stream'), params: {} };
146
+ }
147
+
148
+ function stringify(value, replacer, spaces, escape) {
149
+ let json = replacer || spaces
150
+ ? JSON.stringify(value, replacer, spaces)
151
+ : JSON.stringify(value);
152
+
153
+ if (escape && typeof json === 'string') {
154
+ json = json.replace(/[<>&]/g, function (c) {
155
+ switch (c.charCodeAt(0)) {
156
+ case 0x3c:
157
+ return '\\u003c'
158
+ case 0x3e:
159
+ return '\\u003e'
160
+ case 0x26:
161
+ return '\\u0026'
162
+ default:
163
+ return c
164
+ }
165
+ });
166
+ }
167
+
168
+ return json;
169
+ }
170
+
171
+ const defaultSettings = {
172
+ 'jsonp callback name': 'callback',
173
+ 'env': () => process.env.NODE_ENV ?? 'development',
174
+ 'etag': 'weak',
175
+ 'etag fn': () => createETagGenerator({ weak: true }),
176
+ 'query parser': 'extended',
177
+ 'query parser fn': () => fastQueryParse,
178
+ 'subdomain offset': 2,
179
+ 'trust proxy': false,
180
+ 'views': () => path.join(process.cwd(), 'views'),
181
+ 'view cache': () => process.env.NODE_ENV === 'production',
182
+ 'x-powered-by': true,
183
+ 'case sensitive routing': true,
184
+ 'declarative responses': true
185
+ };
186
+
187
+ function compileTrust(val) {
188
+ if (typeof val === 'function') return val;
189
+
190
+ if (val === true) {
191
+ // Support plain true/false
192
+ return function(){ return true };
193
+ }
194
+
195
+ if (typeof val === 'number') {
196
+ // Support trusting hop count
197
+ return function(a, i){ return i < val };
198
+ }
199
+
200
+ if (typeof val === 'string') {
201
+ // Support comma-separated values
202
+ val = val.split(',')
203
+ .map(function (v) { return v.trim() })
204
+ }
205
+
206
+ return proxyaddr.compile(val || []);
207
+ }
208
+
209
+ const shownWarnings = new Set();
210
+ function deprecated(oldMethod, newMethod, full = false) {
211
+ const err = new Error();
212
+ const pos = full ? err.stack.split('\n').slice(1).join('\n') : err.stack.split('\n')[3].trim().split('(').slice(1).join('(').split(')').slice(0, -1).join(')');
213
+ if(shownWarnings.has(pos)) return;
214
+ shownWarnings.add(pos);
215
+ console.warn(`${new Date().toLocaleString('en-UK', {
216
+ weekday: 'short',
217
+ year: 'numeric',
218
+ month: 'short',
219
+ day: 'numeric',
220
+ hour: 'numeric',
221
+ minute: 'numeric',
222
+ second: 'numeric',
223
+ timeZone: 'GMT',
224
+ timeZoneName: 'short'
225
+ })} u-express deprecated ${oldMethod}: Use ${newMethod} instead at ${pos}`);
226
+ }
227
+
228
+ function findIndexStartingFrom(arr, fn, index = 0) {
229
+ for(let i = index, end = arr.length; i < end; i++) {
230
+ if(fn(arr[i], i, arr)) {
231
+ return i;
232
+ }
233
+ }
234
+ return -1;
235
+ };
236
+
237
+ function decode (path) {
238
+ try {
239
+ return decodeURIComponent(path)
240
+ } catch (err) {
241
+ return -1
242
+ }
243
+ }
244
+
245
+ const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
246
+
247
+ function containsDotFile(parts) {
248
+ for(let i = 0, len = parts.length; i < len; i++) {
249
+ const part = parts[i];
250
+ if(part.length > 1 && part[0] === '.') {
251
+ return true;
252
+ }
253
+ }
254
+
255
+ return false;
256
+ }
257
+
258
+ function parseTokenList(str) {
259
+ let end = 0;
260
+ const list = [];
261
+ let start = 0;
262
+
263
+ // gather tokens
264
+ for (let i = 0, len = str.length; i < len; i++) {
265
+ switch(str.charCodeAt(i)) {
266
+ case 0x20: /* */
267
+ if (start === end) {
268
+ start = end = i + 1;
269
+ }
270
+ break;
271
+ case 0x2c: /* , */
272
+ if (start !== end) {
273
+ list.push(str.substring(start, end));
274
+ }
275
+ start = end = i + 1;
276
+ break;
277
+ default:
278
+ end = i + 1;
279
+ break;
280
+ }
281
+ }
282
+
283
+ // final token
284
+ if (start !== end) {
285
+ list.push(str.substring(start, end));
286
+ }
287
+
288
+ return list;
289
+ }
290
+
291
+
292
+ function parseHttpDate(date) {
293
+ const timestamp = date && Date.parse(date);
294
+ return typeof timestamp === 'number' ? timestamp : NaN;
295
+ }
296
+
297
+ function isPreconditionFailure(req, res) {
298
+ const match = req.headers['if-match'];
299
+
300
+ // if-match
301
+ if(match) {
302
+ const etag = res.get('etag');
303
+ return !etag || (match !== '*' && parseTokenList(match).every(match => {
304
+ return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag;
305
+ }));
306
+ }
307
+
308
+ // if-unmodified-since
309
+ const unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']);
310
+ if(!isNaN(unmodifiedSince)) {
311
+ const lastModified = parseHttpDate(res.get('Last-Modified'));
312
+ return isNaN(lastModified) || lastModified > unmodifiedSince;
313
+ }
314
+
315
+ return false;
316
+ }
317
+
318
+ function createETagGenerator(options) {
319
+ return function generateETag (body, encoding) {
320
+ if(body instanceof Stats) {
321
+ return etag(body, options);
322
+ }
323
+ const buf = !Buffer.isBuffer(body) ? Buffer.from(body, encoding) : body;
324
+ return etag(buf, options);
325
+ }
326
+ }
327
+
328
+ function isRangeFresh(req, res) {
329
+ const ifRange = req.headers['if-range'];
330
+ if(!ifRange) {
331
+ return true;
332
+ }
333
+
334
+ // if-range as etag
335
+ if(ifRange.indexOf('"') !== -1) {
336
+ const etag = res.get('etag');
337
+ return Boolean(etag && ifRange.indexOf(etag) !== -1);
338
+ }
339
+
340
+ // if-range as modified date
341
+ const lastModified = res.get('Last-Modified');
342
+ return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
343
+ }
344
+
345
+ // fast null object
346
+ const NullObject = function() {};
347
+ NullObject.prototype = Object.create(null);
348
+
349
+ module.exports = {
350
+ removeDuplicateSlashes,
351
+ patternToRegex,
352
+ needsConversionToRegex,
353
+ acceptParams,
354
+ normalizeType,
355
+ stringify,
356
+ defaultSettings,
357
+ compileTrust,
358
+ deprecated,
359
+ UP_PATH_REGEXP,
360
+ NullObject,
361
+ decode,
362
+ containsDotFile,
363
+ parseTokenList,
364
+ parseHttpDate,
365
+ isPreconditionFailure,
366
+ createETagGenerator,
367
+ isRangeFresh,
368
+ findIndexStartingFrom,
369
+ fastQueryParse,
370
+ canBeOptimized,
371
+ EMPTY_REGEX
372
+ };