Skip to content

Commit

Permalink
allow fetching file urls when experimental permissions are enabled
Browse files Browse the repository at this point in the history
  • Loading branch information
KhafraDev committed Oct 27, 2024
1 parent 2de0f34 commit a47c6dd
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 14 deletions.
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)

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'))
})

0 comments on commit a47c6dd

Please sign in to comment.