From a47c6dd1d261030bdab3b6c6d4b470ce4a49d4f4 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 27 Oct 2024 13:52:52 -0400 Subject: [PATCH] allow fetching file urls when experimental permissions are enabled --- lib/web/fetch/index.js | 42 ++++++++++++++++++--------- package.json | 3 +- test/fetch/file-url/fetch-file-url.js | 19 ++++++++++++ 3 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 test/fetch/file-url/fetch-file-url.js diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index 00d34071f7f..c6ece56adfa 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -63,6 +63,8 @@ const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = requ const { getGlobalDispatcher } = require('../../global') const { webidl } = require('./webidl') const { STATUS_CODES } = require('node:http') +const { openAsBlob } = require('node:fs') + const GET_OR_HEAD = ['GET', 'HEAD'] const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined' @@ -761,13 +763,13 @@ async function mainFetch (fetchParams, recursive = false) { // https://fetch.spec.whatwg.org/#concept-scheme-fetch // given a fetch params fetchParams -function schemeFetch (fetchParams) { +async function schemeFetch (fetchParams) { // Note: since the connection is destroyed on redirect, which sets fetchParams to a // cancelled state, we do not want this condition to trigger *unless* there have been // no redirects. See https://github.com/nodejs/undici/issues/1776 // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) { - return Promise.resolve(makeAppropriateNetworkError(fetchParams)) + return makeAppropriateNetworkError(fetchParams) } // 2. Let request be fetchParams’s request. @@ -783,7 +785,7 @@ function schemeFetch (fetchParams) { // and body is the empty byte sequence as a body. // Otherwise, return a network error. - return Promise.resolve(makeNetworkError('about scheme is not supported')) + return makeNetworkError('about scheme is not supported') } case 'blob:': { if (!resolveObjectURL) { @@ -796,7 +798,7 @@ function schemeFetch (fetchParams) { // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56 // Buffer.resolveObjectURL does not ignore URL queries. if (blobURLEntry.search.length !== 0) { - return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.')) + return makeNetworkError('NetworkError when attempting to fetch resource.') } const blob = resolveObjectURL(blobURLEntry.toString()) @@ -804,7 +806,7 @@ function schemeFetch (fetchParams) { // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s // object is not a Blob object, then return a network error. if (request.method !== 'GET' || !webidl.is.Blob(blob)) { - return Promise.resolve(makeNetworkError('invalid method')) + return makeNetworkError('invalid method') } // 3. Let blob be blobURLEntry’s object. @@ -852,7 +854,7 @@ function schemeFetch (fetchParams) { // 4. If rangeValue is failure, then return a network error. if (rangeValue === 'failure') { - return Promise.resolve(makeNetworkError('failed to fetch the data URL')) + return makeNetworkError('failed to fetch the data URL') } // 5. Let (rangeStart, rangeEnd) be rangeValue. @@ -869,7 +871,7 @@ function schemeFetch (fetchParams) { } else { // 1. If rangeStart is greater than or equal to fullLength, then return a network error. if (rangeStart >= fullLength) { - return Promise.resolve(makeNetworkError('Range start is greater than the blob\'s size.')) + return makeNetworkError('Range start is greater than the blob\'s size.') } // 2. If rangeEnd is null or rangeEnd is greater than or equal to fullLength, then set @@ -911,7 +913,7 @@ function schemeFetch (fetchParams) { } // 10. Return response. - return Promise.resolve(response) + return response } case 'data:': { // 1. Let dataURLStruct be the result of running the @@ -922,7 +924,7 @@ function schemeFetch (fetchParams) { // 2. If dataURLStruct is failure, then return a // network error. if (dataURLStruct === 'failure') { - return Promise.resolve(makeNetworkError('failed to fetch the data URL')) + return makeNetworkError('failed to fetch the data URL') } // 3. Let mimeType be dataURLStruct’s MIME type, serialized. @@ -931,18 +933,32 @@ function schemeFetch (fetchParams) { // 4. Return a response whose status message is `OK`, // header list is « (`Content-Type`, mimeType) », // and body is dataURLStruct’s body as a body. - return Promise.resolve(makeResponse({ + return makeResponse({ statusText: 'OK', headersList: [ ['content-type', { name: 'Content-Type', value: mimeType }] ], body: safelyExtractBody(dataURLStruct.body)[0] - })) + }) } case 'file:': { // For now, unfortunate as it is, file URLs are left as an exercise for the reader. // When in doubt, return a network error. - return Promise.resolve(makeNetworkError('not implemented... yet...')) + const fileURL = requestCurrentURL(request) + + if (!process.permission?.has('fs.read', fileURL.href)) { + return makeNetworkError(`Access to ${fileURL.href} is not permitted.`) + } + + try { + const blob = await openAsBlob(fileURL) + + return makeResponse({ + body: safelyExtractBody(blob)[0] + }) + } catch (e) { + return makeNetworkError(e) + } } case 'http:': case 'https:': { @@ -952,7 +968,7 @@ function schemeFetch (fetchParams) { .catch((err) => makeNetworkError(err)) } default: { - return Promise.resolve(makeNetworkError('unknown scheme')) + return makeNetworkError('unknown scheme') } } } diff --git a/package.json b/package.json index fb7b4181fa9..6c6c7f50136 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ "test:cookies": "borp -p \"test/cookie/*.js\"", "test:eventsource": "npm run build:node && borp --expose-gc -p \"test/eventsource/*.js\"", "test:fuzzing": "node test/fuzzing/fuzzing.test.js", - "test:fetch": "npm run build:node && borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy", + "test:fetch": "npm run build:node && borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy && npm run test:fetch-file-url", + "test:fetch-file-url": "node scripts/verifyVersion.js 20 || node --experimental-permission --allow-fs-read=. test/fetch/file-url/fetch-file-url.js", "test:h2": "npm run test:h2:core && npm run test:h2:fetch", "test:h2:core": "borp -p \"test/http2*.js\"", "test:h2:fetch": "npm run build:node && borp -p \"test/fetch/http2*.js\"", diff --git a/test/fetch/file-url/fetch-file-url.js b/test/fetch/file-url/fetch-file-url.js new file mode 100644 index 00000000000..3c9eb505ba0 --- /dev/null +++ b/test/fetch/file-url/fetch-file-url.js @@ -0,0 +1,19 @@ +'use strict' + +const { fetch } = require('../../..') +const { test } = require('node:test') +const { pathToFileURL } = require('node:url') +const { join } = require('node:path') +const assert = require('node:assert') + +test('fetching a file url works', async () => { + const url = new URL(join(pathToFileURL(__dirname).toString(), 'fetch-file-url.js')) + + await assert.doesNotReject(fetch(url)) +}) + +test('fetching one outside of the permission scope rejects', async (t) => { + const url = new URL(join(pathToFileURL(process.cwd()).toString(), '..')) + + await assert.rejects(fetch(url), new TypeError('fetch failed')) +})