Skip to content

Commit

Permalink
fix: walking dag-cbor paths
Browse files Browse the repository at this point in the history
  • Loading branch information
SgtPooki committed Mar 30, 2024
1 parent 54b7454 commit 7acf56a
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 6 deletions.
13 changes: 13 additions & 0 deletions packages/verified-fetch/src/utils/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ export function notAcceptableResponse (url: string, body?: SupportedBodyTypes, i
return response
}

export function notFoundResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
const response = new Response(body, {
...(init ?? {}),
status: 404,
statusText: 'Not Found'
})

setType(response, 'basic')
setUrl(response, url)

return response
}

/**
* if body is an Error, it will be converted to a string containing the error message.
*/
Expand Down
9 changes: 7 additions & 2 deletions packages/verified-fetch/src/utils/walk-path.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { walkPath as exporterWalk, type ExporterOptions, type ReadableStorage, type UnixFSEntry } from 'ipfs-unixfs-exporter'
import { CodeError } from '@libp2p/interface'
import { walkPath as exporterWalk, type ExporterOptions, type ReadableStorage, type ObjectNode, type UnixFSEntry } from 'ipfs-unixfs-exporter'
import type { CID } from 'multiformats/cid'

export interface PathWalkerOptions extends ExporterOptions {
Expand All @@ -24,11 +25,15 @@ export async function walkPath (blockstore: ReadableStorage, path: string, optio
}

if (terminalElement == null) {
throw new Error('No terminal element found')
throw new CodeError('No terminal element found', 'NO_TERMINAL_ELEMENT')

Check warning on line 28 in packages/verified-fetch/src/utils/walk-path.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/walk-path.ts#L28

Added line #L28 was not covered by tests
}

return {
ipfsRoots,
terminalElement
}
}

export function objectNodeGuard (node: UnixFSEntry): node is ObjectNode {
return node.type === 'object'
}
41 changes: 37 additions & 4 deletions packages/verified-fetch/src/verified-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterab
import { tarStream } from './utils/get-tar-stream.js'
import { parseResource } from './utils/parse-resource.js'
import { setCacheControlHeader } from './utils/response-headers.js'
import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse } from './utils/responses.js'
import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse, notFoundResponse } from './utils/responses.js'
import { selectOutputType } from './utils/select-output-type.js'
import { walkPath } from './utils/walk-path.js'
import { objectNodeGuard, walkPath } from './utils/walk-path.js'
import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
import type { RequestFormatShorthand } from './types.js'
import type { ParsedUrlStringResults } from './utils/parse-url-string'
import type { Helia } from '@helia/interface'
import type { DNSResolver } from '@multiformats/dns/resolvers'
import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
import type { ObjectNode, UnixFSEntry } from 'ipfs-unixfs-exporter'
import type { CID } from 'multiformats/cid'

interface VerifiedFetchComponents {
Expand Down Expand Up @@ -236,8 +236,33 @@ export class VerifiedFetch {

private async handleDagCbor ({ resource, cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
this.log.trace('fetching %c/%s', cid, path)
let terminalElement: ObjectNode | undefined
let ipfsRoots: CID[] | undefined

// need to walk path, if it exists, to get the terminal element
try {
const pathDetails = await walkPath(this.helia.blockstore, `${cid.toString()}/${path}`, options)
ipfsRoots = pathDetails.ipfsRoots
const potentialTerminalElement = pathDetails.terminalElement
if (potentialTerminalElement == null) {
return notFoundResponse(resource.toString())

Check warning on line 248 in packages/verified-fetch/src/verified-fetch.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/verified-fetch.ts#L248

Added line #L248 was not covered by tests
}
if (objectNodeGuard(potentialTerminalElement)) {
terminalElement = potentialTerminalElement
}
} catch (err: any) {
if (options?.signal?.aborted === true) {
throw new AbortError('signal aborted by user')
}

Check warning on line 256 in packages/verified-fetch/src/verified-fetch.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/verified-fetch.ts#L255-L256

Added lines #L255 - L256 were not covered by tests
if (['ERR_NO_PROP', 'NO_TERMINAL_ELEMENT'].includes(err.code)) {
return notFoundResponse(resource.toString())
}

this.log.error('error walking path %s', path, err)
return badGatewayResponse(resource.toString(), 'Error walking path')

Check warning on line 262 in packages/verified-fetch/src/verified-fetch.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/verified-fetch.ts#L260-L262

Added lines #L260 - L262 were not covered by tests
}
const block = terminalElement?.node ?? await this.helia.blockstore.get(cid, options)

const block = await this.helia.blockstore.get(cid, options)
let body: string | Uint8Array

if (accept === 'application/octet-stream' || accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
Expand Down Expand Up @@ -277,9 +302,14 @@ export class VerifiedFetch {

response.headers.set('content-type', accept)

if (ipfsRoots != null) {
response.headers.set('X-Ipfs-Roots', ipfsRoots.map(cid => cid.toV1().toString()).join(',')) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-roots-response-header
}

return response
}

// eslint-disable-next-line complexity
private async handleDagPb ({ cid, path, resource, options }: FetchHandlerFunctionArg): Promise<Response> {
let terminalElement: UnixFSEntry | undefined
let ipfsRoots: CID[] | undefined
Expand All @@ -294,6 +324,9 @@ export class VerifiedFetch {
if (options?.signal?.aborted === true) {
throw new AbortError('signal aborted by user')
}
if (['ERR_NO_PROP', 'NO_TERMINAL_ELEMENT'].includes(err.code)) {
return notFoundResponse(resource.toString())

Check warning on line 328 in packages/verified-fetch/src/verified-fetch.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/verified-fetch.ts#L328

Added line #L328 was not covered by tests
}
this.log.error('error walking path %s', path, err)

return badGatewayResponse(resource.toString(), 'Error walking path')
Expand Down
31 changes: 31 additions & 0 deletions packages/verified-fetch/test/verified-fetch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,4 +769,35 @@ describe('@helia/verifed-fetch', () => {
expect(new Uint8Array(data)).to.equalBytes(finalRootFileContent)
})
})

describe('404 paths', () => {
let helia: Helia
let verifiedFetch: VerifiedFetch
let contentTypeParser: Sinon.SinonStub

beforeEach(async () => {
contentTypeParser = Sinon.stub()
helia = await createHelia()
verifiedFetch = new VerifiedFetch({
helia
}, {
contentTypeParser
})
})

afterEach(async () => {
await stop(helia, verifiedFetch)
})

it('returns a 404 when walking dag-cbor for non-existent path', async () => {
const obj = {
hello: 'world'
}
const c = dagCbor(helia)
const cid = await c.add(obj)

const resp = await verifiedFetch.fetch(`http://example.com/ipfs/${cid}/foo/i-do-not-exist`)
expect(resp.status).to.equal(404)
})
})
})

0 comments on commit 7acf56a

Please sign in to comment.