Skip to content

Commit 692a9be

Browse files
fix: update logic for URLs ending with '/'
1 parent bee8c81 commit 692a9be

1 file changed

Lines changed: 118 additions & 56 deletions

File tree

src/middleware.js

Lines changed: 118 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,11 @@ const memoizedParse = memorize((url) => {
5858

5959
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
6060

61+
/** @typedef {import("fs").Stats} FSStats */
62+
6163
/**
6264
* @typedef {object} Extra
63-
* @property {import("fs").Stats} stats stats
65+
* @property {FSStats} stats stats
6466
* @property {boolean=} immutable true when immutable, otherwise false
6567
* @property {OutputFileSystem} outputFileSystem outputFileSystem
6668
*/
@@ -81,13 +83,27 @@ class FilenameError extends Error {
8183
constructor(message, code) {
8284
super(message);
8385
this.name = "FilenameError";
84-
this.code = code;
86+
this.statusCode = code;
8587
}
8688
}
8789

8890
/** @typedef {{ filename: string, extra: Extra }} FilenameWithExtra */
8991

90-
// TODO fix redirect logic when `/` at the end, like https://github.com/pillarjs/send/blob/master/index.js#L586
92+
/**
93+
* @param {unknown} error error
94+
* @returns {boolean} true when error is like not found, otherwise false
95+
*/
96+
function isNotFoundError(error) {
97+
switch (/** @type {NodeJS.ErrnoException} */ (error).code) {
98+
case "ENAMETOOLONG":
99+
case "ENOENT":
100+
case "ENOTDIR":
101+
return true;
102+
default:
103+
return false;
104+
}
105+
}
106+
91107
/**
92108
* @template {IncomingMessage} Request
93109
* @template {ServerResponse} Response
@@ -99,9 +115,6 @@ function getFilenameFromUrl(context, url) {
99115
/** @type {URL} */
100116
let urlObject;
101117

102-
/** @type {string | undefined} */
103-
let foundFilename;
104-
105118
try {
106119
// The `url` property of the `request` is contains only `pathname`, `search` and `hash`
107120
urlObject = memoizedParse(url);
@@ -110,14 +123,19 @@ function getFilenameFromUrl(context, url) {
110123
}
111124

112125
const { options, stats } = context;
113-
/** @type {Extra} */
114-
const extra = {};
115126

116127
/** @type {Stats[]} */
117128
const allStats =
118129
/** @type {MultiStats} */
119130
(stats).stats || [/** @type {Stats} */ (stats)];
120131

132+
const index =
133+
options.index === false
134+
? /** @type {string[]} */ ([])
135+
: typeof options.index === "undefined" || options.index === true
136+
? ["index.html"]
137+
: [options.index];
138+
121139
for (const { compilation } of allStats) {
122140
if (compilation.options.devServer === false) {
123141
continue;
@@ -158,6 +176,7 @@ function getFilenameFromUrl(context, url) {
158176
throw new FilenameError("Forbidden", 403);
159177
}
160178

179+
// send file logic
161180
// The `output.path` is always present and always absolute
162181
const outputPath = compilation.getPath(
163182
compilation.outputOptions.path || "",
@@ -172,61 +191,100 @@ function getFilenameFromUrl(context, url) {
172191
pathname.slice(publicPathPathname.length),
173192
);
174193

194+
const { assetsInfo } = compilation;
175195
const { outputFileSystem } =
176196
/** @type {Compiler & { outputFileSystem: OutputFileSystem }} */
177197
(compilation.compiler);
178198

179-
try {
180-
extra.stats = outputFileSystem.statSync(filename);
181-
} catch {
182-
continue;
183-
}
184-
185-
const { assetsInfo } = compilation;
199+
/**
200+
* @param {string} filename filename
201+
* @returns {FilenameWithExtra | undefined} filename when found, otherwise undefined
202+
*/
203+
const resolveIndex = (filename) => {
204+
filename = path.join(filename, index[0]);
186205

187-
if (extra.stats.isFile()) {
188-
foundFilename = filename;
206+
let stats;
189207

190-
// Rspack does not yet support `assetsInfo`, so we need to check if `assetsInfo` exists here
191-
extra.immutable = assetsInfo
192-
? assetsInfo.get(pathname.slice(publicPathPathname.length))?.immutable
193-
: false;
194-
extra.outputFileSystem = outputFileSystem;
208+
try {
209+
stats = outputFileSystem.statSync(filename);
210+
} catch (err) {
211+
if (isNotFoundError(err)) return;
212+
throw err;
213+
}
195214

196-
break;
197-
} else if (
198-
extra.stats.isDirectory() &&
199-
(typeof options.index === "undefined" || options.index)
200-
) {
201-
const indexValue =
202-
typeof options.index === "undefined" ||
203-
typeof options.index === "boolean"
204-
? "index.html"
205-
: options.index;
215+
if (/** @type {FSStats} */ (stats).isDirectory()) {
216+
return resolveIndex(filename);
217+
}
206218

207-
filename = path.join(filename, indexValue);
219+
const extra = {
220+
immutable: assetsInfo
221+
? assetsInfo.get(pathname.slice(publicPathPathname.length))
222+
?.immutable
223+
: false,
224+
outputFileSystem,
225+
stats: /** @type {FSStats} */ (stats),
226+
};
227+
228+
return { filename, extra };
229+
};
230+
231+
/**
232+
* @param {string} filename filename
233+
* @returns {FilenameWithExtra | undefined} filename when found, otherwise undefined
234+
*/
235+
const resolveFile = (filename) => {
236+
let stats;
208237

209238
try {
210-
extra.stats = outputFileSystem.statSync(filename);
211-
} catch {
212-
continue;
239+
stats = outputFileSystem.statSync(filename);
240+
} catch (err) {
241+
if (isNotFoundError(err)) return;
242+
throw err;
213243
}
214244

215-
if (extra.stats.isFile()) {
216-
foundFilename = filename;
217-
extra.outputFileSystem = outputFileSystem;
245+
if (/** @type {FSStats} */ (stats).isDirectory()) {
246+
// Different between `send` and our logic is here, `send` makes a redirect, we just return a file.
247+
return resolveIndex(filename);
248+
}
218249

219-
break;
250+
if (filename.endsWith(path.sep)) {
251+
return;
220252
}
253+
254+
/** @type {Extra} */
255+
const extra = {
256+
immutable: assetsInfo
257+
? assetsInfo.get(pathname.slice(publicPathPathname.length))
258+
?.immutable
259+
: false,
260+
outputFileSystem,
261+
stats: /** @type {FSStats} */ (stats),
262+
};
263+
264+
return { filename, extra };
265+
};
266+
267+
// send index logic
268+
if (index.length > 0 && pathname.endsWith("/")) {
269+
const result = resolveIndex(filename);
270+
271+
if (!result) {
272+
continue;
273+
}
274+
275+
return result;
221276
}
222-
}
223-
}
224277

225-
if (!foundFilename) {
226-
return;
227-
}
278+
// send file logic
279+
const result = resolveFile(filename);
280+
281+
if (!result) {
282+
continue;
283+
}
228284

229-
return { filename: foundFilename, extra };
285+
return result;
286+
}
287+
}
230288
}
231289

232290
/**
@@ -448,9 +506,11 @@ function wrapper(context) {
448506

449507
/**
450508
* @param {NodeJS.ErrnoException} error error
509+
* @param {string=} message override message
510+
* @param {number=} code override code
451511
* @returns {Promise<void>}
452512
*/
453-
async function errorHandler(error) {
513+
async function errorHandler(error, message, code) {
454514
switch (error.code) {
455515
case "ENAMETOOLONG":
456516
case "ENOENT":
@@ -460,7 +520,7 @@ function wrapper(context) {
460520
});
461521
break;
462522
default:
463-
await sendError(error.message, 500, {
523+
await sendError(message || error.message, code || 500, {
464524
modifyResponseData: context.options.modifyResponseData,
465525
});
466526
break;
@@ -713,20 +773,22 @@ function wrapper(context) {
713773
const errorCode =
714774
typeof err === "object" &&
715775
err !== null &&
716-
typeof (/** @type {FilenameError} */ (err).code) !== "undefined"
717-
? /** @type {FilenameError} */ (err).code
718-
: 403;
776+
typeof (/** @type {FilenameError} */ (err).statusCode) !== "undefined"
777+
? /** @type {FilenameError} */ (err).statusCode
778+
: undefined;
719779

720780
if (errorCode === 403) {
721781
context.logger.error(`Malicious path "${requestUrl}".`);
722782
}
723783

724-
await sendError(
725-
errorCode === 400 ? "Bad Request" : "Forbidden",
784+
await errorHandler(
785+
/** @type {NodeJS.ErrnoException} */ (err),
786+
errorCode === 400
787+
? "Bad Request"
788+
: errorCode === 403
789+
? "Forbidden"
790+
: undefined,
726791
errorCode,
727-
{
728-
modifyResponseData: context.options.modifyResponseData,
729-
},
730792
);
731793
return;
732794
}

0 commit comments

Comments
 (0)