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

feat: implement new ipns record&answer properties #23

Merged
merged 9 commits into from
Mar 18, 2024
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
5 changes: 3 additions & 2 deletions packages/verified-fetch/src/utils/parse-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ export async function parseResource (resource: Resource, { ipns, logger }: Parse
cid,
protocol: 'ipfs',
path: '',
query: {}
}
query: {},
ttl: 29030400 // 1 year for ipfs content
} satisfies ParsedUrlStringResults
}

throw new TypeError(`Invalid resource. Cannot determine CID from resource: ${resource}`)
Expand Down
Loading
Loading