fulmine.js 5.0.0-rc.1
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 +26 -0
- package/LICENSE +202 -0
- package/NOTICE +38 -0
- package/README.md +469 -0
- package/package.json +165 -0
- package/src/application.js +561 -0
- package/src/cli.js +369 -0
- package/src/declarative.js +768 -0
- package/src/index.js +71 -0
- package/src/middlewares.js +636 -0
- package/src/node-shim.js +400 -0
- package/src/request.js +807 -0
- package/src/response.js +1360 -0
- package/src/router.js +1240 -0
- package/src/types.d.ts +62 -0
- package/src/utils.js +993 -0
- package/src/view.js +172 -0
- package/src/worker.js +38 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
const acorn = require("acorn");
|
|
2
|
+
const { stringify, withDefaultCharset, withUtf8Charset } = require("./utils.js");
|
|
3
|
+
// H3App, DeclarativeResponse and _cfg all exist at runtime but are missing from the
|
|
4
|
+
// declaration file the package ships, so the module is read through a loose alias
|
|
5
|
+
const uWS = require("uWebSockets.js");
|
|
6
|
+
const uWSAny = /** @type {any} */ (uWS);
|
|
7
|
+
const statuses = require("statuses");
|
|
8
|
+
|
|
9
|
+
const parser = acorn.Parser;
|
|
10
|
+
|
|
11
|
+
const allowedResMethods = ["set", "header", "setHeader", "sendStatus", "status", "send", "json", "end", "append"];
|
|
12
|
+
const allowedIdentifiers = ["query", "params", ...allowedResMethods];
|
|
13
|
+
// the three that write a body, of which only one may appear
|
|
14
|
+
const bodyMethods = new Set(["send", "json", "end"]);
|
|
15
|
+
// and the four that finish the response, after which nothing a handler does is observable
|
|
16
|
+
const terminalMethods = new Set(["send", "json", "end", "sendStatus"]);
|
|
17
|
+
|
|
18
|
+
// Every node type the walk in filterNodes knows how to step through. A type that is missing here
|
|
19
|
+
// is not rejected for being dangerous, it is rejected because the walk would step over it without
|
|
20
|
+
// saying so: a sequence expression hid its calls completely, and `res.append("x", "1"), res.send("k")`
|
|
21
|
+
// compiled into a 200 with no body and no headers at all. Anything unfamiliar falls back to
|
|
22
|
+
// ordinary routing, which is always correct if slower.
|
|
23
|
+
const understoodNodeTypes = new Set([
|
|
24
|
+
"ArrowFunctionExpression",
|
|
25
|
+
"FunctionDeclaration",
|
|
26
|
+
"FunctionExpression",
|
|
27
|
+
"BlockStatement",
|
|
28
|
+
"ExpressionStatement",
|
|
29
|
+
"ReturnStatement",
|
|
30
|
+
"CallExpression",
|
|
31
|
+
"MemberExpression",
|
|
32
|
+
"Identifier",
|
|
33
|
+
"Literal",
|
|
34
|
+
"TemplateLiteral",
|
|
35
|
+
"TemplateElement",
|
|
36
|
+
"ObjectExpression",
|
|
37
|
+
"ArrayExpression",
|
|
38
|
+
"Property",
|
|
39
|
+
"BinaryExpression",
|
|
40
|
+
"UnaryExpression",
|
|
41
|
+
"ObjectPattern"
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Every node type in the tree, found by walking whatever is there rather than by following named
|
|
46
|
+
* edges, so that the answer does not depend on the walk being complete.
|
|
47
|
+
*
|
|
48
|
+
* @param {any} node
|
|
49
|
+
* @param {Set<string>} types
|
|
50
|
+
*/
|
|
51
|
+
function collectNodeTypes(node, types) {
|
|
52
|
+
if (!node || typeof node !== "object") {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(node)) {
|
|
56
|
+
for (const child of node) collectNodeTypes(child, types);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (typeof node.type === "string") {
|
|
60
|
+
types.add(node.type);
|
|
61
|
+
}
|
|
62
|
+
for (const key in node) {
|
|
63
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
64
|
+
collectNodeTypes(node[key], types);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The value a literal expression denotes, for the shapes whose value is known at registration
|
|
70
|
+
* time. Anything else throws, which the catch around the whole compiler turns into ordinary
|
|
71
|
+
* routing: a response written once cannot contain something only a request could produce.
|
|
72
|
+
*
|
|
73
|
+
* This replaced reading the value back out of the source text, which could not see through a
|
|
74
|
+
* nested object or an array and needed a regular expression to re-quote the keys.
|
|
75
|
+
*
|
|
76
|
+
* @param {any} node
|
|
77
|
+
* @returns {any}
|
|
78
|
+
*/
|
|
79
|
+
function literalValue(node) {
|
|
80
|
+
switch (node.type) {
|
|
81
|
+
case "Literal":
|
|
82
|
+
// a regular expression and a bigint are literals that JSON cannot carry
|
|
83
|
+
if (node.regex || typeof node.value === "bigint") {
|
|
84
|
+
throw new Error("not serialisable");
|
|
85
|
+
}
|
|
86
|
+
return node.value;
|
|
87
|
+
case "ArrayExpression":
|
|
88
|
+
return node.elements.map((element) => {
|
|
89
|
+
// a hole, as in [1, , 2], and a spread, which needs something to spread
|
|
90
|
+
if (element === null || element.type === "SpreadElement") {
|
|
91
|
+
throw new Error("not a literal");
|
|
92
|
+
}
|
|
93
|
+
return literalValue(element);
|
|
94
|
+
});
|
|
95
|
+
case "ObjectExpression": {
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const property of node.properties) {
|
|
98
|
+
if (property.type !== "Property" || property.computed || property.kind !== "init") {
|
|
99
|
+
throw new Error("not a literal");
|
|
100
|
+
}
|
|
101
|
+
const key =
|
|
102
|
+
property.key.type === "Identifier"
|
|
103
|
+
? property.key.name
|
|
104
|
+
: property.key.type === "Literal"
|
|
105
|
+
? String(property.key.value)
|
|
106
|
+
: null;
|
|
107
|
+
if (key === null) {
|
|
108
|
+
throw new Error("not a literal");
|
|
109
|
+
}
|
|
110
|
+
out[key] = literalValue(property.value);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
case "UnaryExpression":
|
|
115
|
+
// -1 is a unary minus applied to a literal, not a literal
|
|
116
|
+
if (node.operator === "-" || node.operator === "+") {
|
|
117
|
+
const value = literalValue(node.argument);
|
|
118
|
+
if (typeof value !== "number") {
|
|
119
|
+
throw new Error("not a literal");
|
|
120
|
+
}
|
|
121
|
+
return node.operator === "-" ? -value : value;
|
|
122
|
+
}
|
|
123
|
+
throw new Error("not a literal");
|
|
124
|
+
default:
|
|
125
|
+
throw new Error("not a literal");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// generates a declarative response from a callback
|
|
130
|
+
// uWS allows creating such responses and they are extremely fast
|
|
131
|
+
// since you don't even have to call into Node.js at all
|
|
132
|
+
// declarative response will only be created if callback is 'simple enough'
|
|
133
|
+
// simple enough means:
|
|
134
|
+
// - doesnt call external functions
|
|
135
|
+
// - doesnt create variables
|
|
136
|
+
// - only uses req.query and req.params
|
|
137
|
+
// basically, its only simple, static responses
|
|
138
|
+
module.exports = function compileDeclarative(cb, app) {
|
|
139
|
+
try {
|
|
140
|
+
let code = cb.toString();
|
|
141
|
+
// convert anonymous functions to named ones to make it valid code
|
|
142
|
+
if (code.startsWith("function") || code.startsWith("async function")) {
|
|
143
|
+
code = code.replace(/function *\(/, "function __cb(");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Everything below runs inside the try above, and any shape this does not understand falls
|
|
147
|
+
// out as `return false`, which is the fallback to ordinary routing. That is the design, and
|
|
148
|
+
// it is why the tree is walked loosely: acorn types every shape JavaScript can take, and
|
|
149
|
+
// enumerating them here would be a second, worse copy of that catch.
|
|
150
|
+
/** @type {any[]} */
|
|
151
|
+
const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
tokens.some((token) =>
|
|
155
|
+
[
|
|
156
|
+
"throw",
|
|
157
|
+
"new",
|
|
158
|
+
"await",
|
|
159
|
+
"try",
|
|
160
|
+
"catch",
|
|
161
|
+
"finally",
|
|
162
|
+
"if",
|
|
163
|
+
"else",
|
|
164
|
+
"switch",
|
|
165
|
+
"case",
|
|
166
|
+
"default",
|
|
167
|
+
"for",
|
|
168
|
+
"while",
|
|
169
|
+
"do",
|
|
170
|
+
"var",
|
|
171
|
+
"let",
|
|
172
|
+
"const"
|
|
173
|
+
].includes(token.value)
|
|
174
|
+
)
|
|
175
|
+
) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @type {any[]} */
|
|
180
|
+
const parsed = parser.parse(code, { ecmaVersion: "latest" }).body;
|
|
181
|
+
let fn = parsed[0];
|
|
182
|
+
|
|
183
|
+
if (fn.type === "ExpressionStatement") {
|
|
184
|
+
fn = fn.expression;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// check if it is a function
|
|
188
|
+
if (fn.type !== "FunctionDeclaration" && fn.type !== "ArrowFunctionExpression") {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// before anything is read out of the tree, since reading it is only meaningful for the
|
|
193
|
+
// shapes the walk can see through
|
|
194
|
+
const nodeTypes = new Set();
|
|
195
|
+
collectNodeTypes(fn, nodeTypes);
|
|
196
|
+
for (const type of nodeTypes) {
|
|
197
|
+
if (!understoodNodeTypes.has(type)) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const args = fn.params.map((param) => param.name);
|
|
203
|
+
|
|
204
|
+
if (args.length < 2) {
|
|
205
|
+
// invalid function? doesn't have (req, res) args
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// `return res.send(...)` describes the same response as `res.send(...)` and is written far
|
|
210
|
+
// more often, so it is worth reading. It only describes the same response when nothing
|
|
211
|
+
// follows it: every call in the body is read, whether or not it can be reached, so a return
|
|
212
|
+
// in the middle would compile statements that never run. A return inside a nested function
|
|
213
|
+
// fails the same test, not being the last statement of this one.
|
|
214
|
+
const returns = filterNodes(fn, (node) => node.type === "ReturnStatement");
|
|
215
|
+
if (returns.length) {
|
|
216
|
+
const statements = fn.body.type === "BlockStatement" ? fn.body.body : null;
|
|
217
|
+
if (!statements || returns.length > 1 || returns[0] !== statements[statements.length - 1]) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const [req, res] = args;
|
|
223
|
+
let queryName,
|
|
224
|
+
paramsName,
|
|
225
|
+
queries = [],
|
|
226
|
+
params = [];
|
|
227
|
+
|
|
228
|
+
if (fn.params[0].type === "ObjectPattern") {
|
|
229
|
+
const query = fn.params[0].properties.find((prop) => prop.key.name === "query");
|
|
230
|
+
const param = fn.params[0].properties.find((prop) => prop.key.name === "params");
|
|
231
|
+
|
|
232
|
+
if (query?.value?.type === "Identifier") {
|
|
233
|
+
queryName = query.value.name;
|
|
234
|
+
} else if (query?.value?.type === "ObjectPattern") {
|
|
235
|
+
for (const prop of query.value.properties) {
|
|
236
|
+
if (prop.value.type !== "Identifier") {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
queries.push(prop.value.name);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (param?.value?.type === "Identifier") {
|
|
246
|
+
paramsName = param.value.name;
|
|
247
|
+
} else if (param?.value?.type === "ObjectPattern") {
|
|
248
|
+
for (const prop of param.value.properties) {
|
|
249
|
+
if (prop.value.type !== "Identifier") {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
params.push(prop.value.name);
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// check if it calls any other function other than the one in `res`
|
|
260
|
+
const callExprs = filterNodes(fn, (node) => node.type === "CallExpression");
|
|
261
|
+
const resCalls = [];
|
|
262
|
+
for (const expr of callExprs) {
|
|
263
|
+
let calleeName, propertyName;
|
|
264
|
+
|
|
265
|
+
// get propertyName
|
|
266
|
+
if (expr.type === "MemberExpression") {
|
|
267
|
+
propertyName = expr.property.name;
|
|
268
|
+
} else if (expr.type === "CallExpression") {
|
|
269
|
+
propertyName = expr.callee?.property?.name ?? expr.callee?.name;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// get calleeName
|
|
273
|
+
switch (expr.callee.type) {
|
|
274
|
+
case "Identifier":
|
|
275
|
+
calleeName = expr.callee.name;
|
|
276
|
+
break;
|
|
277
|
+
case "MemberExpression":
|
|
278
|
+
if (expr.callee.object.type === "Identifier") {
|
|
279
|
+
calleeName = expr.callee.object.name;
|
|
280
|
+
} else if (expr.callee.object.type === "CallExpression") {
|
|
281
|
+
// function call chaining
|
|
282
|
+
let callee = expr.callee;
|
|
283
|
+
while (callee.object.callee) {
|
|
284
|
+
callee = callee.object.callee;
|
|
285
|
+
}
|
|
286
|
+
if (callee.object.type !== "Identifier") {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
calleeName = callee.object.name;
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
default:
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
// check if calleeName is res
|
|
296
|
+
if (calleeName !== res) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const obj = { calleeName, propertyName };
|
|
301
|
+
expr.obj = obj;
|
|
302
|
+
resCalls.push(obj);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// check if res property being called are
|
|
306
|
+
// - set, header, setHeader
|
|
307
|
+
// - status
|
|
308
|
+
// - send
|
|
309
|
+
// - end
|
|
310
|
+
for (const call of resCalls) {
|
|
311
|
+
if (!allowedResMethods.includes(call.propertyName)) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// In the order the calls run, which for a chain is not the order the tree is walked in:
|
|
317
|
+
// res.status(201) is a node inside res.status(201).send("k"), so the walk reaches the outer
|
|
318
|
+
// call first and read res.status(201).status(202) backwards. Ending position orders a chain
|
|
319
|
+
// and separate statements alike.
|
|
320
|
+
callExprs.sort((a, b) => a.end - b.end);
|
|
321
|
+
|
|
322
|
+
// Nothing after the call that writes the body has any effect, since by then the response
|
|
323
|
+
// has gone out: res.sendStatus(404) followed by res.set("x-a", "1") sends no x-a on
|
|
324
|
+
// Express, and res.send("k") followed by res.status(201) is still a 200. Two calls that
|
|
325
|
+
// each write a body is a mistake rather than a shape worth compiling, and falls back.
|
|
326
|
+
const terminalIndex = callExprs.findIndex((call) => terminalMethods.has(call.obj.propertyName));
|
|
327
|
+
if (terminalIndex !== -1) {
|
|
328
|
+
for (let i = terminalIndex + 1; i < callExprs.length; i++) {
|
|
329
|
+
if (terminalMethods.has(callExprs[i].obj.propertyName)) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
callExprs.length = terminalIndex + 1;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// check if all identifiers are allowed
|
|
337
|
+
const identifiers = filterNodes(fn, (node) => node.type === "Identifier")
|
|
338
|
+
.slice(args.length)
|
|
339
|
+
.map((id) => id.name);
|
|
340
|
+
if (identifiers[identifiers.length - 1] === "__cb") {
|
|
341
|
+
identifiers.pop();
|
|
342
|
+
}
|
|
343
|
+
if (
|
|
344
|
+
!identifiers.every(
|
|
345
|
+
(id, i) =>
|
|
346
|
+
allowedIdentifiers.includes(id) ||
|
|
347
|
+
id === req ||
|
|
348
|
+
id === res ||
|
|
349
|
+
(identifiers[i - 2] === req && identifiers[i - 1] === "params") ||
|
|
350
|
+
(identifiers[i - 2] === req && identifiers[i - 1] === "query") ||
|
|
351
|
+
id === queryName ||
|
|
352
|
+
id === paramsName ||
|
|
353
|
+
queries.includes(id) ||
|
|
354
|
+
params.includes(id)
|
|
355
|
+
)
|
|
356
|
+
) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let statusCode = 200;
|
|
361
|
+
// sendStatus and a bare send() both leave the body empty, and they mean different things:
|
|
362
|
+
// one sends the status message, the other sends nothing
|
|
363
|
+
let sendStatusUsed = false;
|
|
364
|
+
const headers = [];
|
|
365
|
+
const body = [];
|
|
366
|
+
|
|
367
|
+
// get statusCode
|
|
368
|
+
for (const call of callExprs) {
|
|
369
|
+
if (call.obj.propertyName === "status") {
|
|
370
|
+
if (call.arguments[0].type !== "Literal") {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
statusCode = call.arguments[0].value;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// get headers
|
|
378
|
+
for (const call of callExprs) {
|
|
379
|
+
if (
|
|
380
|
+
call.obj.propertyName === "header" ||
|
|
381
|
+
call.obj.propertyName === "setHeader" ||
|
|
382
|
+
call.obj.propertyName === "set"
|
|
383
|
+
) {
|
|
384
|
+
if (call.arguments[0].type !== "Literal" || call.arguments[1].type !== "Literal") {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
let [header, value] = [call.arguments[0].value, call.arguments[1].value];
|
|
388
|
+
const name = String(header).toLowerCase();
|
|
389
|
+
// res.set charsets a content-type and res.setHeader does not, since the second is
|
|
390
|
+
// node's and node does not know what a media type is
|
|
391
|
+
if (call.obj.propertyName !== "setHeader" && name === "content-type") {
|
|
392
|
+
value = withDefaultCharset(String(value));
|
|
393
|
+
}
|
|
394
|
+
const index = headers.findIndex((entry) => String(entry[0]).toLowerCase() === name);
|
|
395
|
+
if (index === -1) {
|
|
396
|
+
headers.push([header, value]);
|
|
397
|
+
} else {
|
|
398
|
+
// in place, so the header keeps the position it was first given
|
|
399
|
+
headers[index][1] = value;
|
|
400
|
+
// set replaces the header outright, so any further value append left there
|
|
401
|
+
// goes with it. Replacing only the first left the response carrying both.
|
|
402
|
+
for (let i = headers.length - 1; i > index; i--) {
|
|
403
|
+
if (String(headers[i][0]).toLowerCase() === name) {
|
|
404
|
+
headers.splice(i, 1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} else if (call.obj.propertyName === "append") {
|
|
409
|
+
if (call.arguments[0].type !== "Literal" || call.arguments[1].type !== "Literal") {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
headers.push([call.arguments[0].value, call.arguments[1].value]);
|
|
413
|
+
} else if (call.obj.propertyName === "sendStatus") {
|
|
414
|
+
if (call.arguments[0].type !== "Literal") {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
statusCode = call.arguments[0].value;
|
|
418
|
+
sendStatusUsed = true;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// get body
|
|
423
|
+
let sendUsed = false;
|
|
424
|
+
// only send() earns an ETag. end() is node's, which never computes one, and the ordinary
|
|
425
|
+
// path here follows that too, so the compiled path has to as well.
|
|
426
|
+
let bodyFromSend = false;
|
|
427
|
+
for (const call of callExprs) {
|
|
428
|
+
if (bodyMethods.has(call.obj.propertyName)) {
|
|
429
|
+
if (sendUsed) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
// send() with nothing to send gets no content-type, the same as on the ordinary
|
|
433
|
+
// path and the same as Express. It was being given one here regardless, so the
|
|
434
|
+
// two paths disagreed about a route as simple as `res.send()`.
|
|
435
|
+
if (call.obj.propertyName !== "end") {
|
|
436
|
+
bodyFromSend = true;
|
|
437
|
+
}
|
|
438
|
+
const arg = call.arguments[0];
|
|
439
|
+
|
|
440
|
+
if (call.obj.propertyName === "json") {
|
|
441
|
+
// res.json() with nothing to serialise sends no body and no length, which is a
|
|
442
|
+
// shape worth leaving to the ordinary path rather than reproducing here
|
|
443
|
+
if (!arg) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
// a replacer runs per response on the ordinary path, so a body computed once
|
|
447
|
+
// could not honour it
|
|
448
|
+
const replacer = app.get("json replacer");
|
|
449
|
+
if (typeof replacer !== "undefined" && typeof replacer !== "string") {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
// json reaches for a type only when none was chosen, then hands a string to
|
|
453
|
+
// send, which is what charsets whatever type is there
|
|
454
|
+
const existing = headers.find((header) => header[0].toLowerCase() === "content-type");
|
|
455
|
+
if (existing) {
|
|
456
|
+
existing[1] = withUtf8Charset(String(existing[1]));
|
|
457
|
+
} else {
|
|
458
|
+
headers.push(["content-type", "application/json; charset=utf-8"]);
|
|
459
|
+
}
|
|
460
|
+
body.push({
|
|
461
|
+
type: "text",
|
|
462
|
+
value: stringify(literalValue(arg), replacer, app.get("json spaces"), app.get("json escape"))
|
|
463
|
+
});
|
|
464
|
+
sendUsed = true;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (call.obj.propertyName === "send" && arg) {
|
|
469
|
+
// What the body is decides which content-type it would be given, so this comes
|
|
470
|
+
// before the body is read rather than after. Setting one and then overwriting
|
|
471
|
+
// it, which is what happened here, meant res.set("content-type", "text/plain")
|
|
472
|
+
// followed by res.send({}) answered application/json where Express answers
|
|
473
|
+
// text/plain: send only reaches for a type when none was chosen.
|
|
474
|
+
const isJsonBody =
|
|
475
|
+
arg.type === "ObjectExpression" || (arg.type === "Literal" && typeof arg.value === "boolean");
|
|
476
|
+
const isNullBody = arg.type === "Literal" && arg.value === null;
|
|
477
|
+
const existing = headers.find((header) => header[0].toLowerCase() === "content-type");
|
|
478
|
+
if (!existing) {
|
|
479
|
+
if (isJsonBody) {
|
|
480
|
+
headers.push(["content-type", "application/json; charset=utf-8"]);
|
|
481
|
+
} else if (!isNullBody) {
|
|
482
|
+
// send(null) sends an empty string without choosing a type, the same
|
|
483
|
+
// as it does on the ordinary path
|
|
484
|
+
headers.push(["content-type", "text/html; charset=utf-8"]);
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
existing[1] = withUtf8Charset(String(existing[1]));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (arg) {
|
|
491
|
+
if (arg.type === "Literal") {
|
|
492
|
+
if (typeof arg.value === "number") {
|
|
493
|
+
// status code
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
// the content-type was decided above, from what this argument is
|
|
497
|
+
const val = arg.value === null ? "" : arg.value;
|
|
498
|
+
body.push({ type: "text", value: val });
|
|
499
|
+
} else if (arg.type === "TemplateLiteral") {
|
|
500
|
+
const exprs = [...arg.quasis, ...arg.expressions].sort((a, b) => a.start - b.start);
|
|
501
|
+
for (const expr of exprs) {
|
|
502
|
+
if (expr.type === "TemplateElement") {
|
|
503
|
+
body.push({ type: "text", value: expr.value.cooked });
|
|
504
|
+
} else if (expr.type === "MemberExpression") {
|
|
505
|
+
const obj = expr.object;
|
|
506
|
+
let type;
|
|
507
|
+
if (obj.type === "MemberExpression") {
|
|
508
|
+
if (obj.property.type !== "Identifier") {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
type = obj.property.name;
|
|
512
|
+
} else if (obj.type === "Identifier") {
|
|
513
|
+
type = obj.name;
|
|
514
|
+
} else {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
if (type !== "params" && type !== "query") {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
body.push({ type, value: expr.property.name });
|
|
521
|
+
} else if (expr.type === "Identifier") {
|
|
522
|
+
if (queries.includes(expr.name)) {
|
|
523
|
+
body.push({ type: "query", value: expr.name });
|
|
524
|
+
} else if (params.includes(expr.name)) {
|
|
525
|
+
body.push({ type: "params", value: expr.name });
|
|
526
|
+
} else {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} else if (arg.type === "MemberExpression") {
|
|
534
|
+
if (!arg.object.property) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
if (
|
|
538
|
+
arg.object.property.type !== "Identifier" ||
|
|
539
|
+
(arg.object.property.name !== "query" && arg.object.property.name !== "params")
|
|
540
|
+
) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
body.push({ type: arg.object.property.name, value: arg.property.name });
|
|
544
|
+
} else if (arg.type === "BinaryExpression") {
|
|
545
|
+
const stuff = [];
|
|
546
|
+
/**
|
|
547
|
+
* Reads a chain of string concatenations right to left, collecting each side as either a
|
|
548
|
+
* literal or a param or query value. Returns false for anything else, which is what makes
|
|
549
|
+
* the whole handler fall back.
|
|
550
|
+
*
|
|
551
|
+
* @param {any} node a BinaryExpression
|
|
552
|
+
* @returns {boolean}
|
|
553
|
+
*/
|
|
554
|
+
function check(node) {
|
|
555
|
+
if (node.right.type === "Literal") {
|
|
556
|
+
stuff.push({ type: "text", value: node.right.value });
|
|
557
|
+
} else if (node.right.type === "MemberExpression") {
|
|
558
|
+
stuff.push({ type: node.right.object.property.name, value: node.right.property.name });
|
|
559
|
+
} else return false;
|
|
560
|
+
if (node.left.type === "Literal") {
|
|
561
|
+
stuff.push({ type: "text", value: node.left.value });
|
|
562
|
+
} else if (node.left.type === "MemberExpression") {
|
|
563
|
+
stuff.push({ type: node.left.object.property.name, value: node.left.property.name });
|
|
564
|
+
} else if (node.left.type === "BinaryExpression") {
|
|
565
|
+
return check(node.left);
|
|
566
|
+
} else return false;
|
|
567
|
+
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
if (!check(arg)) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
body.push(...stuff.reverse());
|
|
574
|
+
} else if (arg.type === "ObjectExpression") {
|
|
575
|
+
if (call.obj.propertyName === "end") {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
// a replacer runs per response on the ordinary path, so a body computed
|
|
579
|
+
// once could not honour it
|
|
580
|
+
const replacer = app.get("json replacer");
|
|
581
|
+
if (typeof replacer !== "undefined" && typeof replacer !== "string") {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// the content-type was decided above, from what this argument is
|
|
586
|
+
body.push({
|
|
587
|
+
type: "text",
|
|
588
|
+
value: stringify(
|
|
589
|
+
literalValue(arg),
|
|
590
|
+
replacer,
|
|
591
|
+
app.get("json spaces"),
|
|
592
|
+
app.get("json escape")
|
|
593
|
+
)
|
|
594
|
+
});
|
|
595
|
+
} else {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
sendUsed = true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
let decRes = new uWSAny.DeclarativeResponse();
|
|
604
|
+
|
|
605
|
+
if (statusCode != 200) {
|
|
606
|
+
const statusMessage = statuses.message[statusCode] ?? "";
|
|
607
|
+
decRes = decRes.writeStatus(`${statusCode} ${statusMessage}`.trim());
|
|
608
|
+
if (!headers.some((header) => header[0].toLowerCase() === "content-type")) {
|
|
609
|
+
decRes = decRes.writeHeader("content-type", "text/plain; charset=utf-8");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// the same two the ordinary path seeds every response with. Without them a route answered
|
|
614
|
+
// different headers depending only on whether it happened to be compilable, which is worse
|
|
615
|
+
// than either choice on its own, and a client had no idle timeout to go on.
|
|
616
|
+
const connection = headers.find((header) => header[0].toLowerCase() === "connection");
|
|
617
|
+
if (!connection) {
|
|
618
|
+
decRes = decRes.writeHeader("connection", "keep-alive");
|
|
619
|
+
}
|
|
620
|
+
// not on a connection the handler is closing: Keep-Alive describes one that is staying
|
|
621
|
+
// open, and the ordinary path leaves it out for the same reason
|
|
622
|
+
const closing = typeof connection?.[1] === "string" && connection[1].toLowerCase() === "close";
|
|
623
|
+
if (!closing && !headers.some((header) => header[0].toLowerCase() === "keep-alive")) {
|
|
624
|
+
decRes = decRes.writeHeader("keep-alive", "timeout=10");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
for (const header of headers) {
|
|
628
|
+
if (header[0].toLowerCase() === "content-length") {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
decRes = decRes.writeHeader(header[0], header[1]);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// sendStatus sends the status message as its body. It has to join `body` here, before the
|
|
635
|
+
// ETag is computed, and not at write time: computing the ETag over an empty body gave every
|
|
636
|
+
// sendStatus response the same one, so a cache could not tell a 404 from a 500 and a
|
|
637
|
+
// conditional request could be answered 304 with the wrong body entirely.
|
|
638
|
+
if (sendStatusUsed && !body.length) {
|
|
639
|
+
body.push({ type: "text", value: statuses.message[statusCode] || String(statusCode) });
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// an empty body gets no ETag, which is what Express does and what the ordinary path here
|
|
643
|
+
// already did
|
|
644
|
+
if (
|
|
645
|
+
body.length &&
|
|
646
|
+
(bodyFromSend || sendStatusUsed) &&
|
|
647
|
+
app.get("etag") &&
|
|
648
|
+
!headers.some((header) => header[0].toLowerCase() === "etag")
|
|
649
|
+
) {
|
|
650
|
+
if (body.some((part) => part.type !== "text")) {
|
|
651
|
+
return false;
|
|
652
|
+
} else {
|
|
653
|
+
const etag = app.get("etag fn")(body.map((part) => part.value.toString()).join(""));
|
|
654
|
+
// an application's own etag function is allowed to decline, and a declarative
|
|
655
|
+
// response cannot answer with a header whose value is nothing
|
|
656
|
+
if (etag) {
|
|
657
|
+
decRes = decRes.writeHeader("ETag", etag);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// No Content-Length here, and it is not an oversight. uWS writes a DeclarativeResponse
|
|
663
|
+
// with Transfer-Encoding: chunked and adds that header itself, so setting Content-Length
|
|
664
|
+
// as well produces a response carrying both, which is invalid and which clients reject
|
|
665
|
+
// outright. Every declarative response is therefore chunked, where Express always sends
|
|
666
|
+
// a length. Changing it means changing uWS.
|
|
667
|
+
if (app.get("x-powered-by")) {
|
|
668
|
+
decRes = decRes.writeHeader("x-powered-by", "Fulmine");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
for (const bodyPart of body) {
|
|
672
|
+
if (bodyPart.type === "text" && String(bodyPart.value).length) {
|
|
673
|
+
decRes = decRes.write(String(bodyPart.value));
|
|
674
|
+
} else if (bodyPart.type === "params") {
|
|
675
|
+
decRes = decRes.writeParameterValue(bodyPart.value);
|
|
676
|
+
} else if (bodyPart.type === "query") {
|
|
677
|
+
decRes = decRes.writeQueryValue(bodyPart.value);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return decRes.end();
|
|
682
|
+
} catch (e) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Every node in the tree matching the predicate, in the order the named edges below are followed.
|
|
689
|
+
* The edges are written out rather than discovered, which is why compileDeclarative first refuses
|
|
690
|
+
* any node type that is not on the understood list: a shape this walk cannot see through would
|
|
691
|
+
* otherwise be compiled as though it were empty.
|
|
692
|
+
*
|
|
693
|
+
* @param {any} node
|
|
694
|
+
* @param {(node: any) => boolean} fn
|
|
695
|
+
* @returns {any[]}
|
|
696
|
+
*/
|
|
697
|
+
function filterNodes(node, fn) {
|
|
698
|
+
const filtered = [];
|
|
699
|
+
if (fn(node)) {
|
|
700
|
+
filtered.push(node);
|
|
701
|
+
}
|
|
702
|
+
if (node.params) {
|
|
703
|
+
for (const param of node.params) {
|
|
704
|
+
filtered.push(...filterNodes(param, fn));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (node.body) {
|
|
709
|
+
if (Array.isArray(node.body)) {
|
|
710
|
+
for (const child of node.body) {
|
|
711
|
+
filtered.push(...filterNodes(child, fn));
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
filtered.push(...filterNodes(node.body, fn));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (node.declarations) {
|
|
719
|
+
for (const declaration of node.declarations) {
|
|
720
|
+
filtered.push(...filterNodes(declaration, fn));
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (node.expression) {
|
|
725
|
+
filtered.push(...filterNodes(node.expression, fn));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (node.callee) {
|
|
729
|
+
filtered.push(...filterNodes(node.callee, fn));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (node.object) {
|
|
733
|
+
filtered.push(...filterNodes(node.object, fn));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (node.property) {
|
|
737
|
+
filtered.push(...filterNodes(node.property, fn));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (node.id) {
|
|
741
|
+
filtered.push(...filterNodes(node.id, fn));
|
|
742
|
+
}
|
|
743
|
+
if (node.init) {
|
|
744
|
+
filtered.push(...filterNodes(node.init, fn));
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (node.left) {
|
|
748
|
+
filtered.push(...filterNodes(node.left, fn));
|
|
749
|
+
}
|
|
750
|
+
if (node.right) {
|
|
751
|
+
filtered.push(...filterNodes(node.right, fn));
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (node.arguments) {
|
|
755
|
+
for (const argument of node.arguments) {
|
|
756
|
+
filtered.push(...filterNodes(argument, fn));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// singular, and not the list above: it is what a return statement returns, and what a unary
|
|
761
|
+
// operator applies to. Without it `return res.send("x")` looked like a body with no calls in
|
|
762
|
+
// it at all, which would have compiled into an empty response.
|
|
763
|
+
if (node.argument) {
|
|
764
|
+
filtered.push(...filterNodes(node.argument, fn));
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return filtered;
|
|
768
|
+
}
|