From d4f79d8fe8e1e9d64ffaf537b98ff065cc604345 Mon Sep 17 00:00:00 2001 From: ayame113 <40050810+ayame113@users.noreply.github.com> Date: Wed, 10 May 2023 15:15:03 +0900 Subject: [PATCH] fix(http/file_server): dealing with dir listing view that contain system files (#3371) --- http/file_server.ts | 43 +++++++++++++++++++++++++++------------- http/file_server_test.ts | 27 ++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/http/file_server.ts b/http/file_server.ts index be77ca51344b..c4d9325b923d 100644 --- a/http/file_server.ts +++ b/http/file_server.ts @@ -268,6 +268,7 @@ async function serveDirIndex( options: { showDotfiles: boolean; target: string; + quiet: boolean | undefined; }, ): Promise { const { showDotfiles } = options; @@ -294,13 +295,27 @@ async function serveDirIndex( const filePath = posix.join(dirPath, entry.name); const fileUrl = encodeURIComponent(posix.join(dirUrl, entry.name)) .replaceAll("%2F", "/"); - const entryInfo = Deno.stat(filePath).then((fileInfo): EntryInfo => ({ - mode: modeToString(entry.isDirectory, fileInfo.mode), - size: entry.isFile ? formatBytes(fileInfo.size ?? 0) : "", - name: `${entry.name}${entry.isDirectory ? "/" : ""}`, - url: `${fileUrl}${entry.isDirectory ? "/" : ""}`, - })); - listEntryPromise.push(entryInfo); + + listEntryPromise.push((async () => { + try { + const fileInfo = await Deno.stat(filePath); + return { + mode: modeToString(entry.isDirectory, fileInfo.mode), + size: entry.isFile ? formatBytes(fileInfo.size ?? 0) : "", + name: `${entry.name}${entry.isDirectory ? "/" : ""}`, + url: `${fileUrl}${entry.isDirectory ? "/" : ""}`, + }; + } catch (error) { + // Note: Deno.stat for windows system files may be rejected with os error 32. + if (!options.quiet) logError(error); + return { + mode: "(unknown mode)", + size: "", + name: `${entry.name}${entry.isDirectory ? "/" : ""}`, + url: `${fileUrl}${entry.isDirectory ? "/" : ""}`, + }; + } + })()); } const listEntry = await Promise.all(listEntryPromise); @@ -554,11 +569,7 @@ export async function serveDir(req: Request, opts: ServeDirOptions = {}) { try { response = await createServeDirResponse(req, opts); } catch (error) { - if (!opts.quiet) { - console.error( - red(error instanceof Error ? error.message : "[non-error thrown]"), - ); - } + if (!opts.quiet) logError(error); response = serveFallback(error); } @@ -595,7 +606,7 @@ async function createServeDirResponse( const urlRoot = opts.urlRoot; const showIndex = opts.showIndex ?? true; const showDotfiles = opts.showDotfiles || false; - const { etagAlgorithm, showDirListing } = opts; + const { etagAlgorithm, showDirListing, quiet } = opts; const url = new URL(req.url); const decodedUrl = decodeURIComponent(url.pathname); @@ -671,12 +682,16 @@ async function createServeDirResponse( } if (showDirListing) { // serve directory list - return serveDirIndex(fsPath, { showDotfiles, target }); + return serveDirIndex(fsPath, { showDotfiles, target, quiet }); } return createCommonResponse(Status.NotFound); } +function logError(error: unknown) { + console.error(red(error instanceof Error ? error.message : `${error}`)); +} + function main() { const serverArgs = parse(Deno.args, { string: ["port", "host", "cert", "key", "header"], diff --git a/http/file_server_test.ts b/http/file_server_test.ts index b64619011096..b719c8d4c833 100644 --- a/http/file_server_test.ts +++ b/http/file_server_test.ts @@ -4,6 +4,7 @@ import { assertEquals, assertStringIncludes, } from "../testing/asserts.ts"; +import { stub } from "../testing/mock.ts"; import { iterateReader } from "../streams/iterate_reader.ts"; import { writeAll } from "../streams/write_all.ts"; import { TextLineStream } from "../streams/text_line_stream.ts"; @@ -57,7 +58,7 @@ async function startFileServer({ ], cwd: moduleDir, stdout: "piped", - stderr: "null", + stderr: "inherit", }); child = fileServer.spawn(); // Once fileServer is ready it will write to its stdout. @@ -283,6 +284,30 @@ Deno.test("serveDirIndex with filename including hash symbol", async function () } }); +Deno.test("serveDirIndex returns a response even if fileinfo is inaccessible", async function () { + // Note: Deno.stat for windows system files may be rejected with os error 32. + // Mock Deno.stat to test that the dirlisting page can be generated + // even if the fileInfo for a particular file cannot be obtained. + + // Assuming that fileInfo of `test file.txt` cannot be accessible + const denoStatStub = stub(Deno, "stat", (path): Promise => { + if (path.toString().includes("test file.txt")) { + return Promise.reject(new Error("__stubed_error__")); + } + return denoStatStub.original.call(Deno, path); + }); + + try { + const url = "http://localhost:4507/http/testdata/"; + const res = await serveDir(new Request(url), { showDirListing: true }); + + assertEquals(res.status, 200); + assertStringIncludes(await res.text(), "/testdata/test%20file.txt"); + } finally { + denoStatStub.restore(); + } +}); + Deno.test("serveFallback", async function () { await startFileServer(); try {