Skip to content

Commit

Permalink
Merge branch 'main' into fix/cache-header-patch
Browse files Browse the repository at this point in the history
  • Loading branch information
SgtPooki committed Mar 15, 2024
2 parents d2cbc3c + 8bf9c9f commit dbd8863
Show file tree
Hide file tree
Showing 17 changed files with 963 additions and 35 deletions.
8 changes: 8 additions & 0 deletions packages/interop/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## @helia/verified-fetch-interop [1.7.0](https://github.com/ipfs/helia-verified-fetch/compare/@helia/verified-fetch-interop-1.6.0...@helia/verified-fetch-interop-1.7.0) (2024-03-15)



### Dependencies

* **@helia/verified-fetch:** upgraded to 1.2.0

## @helia/verified-fetch-interop [1.6.0](https://github.com/ipfs/helia-verified-fetch/compare/@helia/verified-fetch-interop-1.5.1...@helia/verified-fetch-interop-1.6.0) (2024-03-14)


Expand Down
4 changes: 2 additions & 2 deletions packages/interop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@helia/verified-fetch-interop",
"version": "1.6.0",
"version": "1.7.0",
"description": "Interop tests for @helia/verified-fetch",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/ipfs/helia-verified-fetch/tree/main/packages/interop#readme",
Expand Down Expand Up @@ -57,7 +57,7 @@
"test:electron-main": "aegir test -t electron-main"
},
"dependencies": {
"@helia/verified-fetch": "1.1.3",
"@helia/verified-fetch": "1.2.0",
"aegir": "^42.2.5",
"ipfsd-ctl": "^13.0.0",
"it-drain": "^3.0.5",
Expand Down
12 changes: 12 additions & 0 deletions packages/verified-fetch/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## @helia/verified-fetch [1.2.0](https://github.com/ipfs/helia-verified-fetch/compare/@helia/verified-fetch-1.1.3...@helia/verified-fetch-1.2.0) (2024-03-15)


### Features

* support http range header ([#10](https://github.com/ipfs/helia-verified-fetch/issues/10)) ([9f5078a](https://github.com/ipfs/helia-verified-fetch/commit/9f5078a09846ba6569d637ea1dd90a6d8fb4e629))


### Trivial Changes

* fix build ([#22](https://github.com/ipfs/helia-verified-fetch/issues/22)) ([01261fe](https://github.com/ipfs/helia-verified-fetch/commit/01261feabd4397c10446609b072a7cb97fb81911))

## @helia/verified-fetch [1.1.3](https://github.com/ipfs/helia-verified-fetch/compare/@helia/verified-fetch-1.1.2...@helia/verified-fetch-1.1.3) (2024-03-14)


Expand Down
2 changes: 1 addition & 1 deletion packages/verified-fetch/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@helia/verified-fetch",
"version": "1.1.3",
"version": "1.2.0",
"description": "A fetch-like API for obtaining verified & trustless IPFS content on the web",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/ipfs/helia-verified-fetch/tree/main/packages/verified-fetch#readme",
Expand Down
2 changes: 2 additions & 0 deletions packages/verified-fetch/src/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export type RequestFormatShorthand = 'raw' | 'car' | 'tar' | 'ipns-record' | 'dag-json' | 'dag-cbor' | 'json' | 'cbor'

export type SupportedBodyTypes = string | ArrayBuffer | Blob | ReadableStream<Uint8Array> | null
303 changes: 303 additions & 0 deletions packages/verified-fetch/src/utils/byte-range-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
import { calculateByteRangeIndexes, getHeader } from './request-headers.js'
import { getContentRangeHeader } from './response-headers.js'
import type { SupportedBodyTypes } from '../types.js'
import type { ComponentLogger, Logger } from '@libp2p/interface'

type SliceableBody = Exclude<SupportedBodyTypes, ReadableStream<Uint8Array> | null>

/**
* Gets the body size of a given body if it's possible to calculate it synchronously.
*/
function getBodySizeSync (body: SupportedBodyTypes): number | null {
if (typeof body === 'string') {
return body.length
}

Check warning on line 14 in packages/verified-fetch/src/utils/byte-range-context.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/byte-range-context.ts#L13-L14

Added lines #L13 - L14 were not covered by tests
if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
return body.byteLength
}
if (body instanceof Blob) {
return body.size
}

if (body instanceof ReadableStream) {
return null
}

return null
}

Check warning on line 27 in packages/verified-fetch/src/utils/byte-range-context.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/byte-range-context.ts#L25-L27

Added lines #L25 - L27 were not covered by tests

function getByteRangeFromHeader (rangeHeader: string): { start: string, end: string } {
/**
* Range: bytes=<start>-<end> | bytes=<start2>- | bytes=-<end2>
*/
const match = rangeHeader.match(/^bytes=(?<start>\d+)?-(?<end>\d+)?$/)
if (match?.groups == null) {
throw new Error('Invalid range request')
}

const { start, end } = match.groups

return { start, end }
}

export class ByteRangeContext {
public readonly isRangeRequest: boolean

/**
* This property is purposefully only set in `set fileSize` and should not be set directly.
*/
private _fileSize: number | null | undefined
private _body: SupportedBodyTypes = null
private readonly rangeRequestHeader: string | undefined
private readonly log: Logger
private readonly requestRangeStart: number | null
private readonly requestRangeEnd: number | null
private byteStart: number | undefined
private byteEnd: number | undefined
private byteSize: number | undefined

constructor (logger: ComponentLogger, private readonly headers?: HeadersInit) {
this.log = logger.forComponent('helia:verified-fetch:byte-range-context')
this.rangeRequestHeader = getHeader(this.headers, 'Range')
if (this.rangeRequestHeader != null) {
this.isRangeRequest = true
this.log.trace('range request detected')
try {
const { start, end } = getByteRangeFromHeader(this.rangeRequestHeader)
this.requestRangeStart = start != null ? parseInt(start) : null
this.requestRangeEnd = end != null ? parseInt(end) : null
} catch (e) {
this.log.error('error parsing range request header: %o', e)
this.requestRangeStart = null
this.requestRangeEnd = null
}

this.setOffsetDetails()
} else {
this.log.trace('no range request detected')
this.isRangeRequest = false
this.requestRangeStart = null
this.requestRangeEnd = null
}
}

public setBody (body: SupportedBodyTypes): void {
this._body = body
// if fileSize was already set, don't recalculate it
this.setFileSize(this._fileSize ?? getBodySizeSync(body))

this.log.trace('set request body with fileSize %o', this._fileSize)
}

public getBody (): SupportedBodyTypes {
const body = this._body
if (body == null) {
this.log.trace('body is null')
return body
}

Check warning on line 97 in packages/verified-fetch/src/utils/byte-range-context.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/byte-range-context.ts#L95-L97

Added lines #L95 - L97 were not covered by tests
if (!this.isRangeRequest || !this.isValidRangeRequest) {
this.log.trace('returning body unmodified for non-range, or invalid range, request')
return body
}
const byteStart = this.byteStart
const byteEnd = this.byteEnd
const byteSize = this.byteSize
if (byteStart != null || byteEnd != null) {
this.log.trace('returning body with byteStart=%o, byteEnd=%o, byteSize=%o', byteStart, byteEnd, byteSize)
if (body instanceof ReadableStream) {
// stream should already be spliced by `unixfs.cat`
return body
}
return this.getSlicedBody(body)
}

// we should not reach this point, but return body untouched.
this.log.error('returning unmodified body for valid range request')
return body
}

Check warning on line 117 in packages/verified-fetch/src/utils/byte-range-context.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/byte-range-context.ts#L113-L117

Added lines #L113 - L117 were not covered by tests

private getSlicedBody <T extends SliceableBody>(body: T): SliceableBody {
if (this.isPrefixLengthRequest) {
this.log.trace('sliced body with byteStart %o', this.byteStart)
return body.slice(this.offset) satisfies SliceableBody
}
if (this.isSuffixLengthRequest && this.length != null) {
this.log.trace('sliced body with length %o', -this.length)
return body.slice(-this.length) satisfies SliceableBody
}
const offset = this.byteStart ?? 0
const length = this.byteEnd == null ? undefined : this.byteEnd + 1
this.log.trace('returning body with offset %o and length %o', offset, length)

return body.slice(offset, length) satisfies SliceableBody
}

private get isSuffixLengthRequest (): boolean {
return this.requestRangeStart == null && this.requestRangeEnd != null
}

private get isPrefixLengthRequest (): boolean {
return this.requestRangeStart != null && this.requestRangeEnd == null
}

/**
* Sometimes, we need to set the fileSize explicitly because we can't calculate
* the size of the body (e.g. for unixfs content where we call .stat).
*
* This fileSize should otherwise only be called from `setBody`.
*/
public setFileSize (size: number | bigint | null): void {
this._fileSize = size != null ? Number(size) : null
this.log.trace('set _fileSize to %o', this._fileSize)
// when fileSize changes, we need to recalculate the offset details
this.setOffsetDetails()
}

public getFileSize (): number | null | undefined {
return this._fileSize
}

private isValidByteStart (): boolean {
if (this.byteStart != null) {
if (this.byteStart < 0) {
return false
}
if (this._fileSize != null && this.byteStart > this._fileSize) {
return false
}
}
return true
}

private isValidByteEnd (): boolean {
if (this.byteEnd != null) {
if (this.byteEnd < 0) {
return false
}

Check warning on line 176 in packages/verified-fetch/src/utils/byte-range-context.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/byte-range-context.ts#L175-L176

Added lines #L175 - L176 were not covered by tests
if (this._fileSize != null && this.byteEnd > this._fileSize) {
return false
}
}
return true
}

/**
* We may get the values required to determine if this is a valid range request at different times
* so we need to calculate it when asked.
*/
public get isValidRangeRequest (): boolean {
if (!this.isRangeRequest) {
return false
}

Check warning on line 191 in packages/verified-fetch/src/utils/byte-range-context.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/byte-range-context.ts#L190-L191

Added lines #L190 - L191 were not covered by tests
if (this.requestRangeStart == null && this.requestRangeEnd == null) {
this.log.trace('invalid range request, range request values not provided')
return false
}
if (!this.isValidByteStart()) {
this.log.trace('invalid range request, byteStart is less than 0 or greater than fileSize')
return false
}
if (!this.isValidByteEnd()) {
this.log.trace('invalid range request, byteEnd is less than 0 or greater than fileSize')
return false
}
if (this.requestRangeEnd != null && this.requestRangeStart != null) {
// we may not have enough info.. base check on requested bytes
if (this.requestRangeStart > this.requestRangeEnd) {
this.log.trace('invalid range request, start is greater than end')
return false

Check warning on line 208 in packages/verified-fetch/src/utils/byte-range-context.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/byte-range-context.ts#L207-L208

Added lines #L207 - L208 were not covered by tests
} else if (this.requestRangeStart < 0) {
this.log.trace('invalid range request, start is less than 0')
return false

Check warning on line 211 in packages/verified-fetch/src/utils/byte-range-context.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/byte-range-context.ts#L210-L211

Added lines #L210 - L211 were not covered by tests
} else if (this.requestRangeEnd < 0) {
this.log.trace('invalid range request, end is less than 0')
return false
}

Check warning on line 215 in packages/verified-fetch/src/utils/byte-range-context.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/byte-range-context.ts#L213-L215

Added lines #L213 - L215 were not covered by tests
}

return true
}

/**
* Given all the information we have, this function returns the offset that will be used when:
* 1. calling unixfs.cat
* 2. slicing the body
*/
public get offset (): number {
if (this.byteStart === 0) {
return 0
}
if (this.isPrefixLengthRequest || this.isSuffixLengthRequest) {
if (this.byteStart != null) {
// we have to subtract by 1 because the offset is inclusive
return this.byteStart - 1
}
}

return this.byteStart ?? 0
}

/**
* Given all the information we have, this function returns the length that will be used when:
* 1. calling unixfs.cat
* 2. slicing the body
*/
public get length (): number | undefined {
return this.byteSize ?? undefined
}

/**
* Converts a range request header into helia/unixfs supported range options
* Note that the gateway specification says we "MAY" support multiple ranges (https://specs.ipfs.tech/http-gateways/path-gateway/#range-request-header) but we don't
*
* Also note that @helia/unixfs and ipfs-unixfs-exporter expect length and offset to be numbers, the range header is a string, and the size of the resource is likely a bigint.
*
* SUPPORTED:
* Range: bytes=<range-start>-<range-end>
* Range: bytes=<range-start>-
* Range: bytes=-<suffix-length> // must pass size so we can calculate the offset. suffix-length is the number of bytes from the end of the file.
*
* NOT SUPPORTED:
* Range: bytes=<range-start>-<range-end>, <range-start>-<range-end>
* Range: bytes=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#directives
*/
private setOffsetDetails (): void {
if (this.requestRangeStart == null && this.requestRangeEnd == null) {
this.log.trace('requestRangeStart and requestRangeEnd are null')
return
}

const { start, end, byteSize } = calculateByteRangeIndexes(this.requestRangeStart ?? undefined, this.requestRangeEnd ?? undefined, this._fileSize ?? undefined)
this.log.trace('set byteStart to %o, byteEnd to %o, byteSize to %o', start, end, byteSize)
this.byteStart = start
this.byteEnd = end
this.byteSize = byteSize
}

/**
* This function returns the value of the "content-range" header.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
*
* Returns a string representing the following content ranges:
*
* @example
* - Content-Range: <unit> <byteStart>-<byteEnd>/<byteSize>
* - Content-Range: <unit> <byteStart>-<byteEnd>/*
*/
// - Content-Range: <unit> */<byteSize> // this is purposefully not in jsdoc block
public get contentRangeHeaderValue (): string {
if (!this.isValidRangeRequest) {
this.log.error('cannot get contentRangeHeaderValue for invalid range request')
throw new Error('Invalid range request')
}

Check warning on line 295 in packages/verified-fetch/src/utils/byte-range-context.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/byte-range-context.ts#L293-L295

Added lines #L293 - L295 were not covered by tests

return getContentRangeHeader({
byteStart: this.byteStart,
byteEnd: this.byteEnd,
byteSize: this._fileSize ?? undefined
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8A
const { value: firstChunk, done } = await reader.next()

if (done === true) {
log.error('No content found for path', path)
log.error('no content found for path', path)
throw new Error('No content found')
}

Expand Down
6 changes: 3 additions & 3 deletions packages/verified-fetch/src/utils/parse-url-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,10 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin
log.trace('resolved %s to %c', cidOrPeerIdOrDnsLink, cid)
} catch (err) {
if (peerId == null) {
log.error('Could not parse PeerId string "%s"', cidOrPeerIdOrDnsLink, err)
log.error('could not parse PeerId string "%s"', cidOrPeerIdOrDnsLink, err)
errors.push(new TypeError(`Could not parse PeerId in ipns url "${cidOrPeerIdOrDnsLink}", ${(err as Error).message}`))
} else {
log.error('Could not resolve PeerId %c', peerId, err)
log.error('could not resolve PeerId %c', peerId, err)
errors.push(new TypeError(`Could not resolve PeerId "${cidOrPeerIdOrDnsLink}", ${(err as Error).message}`))
}
}
Expand All @@ -175,7 +175,7 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin
resolvedPath = resolveResult?.path
log.trace('resolved %s to %c', decodedDnsLinkLabel, cid)
} catch (err: any) {
log.error('Could not resolve DnsLink for "%s"', cidOrPeerIdOrDnsLink, err)
log.error('could not resolve DnsLink for "%s"', cidOrPeerIdOrDnsLink, err)
errors.push(err)
}
}
Expand Down
Loading

0 comments on commit dbd8863

Please sign in to comment.