fulmine.js 5.13.1 → 5.13.2
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/middlewares.js +61 -9
- package/src/request.js +40 -1
- package/src/router.js +48 -0
package/package.json
CHANGED
package/src/middlewares.js
CHANGED
|
@@ -363,6 +363,41 @@ function pickPrecompressed(filePath, accept, ttl, statTtl) {
|
|
|
363
363
|
return undefined;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
/**
|
|
367
|
+
* The index file to serve from a directory, tried in the order the option lists them, which is
|
|
368
|
+
* send's sendIndex. Throws the last failure when every name failed, and reports nothing when the
|
|
369
|
+
* list ran out without one, since those are the two different answers send gives.
|
|
370
|
+
*
|
|
371
|
+
* @param {string} dir the directory to look in
|
|
372
|
+
* @param {string[]} indexList the index names, in order
|
|
373
|
+
* @returns {{stat: any, name: string, candidate: string}|null} the file to serve, or null
|
|
374
|
+
*/
|
|
375
|
+
function findIndexFile(dir, indexList) {
|
|
376
|
+
let lastError;
|
|
377
|
+
for (const name of indexList) {
|
|
378
|
+
const candidate = path.join(dir, name);
|
|
379
|
+
let stat;
|
|
380
|
+
try {
|
|
381
|
+
stat = fs.statSync(candidate);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
lastError = err;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
// a directory by that name is not an index and is not an error either: send's own loop
|
|
387
|
+
// carries on with nothing to report, so a later name still answers and an exhausted list
|
|
388
|
+
// is the plain 404 rather than whatever the name before it failed with
|
|
389
|
+
if (stat.isDirectory()) {
|
|
390
|
+
lastError = undefined;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
return { stat, name, candidate };
|
|
394
|
+
}
|
|
395
|
+
if (lastError) {
|
|
396
|
+
throw lastError;
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
366
401
|
/**
|
|
367
402
|
* express.static, which is a thin front for res.sendFile: it resolves the path, refuses anything
|
|
368
403
|
* that climbs out of the root, applies the dotfiles and index rules, and hands the rest over.
|
|
@@ -384,6 +419,9 @@ function serveStatic(root, options) {
|
|
|
384
419
|
// mounts sharing one options object would otherwise also share one root
|
|
385
420
|
options = Object.assign(new NullObject(), options);
|
|
386
421
|
if (typeof options.index === "undefined") options.index = "index.html";
|
|
422
|
+
// send takes a list and tries the names in order, so one name is a list of one and `false` is
|
|
423
|
+
// an empty one. Passing the option along as it came handed an array to path.join, which throws
|
|
424
|
+
const indexList = options.index === false || options.index === "" ? [] : [options.index].flat();
|
|
387
425
|
if (typeof options.redirect === "undefined") options.redirect = true;
|
|
388
426
|
if (typeof options.fallthrough === "undefined") options.fallthrough = true;
|
|
389
427
|
if (typeof options.dotfiles === "undefined") options.dotfiles = "ignore";
|
|
@@ -556,9 +594,9 @@ function serveStatic(root, options) {
|
|
|
556
594
|
// a path written with a trailing slash asks for a directory, and send answers that by
|
|
557
595
|
// looking for the index inside it. With nothing there, the file it names is that
|
|
558
596
|
// index and not the directory that does not exist either
|
|
559
|
-
if (rawPath.endsWith("/") &&
|
|
597
|
+
if (rawPath.endsWith("/") && indexList.length > 0) {
|
|
560
598
|
try {
|
|
561
|
-
|
|
599
|
+
findIndexFile(fullpath, indexList);
|
|
562
600
|
} catch (indexError) {
|
|
563
601
|
statError = indexError;
|
|
564
602
|
}
|
|
@@ -595,8 +633,11 @@ function serveStatic(root, options) {
|
|
|
595
633
|
}
|
|
596
634
|
|
|
597
635
|
// a file asked for with a trailing slash is not that file: send stats the path slash and
|
|
598
|
-
// all and gets ENOTDIR, so a root mounted as a file answers 404 there, not the file
|
|
599
|
-
|
|
636
|
+
// all and gets ENOTDIR, so a root mounted as a file answers 404 there, not the file.
|
|
637
|
+
// With an index configured it never gets that far: a trailing slash sends send looking for
|
|
638
|
+
// the index inside whatever the path turned out to be, so what it reports is that the
|
|
639
|
+
// index under the file is missing, and the directory branch below does it for both
|
|
640
|
+
if (req.endsWithSlash && !stat.isDirectory() && indexList.length === 0) {
|
|
600
641
|
if (!options.fallthrough) {
|
|
601
642
|
res.status(404);
|
|
602
643
|
return next(httpError(404));
|
|
@@ -604,7 +645,7 @@ function serveStatic(root, options) {
|
|
|
604
645
|
return next();
|
|
605
646
|
}
|
|
606
647
|
|
|
607
|
-
if (stat.isDirectory()) {
|
|
648
|
+
if (stat.isDirectory() || req.endsWithSlash) {
|
|
608
649
|
if (!req.endsWithSlash) {
|
|
609
650
|
if (options.redirect) {
|
|
610
651
|
// The query goes along, and the leading slashes are collapsed. Both were
|
|
@@ -624,11 +665,10 @@ function serveStatic(root, options) {
|
|
|
624
665
|
} else return next();
|
|
625
666
|
}
|
|
626
667
|
}
|
|
627
|
-
if (
|
|
668
|
+
if (indexList.length > 0) {
|
|
669
|
+
let found;
|
|
628
670
|
try {
|
|
629
|
-
|
|
630
|
-
_path = path.join(url, options.index);
|
|
631
|
-
filePath = path.join(fullpath, options.index);
|
|
671
|
+
found = findIndexFile(fullpath, indexList);
|
|
632
672
|
} catch (err) {
|
|
633
673
|
if (!options.fallthrough) {
|
|
634
674
|
res.status(404);
|
|
@@ -637,6 +677,18 @@ function serveStatic(root, options) {
|
|
|
637
677
|
return next(asStatError(err));
|
|
638
678
|
} else return next();
|
|
639
679
|
}
|
|
680
|
+
if (found === null) {
|
|
681
|
+
// every name was a directory, which send reports as its plain 404 rather than
|
|
682
|
+
// as a file that could not be read
|
|
683
|
+
if (!options.fallthrough) {
|
|
684
|
+
res.status(404);
|
|
685
|
+
return next(httpError(404));
|
|
686
|
+
}
|
|
687
|
+
return next();
|
|
688
|
+
}
|
|
689
|
+
stat = found.stat;
|
|
690
|
+
_path = path.join(url, found.name);
|
|
691
|
+
filePath = found.candidate;
|
|
640
692
|
} else {
|
|
641
693
|
// a directory with no index to serve is a Not Found, and saying so is the whole
|
|
642
694
|
// point of fallthrough: false. This moved on to the next handler instead, so the
|
package/src/request.js
CHANGED
|
@@ -730,6 +730,10 @@ module.exports = class Request extends LazyReadable {
|
|
|
730
730
|
this._isOptions = this.method === "OPTIONS";
|
|
731
731
|
this._isHead = this.method === "HEAD";
|
|
732
732
|
}
|
|
733
|
+
// what the router last saw as the method. A middleware assigning another one is a rewrite,
|
|
734
|
+
// which express honours because it reads req.method at every layer, and dispatch compares
|
|
735
|
+
// against this to notice it
|
|
736
|
+
this._lastMethod = this.method;
|
|
733
737
|
// the folded _opPath and the percent scan of _originalPath, built on the hop that first
|
|
734
738
|
// wants them and dropped by every rewrite, see _pathMatches and Walk#dispatch
|
|
735
739
|
this._opPathLower = null;
|
|
@@ -753,6 +757,8 @@ module.exports = class Request extends LazyReadable {
|
|
|
753
757
|
// how many characters of _originalPath the mounts entered so far have taken, which is
|
|
754
758
|
// where baseUrl ends and the path below them begins
|
|
755
759
|
this._consumed = 0;
|
|
760
|
+
// whether one of them took a trailing slash, which only a RegExp mount can: see baseUrl
|
|
761
|
+
this._mountSlash = false;
|
|
756
762
|
this._paramStack = null;
|
|
757
763
|
// route and application in pairs, one pair per mounted application entered from another
|
|
758
764
|
// application, so handing back puts the one that was current back, see rememberApp
|
|
@@ -934,8 +940,26 @@ module.exports = class Request extends LazyReadable {
|
|
|
934
940
|
if (this._baseUrlOverride !== undefined) {
|
|
935
941
|
return this._baseUrlOverride;
|
|
936
942
|
}
|
|
943
|
+
if (this._consumed === 0) {
|
|
944
|
+
return "";
|
|
945
|
+
}
|
|
937
946
|
// what the mounts took, which is where the path they left off begins
|
|
938
|
-
|
|
947
|
+
if (this._mountSlash !== true) {
|
|
948
|
+
return this._originalPath.slice(0, this._consumed);
|
|
949
|
+
}
|
|
950
|
+
// Express drops one trailing slash off each mount before joining them, so this is a join
|
|
951
|
+
// of the pieces rather than one slice of the path: a RegExp mount ending in "/" matched
|
|
952
|
+
// against "/a//b" takes "/a/" and reads back as "/a", and what the mount below it took is
|
|
953
|
+
// appended to that rather than to the original. Only a RegExp mount can take a trailing
|
|
954
|
+
// slash, a registered path having had it removed, so almost every request answers above.
|
|
955
|
+
let out = "";
|
|
956
|
+
let at = 0;
|
|
957
|
+
for (const taken of this._stack) {
|
|
958
|
+
const piece = this._originalPath.slice(at, at + taken);
|
|
959
|
+
at += taken;
|
|
960
|
+
out += piece.charCodeAt(taken - 1) === 0x2f ? piece.slice(0, -1) : piece;
|
|
961
|
+
}
|
|
962
|
+
return out;
|
|
939
963
|
}
|
|
940
964
|
|
|
941
965
|
/**
|
|
@@ -1114,6 +1138,21 @@ module.exports = class Request extends LazyReadable {
|
|
|
1114
1138
|
this._lastUrl = newUrl;
|
|
1115
1139
|
}
|
|
1116
1140
|
|
|
1141
|
+
/**
|
|
1142
|
+
* Takes over what a middleware assigned to req.method. Both flags are read as bare fields by
|
|
1143
|
+
* the routing scan rather than compared against req.method, so they are what has to follow it,
|
|
1144
|
+
* and an OPTIONS the request has just become still needs the set the verbs are collected in.
|
|
1145
|
+
*/
|
|
1146
|
+
_absorbMethodRewrite() {
|
|
1147
|
+
const method = this.method;
|
|
1148
|
+
this._isOptions = method === "OPTIONS";
|
|
1149
|
+
this._isHead = method === "HEAD";
|
|
1150
|
+
if (this._isOptions && this._matchedMethods === null) {
|
|
1151
|
+
this._matchedMethods = new Set();
|
|
1152
|
+
}
|
|
1153
|
+
this._lastMethod = method;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1117
1156
|
/**
|
|
1118
1157
|
* The query string parsed by whichever parser the "query parser" setting names. A null-prototype
|
|
1119
1158
|
* object, so a key like "__proto__" cannot reach Object.prototype. No setter, so assigning to
|
package/src/router.js
CHANGED
|
@@ -216,6 +216,11 @@ class Walk {
|
|
|
216
216
|
if (req.url !== req._lastUrl && this.takeUrlRewrite(startIndex)) {
|
|
217
217
|
return;
|
|
218
218
|
}
|
|
219
|
+
// and the same for req.method, which method-override assigns: the compiled chain was
|
|
220
|
+
// picked by the verb the request arrived with, so it no longer stands for this one
|
|
221
|
+
if (req.method !== req._lastMethod && this.takeMethodRewrite(startIndex)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
219
224
|
let routeIndex = startIndex;
|
|
220
225
|
// a compiled chain runs what is in it without matching again, so this is where a layer that
|
|
221
226
|
// provably has nothing to do for this request is stepped over rather than entered
|
|
@@ -345,6 +350,34 @@ class Walk {
|
|
|
345
350
|
return true;
|
|
346
351
|
}
|
|
347
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Takes over a req.method a middleware assigned, which method-override is written to do. The
|
|
355
|
+
* ordinary scan reads req.method per route and is right from the next hop on; a compiled chain
|
|
356
|
+
* was chosen by the method µWS dispatched on, so ordinary routing takes over from the top the
|
|
357
|
+
* way a url rewrite does.
|
|
358
|
+
*
|
|
359
|
+
* @param {number} startIndex where dispatch was about to resume
|
|
360
|
+
* @returns {boolean} whether this rerouted the walk itself
|
|
361
|
+
*/
|
|
362
|
+
takeMethodRewrite(startIndex) {
|
|
363
|
+
const req = this.req;
|
|
364
|
+
const router = this.router;
|
|
365
|
+
req._absorbMethodRewrite();
|
|
366
|
+
if (!this.skipCheck) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
this.skipUntil = startIndex > 0 ? this.routes[startIndex - 1] : undefined;
|
|
370
|
+
if (req._stack !== null && req._stack.length > 0) {
|
|
371
|
+
req._stack.length = 0;
|
|
372
|
+
req._consumed = 0;
|
|
373
|
+
setMountedPath(req);
|
|
374
|
+
}
|
|
375
|
+
this.routes = router._routes;
|
|
376
|
+
this.skipCheck = false;
|
|
377
|
+
this.dispatch(0);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
|
|
348
381
|
/**
|
|
349
382
|
* Enters the route the walk is on: a mount adjusts req.url, req.path and the mount stack on the
|
|
350
383
|
* way in, and then the route's callbacks run one after another through next().
|
|
@@ -375,6 +408,11 @@ class Walk {
|
|
|
375
408
|
// one cannot bite. An application is mostly pathless middleware, and this is per hop
|
|
376
409
|
if (taken !== 0 || req.endsWithSlash) {
|
|
377
410
|
req._consumed += taken;
|
|
411
|
+
// a mount that took a trailing slash: req.baseUrl then has to join the pieces
|
|
412
|
+
// rather than slice the path, which is the slower half of its getter
|
|
413
|
+
if (taken !== 0 && req._originalPath.charCodeAt(req._consumed - 1) === 0x2f) {
|
|
414
|
+
req._mountSlash = true;
|
|
415
|
+
}
|
|
378
416
|
setMountedPath(req);
|
|
379
417
|
}
|
|
380
418
|
}
|
|
@@ -707,6 +745,12 @@ const PATH_PROPERTY = {
|
|
|
707
745
|
enumerable: true
|
|
708
746
|
};
|
|
709
747
|
|
|
748
|
+
// and the two the walk calls when a middleware rewrote req.url or req.method, for the same reason:
|
|
749
|
+
// an adopted request has no prototype of ours to find them on, and a rewrite through one of those
|
|
750
|
+
// routers threw instead of being taken over
|
|
751
|
+
const ABSORB_URL = Request.prototype._absorbUrlRewrite;
|
|
752
|
+
const ABSORB_METHOD = Request.prototype._absorbMethodRewrite;
|
|
753
|
+
|
|
710
754
|
const NO_PARAM_NAMES = [];
|
|
711
755
|
|
|
712
756
|
/**
|
|
@@ -854,6 +898,8 @@ function adoptPlainRequest(req, router) {
|
|
|
854
898
|
// an adopted request is a plain object, so it carries no prototype of ours and reads its path
|
|
855
899
|
// off a property of its own. The class's getter itself, so there is one of it
|
|
856
900
|
Object.defineProperty(req, "path", PATH_PROPERTY);
|
|
901
|
+
req._absorbUrlRewrite = ABSORB_URL;
|
|
902
|
+
req._absorbMethodRewrite = ABSORB_METHOD;
|
|
857
903
|
req.originalUrl = req.originalUrl ?? arrived;
|
|
858
904
|
req._originalPath = path;
|
|
859
905
|
req.endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
|
|
@@ -861,6 +907,7 @@ function adoptPlainRequest(req, router) {
|
|
|
861
907
|
req._opPathLower = null;
|
|
862
908
|
req._mayFailDecode = null;
|
|
863
909
|
req._lastUrl = req.url;
|
|
910
|
+
req._lastMethod = req.method;
|
|
864
911
|
req._isOptions = req.method === "OPTIONS";
|
|
865
912
|
req._isHead = req.method === "HEAD";
|
|
866
913
|
req.params = req.params ?? Object.create(null);
|
|
@@ -868,6 +915,7 @@ function adoptPlainRequest(req, router) {
|
|
|
868
915
|
// requests never see one, same as the Request constructor
|
|
869
916
|
req._stack = null;
|
|
870
917
|
req._consumed = 0;
|
|
918
|
+
req._mountSlash = false;
|
|
871
919
|
req._paramStack = null;
|
|
872
920
|
req._matchedMethods = req._isOptions ? new Set() : null;
|
|
873
921
|
req.routeCount = 1;
|