ultimate-express 2.0.8 → 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/EXPRESS_LICENSE +25 -25
- package/LICENSE +201 -201
- package/README.md +350 -350
- package/package.json +109 -109
- package/src/application.js +342 -337
- package/src/declarative.js +463 -463
- package/src/index.js +65 -65
- package/src/middlewares.js +351 -346
- package/src/request.js +428 -428
- package/src/response.js +869 -869
- package/src/router.js +659 -658
- package/src/types.d.ts +51 -51
- package/src/utils.js +372 -372
- package/src/view.js +125 -125
- package/src/worker.js +30 -30
package/src/declarative.js
CHANGED
|
@@ -1,463 +1,463 @@
|
|
|
1
|
-
const acorn = require("acorn");
|
|
2
|
-
const { stringify } = require("./utils.js");
|
|
3
|
-
const uWS = require("uWebSockets.js");
|
|
4
|
-
|
|
5
|
-
const parser = acorn.Parser;
|
|
6
|
-
|
|
7
|
-
const allowedResMethods = ['set', 'header', 'setHeader', 'status', 'send', 'end', 'append'];
|
|
8
|
-
const allowedIdentifiers = ['query', 'params', ...allowedResMethods];
|
|
9
|
-
const objKeyRegex = /[\s{\n]([A-Za-z-0-9_]+)(\s|\n)*?:/g;
|
|
10
|
-
|
|
11
|
-
function replaceSingleCharacter(str, index, char) {
|
|
12
|
-
return str.slice(0, index) + char + str.slice(index + 1);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// generates a declarative response from a callback
|
|
16
|
-
// uWS allows creating such responses and they are extremely fast
|
|
17
|
-
// since you don't even have to call into Node.js at all
|
|
18
|
-
// declarative response will only be created if callback is 'simple enough'
|
|
19
|
-
// simple enough means:
|
|
20
|
-
// - doesnt call external functions
|
|
21
|
-
// - doesnt create variables
|
|
22
|
-
// - only uses req.query and req.params
|
|
23
|
-
// basically, its only simple, static responses
|
|
24
|
-
module.exports = function compileDeclarative(cb, app) {
|
|
25
|
-
try {
|
|
26
|
-
let code = cb.toString();
|
|
27
|
-
// convert anonymous functions to named ones to make it valid code
|
|
28
|
-
if(code.startsWith("function") || code.startsWith("async function")) {
|
|
29
|
-
code = code.replace(/function *\(/, "function __cb(");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
|
|
33
|
-
|
|
34
|
-
if(tokens.some(token => ['throw', 'new', 'await', 'return'].includes(token.value))) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const parsed = parser.parse(code, { ecmaVersion: "latest" }).body;
|
|
39
|
-
let fn = parsed[0];
|
|
40
|
-
|
|
41
|
-
if(fn.type === 'ExpressionStatement') {
|
|
42
|
-
fn = fn.expression;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// check if it is a function
|
|
46
|
-
if (fn.type !== 'FunctionDeclaration' && fn.type !== 'ArrowFunctionExpression') {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const args = fn.params.map(param => param.name);
|
|
51
|
-
|
|
52
|
-
if(args.length < 2) {
|
|
53
|
-
// invalid function? doesn't have (req, res) args
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const [req, res] = args;
|
|
58
|
-
let queryName, paramsName, queries = [], params = [];
|
|
59
|
-
|
|
60
|
-
if(fn.params[0].type === 'ObjectPattern') {
|
|
61
|
-
let query = fn.params[0].properties.find(prop => prop.key.name === 'query');
|
|
62
|
-
let param = fn.params[0].properties.find(prop => prop.key.name === 'params');
|
|
63
|
-
|
|
64
|
-
if(query?.value?.type === 'Identifier') {
|
|
65
|
-
queryName = query.value.name;
|
|
66
|
-
} else if(query?.value?.type === 'ObjectPattern') {
|
|
67
|
-
for(let prop of query.value.properties) {
|
|
68
|
-
if(prop.value.type !== 'Identifier') {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
queries.push(prop.value.name);
|
|
72
|
-
}
|
|
73
|
-
} else {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if(param?.value?.type === 'Identifier') {
|
|
78
|
-
paramsName = param.value.name;
|
|
79
|
-
} else if(param?.value?.type === 'ObjectPattern') {
|
|
80
|
-
for(let prop of param.value.properties) {
|
|
81
|
-
if(prop.value.type !== 'Identifier') {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
params.push(prop.value.name);
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// check if it calls any other function other than the one in `res`
|
|
92
|
-
const callExprs = filterNodes(fn, node => node.type === 'CallExpression');
|
|
93
|
-
const resCalls = [];
|
|
94
|
-
for(let expr of callExprs) {
|
|
95
|
-
let calleeName, propertyName;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// get propertyName
|
|
99
|
-
if(expr.type === 'MemberExpression') {
|
|
100
|
-
propertyName = expr.property.name;
|
|
101
|
-
} else if(expr.type === 'CallExpression') {
|
|
102
|
-
propertyName = expr.callee?.property?.name ?? expr.callee?.name;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// get calleeName
|
|
106
|
-
switch(expr.callee.type) {
|
|
107
|
-
case "Identifier":
|
|
108
|
-
calleeName = expr.callee.name;
|
|
109
|
-
break;
|
|
110
|
-
case "MemberExpression":
|
|
111
|
-
if(expr.callee.object.type === 'Identifier') {
|
|
112
|
-
calleeName = expr.callee.object.name;
|
|
113
|
-
} else if(expr.callee.object.type === 'CallExpression') {
|
|
114
|
-
// function call chaining
|
|
115
|
-
let callee = expr.callee;
|
|
116
|
-
while(callee.object.callee) {
|
|
117
|
-
callee = callee.object.callee;
|
|
118
|
-
}
|
|
119
|
-
if(callee.object.type !== 'Identifier') {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
calleeName = callee.object.name;
|
|
123
|
-
}
|
|
124
|
-
break;
|
|
125
|
-
default:
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
// check if calleeName is res
|
|
129
|
-
if(calleeName !== res) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const obj = { calleeName, propertyName };
|
|
134
|
-
expr.obj = obj;
|
|
135
|
-
resCalls.push(obj);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// check if res property being called are
|
|
139
|
-
// - set, header, setHeader
|
|
140
|
-
// - status
|
|
141
|
-
// - send
|
|
142
|
-
// - end
|
|
143
|
-
for(let call of resCalls) {
|
|
144
|
-
if(!allowedResMethods.includes(call.propertyName)) {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// check if all identifiers are allowed
|
|
150
|
-
const identifiers = filterNodes(fn, node => node.type === 'Identifier').slice(args.length).map(id => id.name);
|
|
151
|
-
if(identifiers[identifiers.length - 1] === '__cb') {
|
|
152
|
-
identifiers.pop();
|
|
153
|
-
}
|
|
154
|
-
if(!identifiers.every((id, i) =>
|
|
155
|
-
allowedIdentifiers.includes(id) ||
|
|
156
|
-
id === req ||
|
|
157
|
-
id === res ||
|
|
158
|
-
(identifiers[i - 2] === req && identifiers[i - 1] === 'params') ||
|
|
159
|
-
(identifiers[i - 2] === req && identifiers[i - 1] === 'query') ||
|
|
160
|
-
id === queryName ||
|
|
161
|
-
id === paramsName ||
|
|
162
|
-
queries.includes(id) ||
|
|
163
|
-
params.includes(id)
|
|
164
|
-
)) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
let statusCode = 200;
|
|
170
|
-
const headers = [];
|
|
171
|
-
const body = [];
|
|
172
|
-
|
|
173
|
-
// get statusCode
|
|
174
|
-
for(let call of callExprs) {
|
|
175
|
-
if(call.obj.propertyName === 'status') {
|
|
176
|
-
if(call.arguments[0].type !== 'Literal') {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
statusCode = call.arguments[0].value;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// get headers
|
|
184
|
-
for(let call of callExprs) {
|
|
185
|
-
if(call.obj.propertyName === 'header' || call.obj.propertyName === 'setHeader' || call.obj.propertyName === 'set') {
|
|
186
|
-
if(call.arguments[0].type !== 'Literal' || call.arguments[1].type !== 'Literal') {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
const sameHeader = headers.find(header => header[0].toLowerCase() === call.arguments[0].value.toLowerCase());
|
|
190
|
-
let [header, value] = [call.arguments[0].value, call.arguments[1].value];
|
|
191
|
-
if(call.obj.propertyName !== 'setHeader') {
|
|
192
|
-
if(value.includes('text/') && !value.includes('; charset=')) {
|
|
193
|
-
value += '; charset=utf-8';
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if(sameHeader) {
|
|
197
|
-
sameHeader[1] = value;
|
|
198
|
-
} else {
|
|
199
|
-
headers.push([header, value]);
|
|
200
|
-
}
|
|
201
|
-
} else if(call.obj.propertyName === 'append') {
|
|
202
|
-
if(call.arguments[0].type !== 'Literal' || call.arguments[1].type !== 'Literal') {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
headers.push([call.arguments[0].value, call.arguments[1].value]);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// get body
|
|
210
|
-
let sendUsed = false;
|
|
211
|
-
for(let call of callExprs) {
|
|
212
|
-
if(call.obj.propertyName === 'send' || call.obj.propertyName === 'end') {
|
|
213
|
-
if(sendUsed) {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
if(call.obj.propertyName === 'send') {
|
|
217
|
-
const index = headers.findIndex(header => header[0].toLowerCase() === 'content-type');
|
|
218
|
-
if(index === -1) {
|
|
219
|
-
headers.push(['content-type', 'text/html; charset=utf-8']);
|
|
220
|
-
} else {
|
|
221
|
-
if(headers[index][1].includes('text/') && !headers[index][1].includes('; charset=')) {
|
|
222
|
-
headers[index][1] += '; charset=utf-8';
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const arg = call.arguments[0];
|
|
227
|
-
if(arg) {
|
|
228
|
-
if(arg.type === 'Literal') {
|
|
229
|
-
if(typeof arg.value === 'number') { // status code
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
let val = arg.value;
|
|
233
|
-
if(val === null) {
|
|
234
|
-
val = '';
|
|
235
|
-
const index = headers.findIndex(header => header[0].toLowerCase() === 'content-type');
|
|
236
|
-
if(index !== -1) {
|
|
237
|
-
headers.splice(index, 1);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
if(typeof val === 'boolean') {
|
|
241
|
-
if(!headers.some(header => header[0].toLowerCase() === 'content-type')) {
|
|
242
|
-
headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
243
|
-
} else {
|
|
244
|
-
headers.find(header => header[0].toLowerCase() === 'content-type')[1] = 'application/json; charset=utf-8';
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
body.push({type: 'text', value: val});
|
|
248
|
-
} else if(arg.type === 'TemplateLiteral') {
|
|
249
|
-
const exprs = [...arg.quasis, ...arg.expressions].sort((a, b) => a.start - b.start);
|
|
250
|
-
for(let expr of exprs) {
|
|
251
|
-
if(expr.type === 'TemplateElement') {
|
|
252
|
-
body.push({type: 'text', value: expr.value.cooked});
|
|
253
|
-
} else if(expr.type === 'MemberExpression') {
|
|
254
|
-
const obj = expr.object;
|
|
255
|
-
let type;
|
|
256
|
-
if(obj.type === 'MemberExpression') {
|
|
257
|
-
if(obj.property.type !== 'Identifier') {
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
type = obj.property.name;
|
|
261
|
-
} else if(obj.type === 'Identifier') {
|
|
262
|
-
type = obj.name;
|
|
263
|
-
} else {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
if(type !== 'params' && type !== 'query') {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
body.push({type, value: expr.property.name});
|
|
270
|
-
} else if(expr.type === 'Identifier') {
|
|
271
|
-
if(queries.includes(expr.name)) {
|
|
272
|
-
body.push({type: 'query', value: expr.name});
|
|
273
|
-
} else if(params.includes(expr.name)) {
|
|
274
|
-
body.push({type: 'params', value: expr.name});
|
|
275
|
-
} else {
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
} else {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
} else if(arg.type === 'MemberExpression') {
|
|
283
|
-
if(!arg.object.property) {
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
if(arg.object.property.type !== 'Identifier' || (arg.object.property.name !== 'query' && arg.object.property.name !== 'params')) {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
body.push({type: arg.object.property.name, value: arg.property.name});
|
|
290
|
-
} else if(arg.type === 'BinaryExpression') {
|
|
291
|
-
let stuff = [];
|
|
292
|
-
function check(node) {
|
|
293
|
-
if(node.right.type === 'Literal') {
|
|
294
|
-
stuff.push({type: 'text', value: node.right.value});
|
|
295
|
-
} else if(node.right.type === 'MemberExpression') {
|
|
296
|
-
stuff.push({type: node.right.object.property.name, value: node.right.property.name});
|
|
297
|
-
} else return false;
|
|
298
|
-
if(node.left.type === 'Literal') {
|
|
299
|
-
stuff.push({type: 'text', value: node.left.value});
|
|
300
|
-
} else if(node.left.type === 'MemberExpression') {
|
|
301
|
-
stuff.push({type: node.left.object.property.name, value: node.left.property.name});
|
|
302
|
-
} else if(node.left.type === 'BinaryExpression') {
|
|
303
|
-
return check(node.left);
|
|
304
|
-
} else return false;
|
|
305
|
-
|
|
306
|
-
return true;
|
|
307
|
-
}
|
|
308
|
-
if(!check(arg)) {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
body.push(...stuff.reverse());
|
|
312
|
-
} else if(arg.type === 'ObjectExpression') {
|
|
313
|
-
if(call.obj.propertyName === 'end') {
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
316
|
-
// only simple objects can be optimized
|
|
317
|
-
let objCode = code;
|
|
318
|
-
for(let property of arg.properties) {
|
|
319
|
-
if(property.key.type !== 'Identifier' && property.key.type !== 'Literal') {
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
if(property.value.raw.startsWith("'") && property.value.raw.endsWith("'") && !property.value.value.includes("'")) {
|
|
323
|
-
objCode = replaceSingleCharacter(objCode, property.value.start, '"');
|
|
324
|
-
objCode = replaceSingleCharacter(objCode, property.value.end - 1, '"');
|
|
325
|
-
}
|
|
326
|
-
if(property.value.type !== 'Literal') {
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
if(typeof app.get('json replacer') !== 'undefined' && typeof app.get('json replacer') !== 'string') {
|
|
331
|
-
return false;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if(!headers.some(header => header[0].toLowerCase() === 'content-type')) {
|
|
335
|
-
headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
336
|
-
} else {
|
|
337
|
-
headers.find(header => header[0].toLowerCase() === 'content-type')[1] = 'application/json; charset=utf-8';
|
|
338
|
-
}
|
|
339
|
-
body.push({
|
|
340
|
-
type: 'text',
|
|
341
|
-
value:
|
|
342
|
-
stringify(
|
|
343
|
-
JSON.parse(objCode.slice(arg.start, arg.end).replace(objKeyRegex, '"$1":')),
|
|
344
|
-
app.get('json replacer'),
|
|
345
|
-
app.get('json spaces'),
|
|
346
|
-
app.get('json escape')
|
|
347
|
-
)
|
|
348
|
-
});
|
|
349
|
-
} else {
|
|
350
|
-
return false;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
sendUsed = true;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// uws doesnt support status codes other than 200 currently
|
|
358
|
-
if(statusCode != 200) {
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
let decRes = new uWS.DeclarativeResponse();
|
|
363
|
-
|
|
364
|
-
for(let header of headers) {
|
|
365
|
-
if(header[0].toLowerCase() === 'content-length') {
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
decRes = decRes.writeHeader(header[0], header[1]);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if(app.get('etag') && !headers.some(header => header[0].toLowerCase() === 'etag')) {
|
|
372
|
-
if(body.some(part => part.type !== 'text')) {
|
|
373
|
-
return false;
|
|
374
|
-
} else {
|
|
375
|
-
decRes = decRes.writeHeader('ETag', app.get('etag fn')(body.map(part => part.value.toString()).join('')));
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if(app.get('x-powered-by')) {
|
|
380
|
-
decRes = decRes.writeHeader('x-powered-by', 'UltimateExpress');
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
for(let bodyPart of body) {
|
|
384
|
-
if(bodyPart.type === 'text' && String(bodyPart.value).length) {
|
|
385
|
-
decRes = decRes.write(String(bodyPart.value));
|
|
386
|
-
} else if(bodyPart.type === 'params') {
|
|
387
|
-
decRes = decRes.writeParameterValue(bodyPart.value);
|
|
388
|
-
} else if(bodyPart.type === 'query') {
|
|
389
|
-
decRes = decRes.writeQueryValue(bodyPart.value);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return decRes.end();
|
|
394
|
-
} catch(e) {
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function filterNodes(node, fn) {
|
|
400
|
-
const filtered = [];
|
|
401
|
-
if(fn(node)) {
|
|
402
|
-
filtered.push(node);
|
|
403
|
-
}
|
|
404
|
-
if(node.params) {
|
|
405
|
-
for(let param of node.params) {
|
|
406
|
-
filtered.push(...filterNodes(param, fn));
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if(node.body) {
|
|
411
|
-
if(Array.isArray(node.body)) {
|
|
412
|
-
for(let child of node.body) {
|
|
413
|
-
filtered.push(...filterNodes(child, fn));
|
|
414
|
-
}
|
|
415
|
-
} else {
|
|
416
|
-
filtered.push(...filterNodes(node.body, fn));
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if(node.declarations) {
|
|
421
|
-
for(let declaration of node.declarations) {
|
|
422
|
-
filtered.push(...filterNodes(declaration, fn));
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if(node.expression) {
|
|
427
|
-
filtered.push(...filterNodes(node.expression, fn));
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if(node.callee) {
|
|
431
|
-
filtered.push(...filterNodes(node.callee, fn));
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if(node.object) {
|
|
435
|
-
filtered.push(...filterNodes(node.object, fn));
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if(node.property) {
|
|
439
|
-
filtered.push(...filterNodes(node.property, fn));
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if(node.id) {
|
|
443
|
-
filtered.push(...filterNodes(node.id, fn));
|
|
444
|
-
}
|
|
445
|
-
if(node.init) {
|
|
446
|
-
filtered.push(...filterNodes(node.init, fn));
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if(node.left) {
|
|
450
|
-
filtered.push(...filterNodes(node.left, fn));
|
|
451
|
-
}
|
|
452
|
-
if(node.right) {
|
|
453
|
-
filtered.push(...filterNodes(node.right, fn));
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if(node.arguments) {
|
|
457
|
-
for(let argument of node.arguments) {
|
|
458
|
-
filtered.push(...filterNodes(argument, fn));
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return filtered;
|
|
463
|
-
}
|
|
1
|
+
const acorn = require("acorn");
|
|
2
|
+
const { stringify } = require("./utils.js");
|
|
3
|
+
const uWS = require("uWebSockets.js");
|
|
4
|
+
|
|
5
|
+
const parser = acorn.Parser;
|
|
6
|
+
|
|
7
|
+
const allowedResMethods = ['set', 'header', 'setHeader', 'status', 'send', 'end', 'append'];
|
|
8
|
+
const allowedIdentifiers = ['query', 'params', ...allowedResMethods];
|
|
9
|
+
const objKeyRegex = /[\s{\n]([A-Za-z-0-9_]+)(\s|\n)*?:/g;
|
|
10
|
+
|
|
11
|
+
function replaceSingleCharacter(str, index, char) {
|
|
12
|
+
return str.slice(0, index) + char + str.slice(index + 1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// generates a declarative response from a callback
|
|
16
|
+
// uWS allows creating such responses and they are extremely fast
|
|
17
|
+
// since you don't even have to call into Node.js at all
|
|
18
|
+
// declarative response will only be created if callback is 'simple enough'
|
|
19
|
+
// simple enough means:
|
|
20
|
+
// - doesnt call external functions
|
|
21
|
+
// - doesnt create variables
|
|
22
|
+
// - only uses req.query and req.params
|
|
23
|
+
// basically, its only simple, static responses
|
|
24
|
+
module.exports = function compileDeclarative(cb, app) {
|
|
25
|
+
try {
|
|
26
|
+
let code = cb.toString();
|
|
27
|
+
// convert anonymous functions to named ones to make it valid code
|
|
28
|
+
if(code.startsWith("function") || code.startsWith("async function")) {
|
|
29
|
+
code = code.replace(/function *\(/, "function __cb(");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
|
|
33
|
+
|
|
34
|
+
if(tokens.some(token => ['throw', 'new', 'await', 'return'].includes(token.value))) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parsed = parser.parse(code, { ecmaVersion: "latest" }).body;
|
|
39
|
+
let fn = parsed[0];
|
|
40
|
+
|
|
41
|
+
if(fn.type === 'ExpressionStatement') {
|
|
42
|
+
fn = fn.expression;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// check if it is a function
|
|
46
|
+
if (fn.type !== 'FunctionDeclaration' && fn.type !== 'ArrowFunctionExpression') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const args = fn.params.map(param => param.name);
|
|
51
|
+
|
|
52
|
+
if(args.length < 2) {
|
|
53
|
+
// invalid function? doesn't have (req, res) args
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const [req, res] = args;
|
|
58
|
+
let queryName, paramsName, queries = [], params = [];
|
|
59
|
+
|
|
60
|
+
if(fn.params[0].type === 'ObjectPattern') {
|
|
61
|
+
let query = fn.params[0].properties.find(prop => prop.key.name === 'query');
|
|
62
|
+
let param = fn.params[0].properties.find(prop => prop.key.name === 'params');
|
|
63
|
+
|
|
64
|
+
if(query?.value?.type === 'Identifier') {
|
|
65
|
+
queryName = query.value.name;
|
|
66
|
+
} else if(query?.value?.type === 'ObjectPattern') {
|
|
67
|
+
for(let prop of query.value.properties) {
|
|
68
|
+
if(prop.value.type !== 'Identifier') {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
queries.push(prop.value.name);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if(param?.value?.type === 'Identifier') {
|
|
78
|
+
paramsName = param.value.name;
|
|
79
|
+
} else if(param?.value?.type === 'ObjectPattern') {
|
|
80
|
+
for(let prop of param.value.properties) {
|
|
81
|
+
if(prop.value.type !== 'Identifier') {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
params.push(prop.value.name);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// check if it calls any other function other than the one in `res`
|
|
92
|
+
const callExprs = filterNodes(fn, node => node.type === 'CallExpression');
|
|
93
|
+
const resCalls = [];
|
|
94
|
+
for(let expr of callExprs) {
|
|
95
|
+
let calleeName, propertyName;
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
// get propertyName
|
|
99
|
+
if(expr.type === 'MemberExpression') {
|
|
100
|
+
propertyName = expr.property.name;
|
|
101
|
+
} else if(expr.type === 'CallExpression') {
|
|
102
|
+
propertyName = expr.callee?.property?.name ?? expr.callee?.name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// get calleeName
|
|
106
|
+
switch(expr.callee.type) {
|
|
107
|
+
case "Identifier":
|
|
108
|
+
calleeName = expr.callee.name;
|
|
109
|
+
break;
|
|
110
|
+
case "MemberExpression":
|
|
111
|
+
if(expr.callee.object.type === 'Identifier') {
|
|
112
|
+
calleeName = expr.callee.object.name;
|
|
113
|
+
} else if(expr.callee.object.type === 'CallExpression') {
|
|
114
|
+
// function call chaining
|
|
115
|
+
let callee = expr.callee;
|
|
116
|
+
while(callee.object.callee) {
|
|
117
|
+
callee = callee.object.callee;
|
|
118
|
+
}
|
|
119
|
+
if(callee.object.type !== 'Identifier') {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
calleeName = callee.object.name;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
// check if calleeName is res
|
|
129
|
+
if(calleeName !== res) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const obj = { calleeName, propertyName };
|
|
134
|
+
expr.obj = obj;
|
|
135
|
+
resCalls.push(obj);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// check if res property being called are
|
|
139
|
+
// - set, header, setHeader
|
|
140
|
+
// - status
|
|
141
|
+
// - send
|
|
142
|
+
// - end
|
|
143
|
+
for(let call of resCalls) {
|
|
144
|
+
if(!allowedResMethods.includes(call.propertyName)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// check if all identifiers are allowed
|
|
150
|
+
const identifiers = filterNodes(fn, node => node.type === 'Identifier').slice(args.length).map(id => id.name);
|
|
151
|
+
if(identifiers[identifiers.length - 1] === '__cb') {
|
|
152
|
+
identifiers.pop();
|
|
153
|
+
}
|
|
154
|
+
if(!identifiers.every((id, i) =>
|
|
155
|
+
allowedIdentifiers.includes(id) ||
|
|
156
|
+
id === req ||
|
|
157
|
+
id === res ||
|
|
158
|
+
(identifiers[i - 2] === req && identifiers[i - 1] === 'params') ||
|
|
159
|
+
(identifiers[i - 2] === req && identifiers[i - 1] === 'query') ||
|
|
160
|
+
id === queryName ||
|
|
161
|
+
id === paramsName ||
|
|
162
|
+
queries.includes(id) ||
|
|
163
|
+
params.includes(id)
|
|
164
|
+
)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
let statusCode = 200;
|
|
170
|
+
const headers = [];
|
|
171
|
+
const body = [];
|
|
172
|
+
|
|
173
|
+
// get statusCode
|
|
174
|
+
for(let call of callExprs) {
|
|
175
|
+
if(call.obj.propertyName === 'status') {
|
|
176
|
+
if(call.arguments[0].type !== 'Literal') {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
statusCode = call.arguments[0].value;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// get headers
|
|
184
|
+
for(let call of callExprs) {
|
|
185
|
+
if(call.obj.propertyName === 'header' || call.obj.propertyName === 'setHeader' || call.obj.propertyName === 'set') {
|
|
186
|
+
if(call.arguments[0].type !== 'Literal' || call.arguments[1].type !== 'Literal') {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
const sameHeader = headers.find(header => header[0].toLowerCase() === call.arguments[0].value.toLowerCase());
|
|
190
|
+
let [header, value] = [call.arguments[0].value, call.arguments[1].value];
|
|
191
|
+
if(call.obj.propertyName !== 'setHeader') {
|
|
192
|
+
if(value.includes('text/') && !value.includes('; charset=')) {
|
|
193
|
+
value += '; charset=utf-8';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if(sameHeader) {
|
|
197
|
+
sameHeader[1] = value;
|
|
198
|
+
} else {
|
|
199
|
+
headers.push([header, value]);
|
|
200
|
+
}
|
|
201
|
+
} else if(call.obj.propertyName === 'append') {
|
|
202
|
+
if(call.arguments[0].type !== 'Literal' || call.arguments[1].type !== 'Literal') {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
headers.push([call.arguments[0].value, call.arguments[1].value]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// get body
|
|
210
|
+
let sendUsed = false;
|
|
211
|
+
for(let call of callExprs) {
|
|
212
|
+
if(call.obj.propertyName === 'send' || call.obj.propertyName === 'end') {
|
|
213
|
+
if(sendUsed) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if(call.obj.propertyName === 'send') {
|
|
217
|
+
const index = headers.findIndex(header => header[0].toLowerCase() === 'content-type');
|
|
218
|
+
if(index === -1) {
|
|
219
|
+
headers.push(['content-type', 'text/html; charset=utf-8']);
|
|
220
|
+
} else {
|
|
221
|
+
if(headers[index][1].includes('text/') && !headers[index][1].includes('; charset=')) {
|
|
222
|
+
headers[index][1] += '; charset=utf-8';
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const arg = call.arguments[0];
|
|
227
|
+
if(arg) {
|
|
228
|
+
if(arg.type === 'Literal') {
|
|
229
|
+
if(typeof arg.value === 'number') { // status code
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
let val = arg.value;
|
|
233
|
+
if(val === null) {
|
|
234
|
+
val = '';
|
|
235
|
+
const index = headers.findIndex(header => header[0].toLowerCase() === 'content-type');
|
|
236
|
+
if(index !== -1) {
|
|
237
|
+
headers.splice(index, 1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if(typeof val === 'boolean') {
|
|
241
|
+
if(!headers.some(header => header[0].toLowerCase() === 'content-type')) {
|
|
242
|
+
headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
243
|
+
} else {
|
|
244
|
+
headers.find(header => header[0].toLowerCase() === 'content-type')[1] = 'application/json; charset=utf-8';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
body.push({type: 'text', value: val});
|
|
248
|
+
} else if(arg.type === 'TemplateLiteral') {
|
|
249
|
+
const exprs = [...arg.quasis, ...arg.expressions].sort((a, b) => a.start - b.start);
|
|
250
|
+
for(let expr of exprs) {
|
|
251
|
+
if(expr.type === 'TemplateElement') {
|
|
252
|
+
body.push({type: 'text', value: expr.value.cooked});
|
|
253
|
+
} else if(expr.type === 'MemberExpression') {
|
|
254
|
+
const obj = expr.object;
|
|
255
|
+
let type;
|
|
256
|
+
if(obj.type === 'MemberExpression') {
|
|
257
|
+
if(obj.property.type !== 'Identifier') {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
type = obj.property.name;
|
|
261
|
+
} else if(obj.type === 'Identifier') {
|
|
262
|
+
type = obj.name;
|
|
263
|
+
} else {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
if(type !== 'params' && type !== 'query') {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
body.push({type, value: expr.property.name});
|
|
270
|
+
} else if(expr.type === 'Identifier') {
|
|
271
|
+
if(queries.includes(expr.name)) {
|
|
272
|
+
body.push({type: 'query', value: expr.name});
|
|
273
|
+
} else if(params.includes(expr.name)) {
|
|
274
|
+
body.push({type: 'params', value: expr.name});
|
|
275
|
+
} else {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} else if(arg.type === 'MemberExpression') {
|
|
283
|
+
if(!arg.object.property) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if(arg.object.property.type !== 'Identifier' || (arg.object.property.name !== 'query' && arg.object.property.name !== 'params')) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
body.push({type: arg.object.property.name, value: arg.property.name});
|
|
290
|
+
} else if(arg.type === 'BinaryExpression') {
|
|
291
|
+
let stuff = [];
|
|
292
|
+
function check(node) {
|
|
293
|
+
if(node.right.type === 'Literal') {
|
|
294
|
+
stuff.push({type: 'text', value: node.right.value});
|
|
295
|
+
} else if(node.right.type === 'MemberExpression') {
|
|
296
|
+
stuff.push({type: node.right.object.property.name, value: node.right.property.name});
|
|
297
|
+
} else return false;
|
|
298
|
+
if(node.left.type === 'Literal') {
|
|
299
|
+
stuff.push({type: 'text', value: node.left.value});
|
|
300
|
+
} else if(node.left.type === 'MemberExpression') {
|
|
301
|
+
stuff.push({type: node.left.object.property.name, value: node.left.property.name});
|
|
302
|
+
} else if(node.left.type === 'BinaryExpression') {
|
|
303
|
+
return check(node.left);
|
|
304
|
+
} else return false;
|
|
305
|
+
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
if(!check(arg)) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
body.push(...stuff.reverse());
|
|
312
|
+
} else if(arg.type === 'ObjectExpression') {
|
|
313
|
+
if(call.obj.propertyName === 'end') {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
// only simple objects can be optimized
|
|
317
|
+
let objCode = code;
|
|
318
|
+
for(let property of arg.properties) {
|
|
319
|
+
if(property.key.type !== 'Identifier' && property.key.type !== 'Literal') {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
if(property.value.raw.startsWith("'") && property.value.raw.endsWith("'") && !property.value.value.includes("'")) {
|
|
323
|
+
objCode = replaceSingleCharacter(objCode, property.value.start, '"');
|
|
324
|
+
objCode = replaceSingleCharacter(objCode, property.value.end - 1, '"');
|
|
325
|
+
}
|
|
326
|
+
if(property.value.type !== 'Literal') {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if(typeof app.get('json replacer') !== 'undefined' && typeof app.get('json replacer') !== 'string') {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if(!headers.some(header => header[0].toLowerCase() === 'content-type')) {
|
|
335
|
+
headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
336
|
+
} else {
|
|
337
|
+
headers.find(header => header[0].toLowerCase() === 'content-type')[1] = 'application/json; charset=utf-8';
|
|
338
|
+
}
|
|
339
|
+
body.push({
|
|
340
|
+
type: 'text',
|
|
341
|
+
value:
|
|
342
|
+
stringify(
|
|
343
|
+
JSON.parse(objCode.slice(arg.start, arg.end).replace(objKeyRegex, '"$1":')),
|
|
344
|
+
app.get('json replacer'),
|
|
345
|
+
app.get('json spaces'),
|
|
346
|
+
app.get('json escape')
|
|
347
|
+
)
|
|
348
|
+
});
|
|
349
|
+
} else {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
sendUsed = true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// uws doesnt support status codes other than 200 currently
|
|
358
|
+
if(statusCode != 200) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let decRes = new uWS.DeclarativeResponse();
|
|
363
|
+
|
|
364
|
+
for(let header of headers) {
|
|
365
|
+
if(header[0].toLowerCase() === 'content-length') {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
decRes = decRes.writeHeader(header[0], header[1]);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if(app.get('etag') && !headers.some(header => header[0].toLowerCase() === 'etag')) {
|
|
372
|
+
if(body.some(part => part.type !== 'text')) {
|
|
373
|
+
return false;
|
|
374
|
+
} else {
|
|
375
|
+
decRes = decRes.writeHeader('ETag', app.get('etag fn')(body.map(part => part.value.toString()).join('')));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if(app.get('x-powered-by')) {
|
|
380
|
+
decRes = decRes.writeHeader('x-powered-by', 'UltimateExpress');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for(let bodyPart of body) {
|
|
384
|
+
if(bodyPart.type === 'text' && String(bodyPart.value).length) {
|
|
385
|
+
decRes = decRes.write(String(bodyPart.value));
|
|
386
|
+
} else if(bodyPart.type === 'params') {
|
|
387
|
+
decRes = decRes.writeParameterValue(bodyPart.value);
|
|
388
|
+
} else if(bodyPart.type === 'query') {
|
|
389
|
+
decRes = decRes.writeQueryValue(bodyPart.value);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return decRes.end();
|
|
394
|
+
} catch(e) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function filterNodes(node, fn) {
|
|
400
|
+
const filtered = [];
|
|
401
|
+
if(fn(node)) {
|
|
402
|
+
filtered.push(node);
|
|
403
|
+
}
|
|
404
|
+
if(node.params) {
|
|
405
|
+
for(let param of node.params) {
|
|
406
|
+
filtered.push(...filterNodes(param, fn));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if(node.body) {
|
|
411
|
+
if(Array.isArray(node.body)) {
|
|
412
|
+
for(let child of node.body) {
|
|
413
|
+
filtered.push(...filterNodes(child, fn));
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
filtered.push(...filterNodes(node.body, fn));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if(node.declarations) {
|
|
421
|
+
for(let declaration of node.declarations) {
|
|
422
|
+
filtered.push(...filterNodes(declaration, fn));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if(node.expression) {
|
|
427
|
+
filtered.push(...filterNodes(node.expression, fn));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if(node.callee) {
|
|
431
|
+
filtered.push(...filterNodes(node.callee, fn));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if(node.object) {
|
|
435
|
+
filtered.push(...filterNodes(node.object, fn));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if(node.property) {
|
|
439
|
+
filtered.push(...filterNodes(node.property, fn));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if(node.id) {
|
|
443
|
+
filtered.push(...filterNodes(node.id, fn));
|
|
444
|
+
}
|
|
445
|
+
if(node.init) {
|
|
446
|
+
filtered.push(...filterNodes(node.init, fn));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if(node.left) {
|
|
450
|
+
filtered.push(...filterNodes(node.left, fn));
|
|
451
|
+
}
|
|
452
|
+
if(node.right) {
|
|
453
|
+
filtered.push(...filterNodes(node.right, fn));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if(node.arguments) {
|
|
457
|
+
for(let argument of node.arguments) {
|
|
458
|
+
filtered.push(...filterNodes(argument, fn));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return filtered;
|
|
463
|
+
}
|