fulmine.js 5.1.7 → 5.1.8
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/package.json +1 -1
- package/src/application.js +1 -0
- package/src/request.js +10 -3
- package/src/router.js +16 -12
- package/src/usage.js +81 -42
package/package.json
CHANGED
package/src/application.js
CHANGED
package/src/request.js
CHANGED
|
@@ -263,9 +263,16 @@ module.exports = class Request extends Readable {
|
|
|
263
263
|
this.app = app;
|
|
264
264
|
// both forms are kept, because both are asked for: the query with its "?" goes into
|
|
265
265
|
// req.url, and req.query parses the raw one. Keeping only the first meant slicing the "?"
|
|
266
|
-
// back off for every request that reads req.query.
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
// back off for every request that reads req.query. When the chain provably reads
|
|
267
|
+
// neither, the native call is not made at all: the framework's own answers, the 404
|
|
268
|
+
// included, are written from the path alone
|
|
269
|
+
if (skipHolder !== undefined && skipHolder.skipQuery) {
|
|
270
|
+
this._rawQuery = "";
|
|
271
|
+
this.urlQuery = "";
|
|
272
|
+
} else {
|
|
273
|
+
this._rawQuery = req.getQuery() ?? "";
|
|
274
|
+
this.urlQuery = this._rawQuery === "" ? "" : "?" + this._rawQuery;
|
|
275
|
+
}
|
|
269
276
|
if (preset) {
|
|
270
277
|
// the registration's constants: two native crossings and their strings not asked for
|
|
271
278
|
this.path = preset.path;
|
package/src/router.js
CHANGED
|
@@ -36,7 +36,7 @@ const compileDeclarative = require("./declarative.js");
|
|
|
36
36
|
const statuses = require("statuses");
|
|
37
37
|
const { METHODS } = require("http");
|
|
38
38
|
const { isNodeRequest, serveNodeRequest } = require("./node-shim.js");
|
|
39
|
-
const {
|
|
39
|
+
const { chainUsage } = require("./usage.js");
|
|
40
40
|
|
|
41
41
|
// every method the declarative compiler can emit: a patched one must disable compilation, or the
|
|
42
42
|
// patch would be honoured everywhere but on compiled routes
|
|
@@ -693,9 +693,10 @@ function nativePreset(path, method, strict) {
|
|
|
693
693
|
opPath: endsWithSlash && path !== "/" && !strict ? path.slice(0, -1) : path,
|
|
694
694
|
isOptions: method === "OPTIONS",
|
|
695
695
|
isHead: method === "HEAD",
|
|
696
|
-
// set at registration when the whole chain provably never reads a header
|
|
697
|
-
// because a middleware added after listen
|
|
698
|
-
skipHeaders: false
|
|
696
|
+
// set at registration when the whole chain provably never reads a header, or never
|
|
697
|
+
// reads the query; mutable, because a middleware added after listen takes them back
|
|
698
|
+
skipHeaders: false,
|
|
699
|
+
skipQuery: false
|
|
699
700
|
};
|
|
700
701
|
}
|
|
701
702
|
|
|
@@ -1148,6 +1149,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1148
1149
|
if (this._skipPresets?.size) {
|
|
1149
1150
|
for (const preset of this._skipPresets) {
|
|
1150
1151
|
preset.skipHeaders = false;
|
|
1152
|
+
preset.skipQuery = false;
|
|
1151
1153
|
}
|
|
1152
1154
|
this._skipPresets.clear();
|
|
1153
1155
|
}
|
|
@@ -1469,8 +1471,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
1469
1471
|
// listen can take it back: a literal registration's preset doubles as it, and a
|
|
1470
1472
|
// parameterised one, which has no preset, gets a holder of its own
|
|
1471
1473
|
let skipHolder = preset;
|
|
1472
|
-
if (skipHolder === undefined && skips) {
|
|
1473
|
-
skipHolder = { skipHeaders:
|
|
1474
|
+
if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery)) {
|
|
1475
|
+
skipHolder = { skipHeaders: skips.skipHeaders, skipQuery: skips.skipQuery };
|
|
1474
1476
|
(this._skipPresets ??= new Set()).add(skipHolder);
|
|
1475
1477
|
}
|
|
1476
1478
|
// all three are registration-time constants: computing them in the handler was a
|
|
@@ -1532,8 +1534,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
1532
1534
|
// headers), no error middleware may exist anywhere (a throw hands the request to code
|
|
1533
1535
|
// the analysis never saw), and every callback in the chain has to pass the source
|
|
1534
1536
|
// analysis in usage.js, whose default answer is no.
|
|
1535
|
-
|
|
1536
|
-
let
|
|
1537
|
+
const NO_SKIPS = { skipHeaders: false, skipQuery: false };
|
|
1538
|
+
let getSkips = NO_SKIPS;
|
|
1539
|
+
let headSkips = NO_SKIPS;
|
|
1537
1540
|
if (route.method === "GET" && this.get("etag") === false) {
|
|
1538
1541
|
let hasErr = this._hasErrMwCache;
|
|
1539
1542
|
if (hasErr === undefined) {
|
|
@@ -1544,15 +1547,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
1544
1547
|
// route may be able to catch the same path
|
|
1545
1548
|
const owner = route.owner ?? this;
|
|
1546
1549
|
const noLaterMatch = !owner._isFollowedByAnOverlap.call(owner, route, owner._routes);
|
|
1547
|
-
getSkips =
|
|
1548
|
-
headSkips = headChain === getChain ? getSkips :
|
|
1550
|
+
getSkips = chainUsage(getChain, noLaterMatch);
|
|
1551
|
+
headSkips = headChain === getChain ? getSkips : chainUsage(headChain, noLaterMatch);
|
|
1549
1552
|
}
|
|
1550
1553
|
}
|
|
1551
1554
|
// remembered so a middleware or setting arriving after listen can take the skips back
|
|
1552
1555
|
const makePreset = (path, method, skips) => {
|
|
1553
1556
|
const preset = nativePreset(path, method, strictHere);
|
|
1554
|
-
if (skips) {
|
|
1555
|
-
preset.skipHeaders =
|
|
1557
|
+
if (skips.skipHeaders || skips.skipQuery) {
|
|
1558
|
+
preset.skipHeaders = skips.skipHeaders;
|
|
1559
|
+
preset.skipQuery = skips.skipQuery;
|
|
1556
1560
|
(this._skipPresets ??= new Set()).add(preset);
|
|
1557
1561
|
}
|
|
1558
1562
|
return preset;
|
package/src/usage.js
CHANGED
|
@@ -13,6 +13,13 @@ const kGetSafe = Symbol("fulmine.getSafe");
|
|
|
13
13
|
// the parameter, so anything that can reach another object could reach headers through it.
|
|
14
14
|
const REQ_OK = new Set(["query", "params", "body", "method", "path", "url", "baseUrl", "originalUrl", "route"]);
|
|
15
15
|
|
|
16
|
+
// Reading any of these needs the query string fetched: req.url and req.originalUrl carry it
|
|
17
|
+
const REQ_QUERY = new Set(["query", "url", "originalUrl"]);
|
|
18
|
+
|
|
19
|
+
// Writing any of these re-enters routing: dispatch treats a changed req.url as a rewrite and
|
|
20
|
+
// walks routes nobody analyzed, so an assignment is as disqualifying as an unknown call
|
|
21
|
+
const REQ_NO_WRITE = new Set(["url", "originalUrl", "path", "baseUrl", "method"]);
|
|
22
|
+
|
|
16
23
|
// What a handler may do with `res`: writing the response. Anything that negotiates against
|
|
17
24
|
// request headers (format, redirect, sendFile, jsonp) is deliberately absent.
|
|
18
25
|
const RES_OK = new Set([
|
|
@@ -38,25 +45,26 @@ const RES_OK = new Set([
|
|
|
38
45
|
"cork"
|
|
39
46
|
]);
|
|
40
47
|
|
|
41
|
-
// what the analysis can say about one callback
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
48
|
+
// what the analysis can say about one callback, as independent facts
|
|
49
|
+
const UNKNOWN = 1; // a shape the walk cannot vouch for: could do anything
|
|
50
|
+
const NEXT_PLAIN = 2; // calls next() bare: advances the chain, may fall off its end
|
|
51
|
+
const NEXT_ERROR = 4; // calls next(err): lands in the framework's own error answer
|
|
52
|
+
const QUERY = 8; // reads req.query, req.url or req.originalUrl
|
|
46
53
|
|
|
47
54
|
const verdicts = new WeakMap();
|
|
48
55
|
|
|
49
56
|
/**
|
|
50
|
-
* What one callback provably does. The default is
|
|
51
|
-
* and any alias of req, res or next
|
|
52
|
-
* source analysis sound to act on.
|
|
57
|
+
* What one callback provably does, as a mask of the facts above. The default is UNKNOWN: any
|
|
58
|
+
* shape this walk does not understand and any alias of req, res or next could do anything.
|
|
59
|
+
* That inversion is what makes source analysis sound to act on.
|
|
53
60
|
*
|
|
54
61
|
* @param {Function} fn
|
|
55
|
-
* @returns {number}
|
|
62
|
+
* @returns {number}
|
|
56
63
|
*/
|
|
57
|
-
function
|
|
64
|
+
function callbackUsage(fn) {
|
|
58
65
|
if (fn[kGetSafe]) {
|
|
59
|
-
|
|
66
|
+
// the body parsers: they advance the chain and read nothing a skip would miss
|
|
67
|
+
return NEXT_PLAIN;
|
|
60
68
|
}
|
|
61
69
|
let verdict = verdicts.get(fn);
|
|
62
70
|
if (verdict === undefined) {
|
|
@@ -77,11 +85,11 @@ function analyze(fn) {
|
|
|
77
85
|
tree = acorn.parse(code, { ecmaVersion: "latest" });
|
|
78
86
|
} catch {
|
|
79
87
|
// class methods and native functions do not parse alone, and unread code is unknown code
|
|
80
|
-
return
|
|
88
|
+
return UNKNOWN;
|
|
81
89
|
}
|
|
82
90
|
let root = /** @type {any} */ (tree.body[0]);
|
|
83
91
|
if (!root) {
|
|
84
|
-
return
|
|
92
|
+
return UNKNOWN;
|
|
85
93
|
}
|
|
86
94
|
if (root.type === "ExpressionStatement") {
|
|
87
95
|
root = root.expression;
|
|
@@ -91,14 +99,14 @@ function analyze(fn) {
|
|
|
91
99
|
root.type !== "ArrowFunctionExpression" &&
|
|
92
100
|
root.type !== "FunctionExpression"
|
|
93
101
|
) {
|
|
94
|
-
return
|
|
102
|
+
return UNKNOWN;
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
const params = /** @type {any[]} */ (root.params);
|
|
98
106
|
// rest or destructured parameters alias the objects somewhere the walk cannot follow
|
|
99
107
|
for (const p of params) {
|
|
100
108
|
if (p.type !== "Identifier") {
|
|
101
|
-
return
|
|
109
|
+
return UNKNOWN;
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
const reqName = params[0] ? params[0].name : null;
|
|
@@ -108,15 +116,38 @@ function analyze(fn) {
|
|
|
108
116
|
// Every appearance of the three names in the whole body is judged, nested functions
|
|
109
117
|
// included: an inner binding that shadows one of them only makes this stricter, never
|
|
110
118
|
// looser, so scope tracking is not needed for soundness.
|
|
111
|
-
let
|
|
112
|
-
let usesNext = false;
|
|
119
|
+
let mask = 0;
|
|
113
120
|
walk(root.body, null, (node, parent) => {
|
|
114
|
-
if (
|
|
121
|
+
if (mask & UNKNOWN) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier") {
|
|
125
|
+
const owner = node.object.name;
|
|
126
|
+
if (owner !== reqName) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const member = node.property.name;
|
|
130
|
+
if (REQ_QUERY.has(member)) {
|
|
131
|
+
mask |= QUERY;
|
|
132
|
+
}
|
|
133
|
+
// an assignment, an update or a delete on the routing members re-enters dispatch
|
|
134
|
+
if (
|
|
135
|
+
REQ_NO_WRITE.has(member) &&
|
|
136
|
+
parent &&
|
|
137
|
+
((parent.type === "AssignmentExpression" && parent.left === node) ||
|
|
138
|
+
(parent.type === "UpdateExpression" && parent.argument === node) ||
|
|
139
|
+
(parent.type === "UnaryExpression" && parent.operator === "delete" && parent.argument === node))
|
|
140
|
+
) {
|
|
141
|
+
mask |= UNKNOWN;
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (node.type !== "Identifier") {
|
|
115
146
|
return;
|
|
116
147
|
}
|
|
117
148
|
const name = node.name;
|
|
118
149
|
if (name === "eval" || name === "arguments") {
|
|
119
|
-
|
|
150
|
+
mask |= UNKNOWN;
|
|
120
151
|
return;
|
|
121
152
|
}
|
|
122
153
|
if (name !== reqName && name !== resName && name !== nextName) {
|
|
@@ -140,17 +171,17 @@ function analyze(fn) {
|
|
|
140
171
|
}
|
|
141
172
|
if (name === nextName) {
|
|
142
173
|
// calling next is how a chain advances, and past its end or with an error the
|
|
143
|
-
// request lands in the framework's own final
|
|
174
|
+
// request lands in the framework's own final answer, which the constructor's
|
|
144
175
|
// accept pre-read covers. Anything but a direct call aliases the continuation,
|
|
145
176
|
// and an argument that could be the string "route" would leave the chain for
|
|
146
177
|
// routes nobody analyzed, so only shapes that cannot be a string pass.
|
|
147
178
|
if (!parent || parent.type !== "CallExpression" || parent.callee !== node) {
|
|
148
|
-
|
|
179
|
+
mask |= UNKNOWN;
|
|
149
180
|
return;
|
|
150
181
|
}
|
|
151
|
-
usesNext = true;
|
|
152
182
|
const args = parent.arguments;
|
|
153
183
|
if (args.length === 0) {
|
|
184
|
+
mask |= NEXT_PLAIN;
|
|
154
185
|
return;
|
|
155
186
|
}
|
|
156
187
|
const arg = args[0];
|
|
@@ -160,20 +191,22 @@ function analyze(fn) {
|
|
|
160
191
|
arg.type !== "ObjectExpression" &&
|
|
161
192
|
!(arg.type === "Literal" && typeof arg.value !== "string"))
|
|
162
193
|
) {
|
|
163
|
-
|
|
194
|
+
mask |= UNKNOWN;
|
|
195
|
+
return;
|
|
164
196
|
}
|
|
197
|
+
mask |= NEXT_ERROR;
|
|
165
198
|
return;
|
|
166
199
|
}
|
|
167
200
|
if (!parent || parent.type !== "MemberExpression" || parent.object !== node || parent.computed) {
|
|
168
|
-
|
|
201
|
+
mask |= UNKNOWN;
|
|
169
202
|
return;
|
|
170
203
|
}
|
|
171
204
|
const member = parent.property.name;
|
|
172
205
|
if (name === reqName ? !REQ_OK.has(member) : !RES_OK.has(member)) {
|
|
173
|
-
|
|
206
|
+
mask |= UNKNOWN;
|
|
174
207
|
}
|
|
175
208
|
});
|
|
176
|
-
return
|
|
209
|
+
return mask;
|
|
177
210
|
}
|
|
178
211
|
|
|
179
212
|
/**
|
|
@@ -207,41 +240,47 @@ function walk(node, parent, visit) {
|
|
|
207
240
|
}
|
|
208
241
|
|
|
209
242
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
243
|
+
* What a native route's whole chain provably never does, so the request constructor may leave
|
|
244
|
+
* that work undone: skipHeaders spares the header copy, skipQuery the query fetch.
|
|
212
245
|
*
|
|
213
|
-
* A callback that calls next passes anywhere but in the terminal route, where
|
|
214
|
-
* out of the chain: there it only passes when the caller established that no later route
|
|
215
|
-
* could catch the fall-through.
|
|
246
|
+
* A callback that calls next() bare passes anywhere but in the terminal route, where it would
|
|
247
|
+
* fall out of the chain: there it only passes when the caller established that no later route
|
|
248
|
+
* could catch the fall-through. The framework's own 404 answers with the path alone, so the
|
|
249
|
+
* fall-through itself needs neither headers nor query.
|
|
216
250
|
*
|
|
217
251
|
* @param {any[]} chain the routes the native handler runs, in order, this route last
|
|
218
252
|
* @param {boolean} allowTerminalNext whether a fall-through past the chain lands only in the
|
|
219
|
-
* framework's own final
|
|
220
|
-
* @returns {boolean}
|
|
253
|
+
* framework's own final answer
|
|
254
|
+
* @returns {{skipHeaders: boolean, skipQuery: boolean}}
|
|
221
255
|
*/
|
|
222
|
-
function
|
|
256
|
+
function chainUsage(chain, allowTerminalNext) {
|
|
257
|
+
const none = { skipHeaders: false, skipQuery: false };
|
|
258
|
+
let query = false;
|
|
223
259
|
for (let i = 0; i < chain.length; i++) {
|
|
224
260
|
const entry = chain[i];
|
|
225
261
|
const callbacks = entry.callbacks;
|
|
226
262
|
if (!Array.isArray(callbacks)) {
|
|
227
|
-
return
|
|
263
|
+
return none;
|
|
228
264
|
}
|
|
229
265
|
const terminal = i === chain.length - 1;
|
|
230
266
|
for (const cb of callbacks) {
|
|
231
267
|
if (typeof cb !== "function") {
|
|
232
|
-
return
|
|
268
|
+
return none;
|
|
269
|
+
}
|
|
270
|
+
const mask = callbackUsage(cb);
|
|
271
|
+
if (mask & UNKNOWN || (mask & NEXT_PLAIN && terminal && !allowTerminalNext)) {
|
|
272
|
+
return none;
|
|
233
273
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return false;
|
|
274
|
+
if (mask & QUERY) {
|
|
275
|
+
query = true;
|
|
237
276
|
}
|
|
238
277
|
}
|
|
239
278
|
// a param callback runs code this walk never saw
|
|
240
279
|
if (entry.paramCallbacks && entry.paramCallbacks.size > 0) {
|
|
241
|
-
return
|
|
280
|
+
return none;
|
|
242
281
|
}
|
|
243
282
|
}
|
|
244
|
-
return true;
|
|
283
|
+
return { skipHeaders: true, skipQuery: !query };
|
|
245
284
|
}
|
|
246
285
|
|
|
247
|
-
module.exports = {
|
|
286
|
+
module.exports = { chainUsage, callbackUsage, kGetSafe, UNKNOWN, NEXT_PLAIN, NEXT_ERROR, QUERY };
|