Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow fetching file urls when experimental permissions are enabled #3775

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Expand All @@ -796,15 +798,15 @@ 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())

// 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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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)
mcollina marked this conversation as resolved.
Show resolved Hide resolved

return makeResponse({
body: safelyExtractBody(blob)[0]
})
} catch (e) {
return makeNetworkError(e)
}
}
case 'http:':
case 'https:': {
Expand All @@ -952,7 +968,7 @@ function schemeFetch (fetchParams) {
.catch((err) => makeNetworkError(err))
}
default: {
return Promise.resolve(makeNetworkError('unknown scheme'))
return makeNetworkError('unknown scheme')
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\"",
Expand Down
19 changes: 19 additions & 0 deletions test/fetch/file-url/fetch-file-url.js
Original file line number Diff line number Diff line change
@@ -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'))
})
Loading