@helia/verified-fetch 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -1
- package/dist/index.min.js +8 -19
- package/dist/src/index.d.ts +29 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +41 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.d.ts +1 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/byte-range-context.d.ts +82 -0
- package/dist/src/utils/byte-range-context.d.ts.map +1 -0
- package/dist/src/utils/byte-range-context.js +275 -0
- package/dist/src/utils/byte-range-context.js.map +1 -0
- package/dist/src/utils/get-stream-from-async-iterable.js +1 -1
- package/dist/src/utils/parse-resource.d.ts +2 -2
- package/dist/src/utils/parse-url-string.d.ts +2 -2
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +5 -5
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/request-headers.d.ts +13 -0
- package/dist/src/utils/request-headers.d.ts.map +1 -0
- package/dist/src/utils/request-headers.js +50 -0
- package/dist/src/utils/request-headers.js.map +1 -0
- package/dist/src/utils/response-headers.d.ts +12 -0
- package/dist/src/utils/response-headers.d.ts.map +1 -0
- package/dist/src/utils/response-headers.js +29 -0
- package/dist/src/utils/response-headers.js.map +1 -0
- package/dist/src/utils/responses.d.ts +21 -4
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/responses.js +58 -0
- package/dist/src/utils/responses.js.map +1 -1
- package/dist/src/verified-fetch.d.ts +2 -1
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +61 -25
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +49 -8
- package/src/types.ts +2 -0
- package/src/utils/byte-range-context.ts +303 -0
- package/src/utils/get-stream-from-async-iterable.ts +1 -1
- package/src/utils/parse-resource.ts +2 -2
- package/src/utils/parse-url-string.ts +7 -7
- package/src/utils/request-headers.ts +51 -0
- package/src/utils/response-headers.ts +32 -0
- package/src/utils/responses.ts +82 -4
- package/src/verified-fetch.ts +68 -29
package/src/utils/responses.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { ByteRangeContext } from './byte-range-context'
|
|
2
|
+
import type { SupportedBodyTypes } from '../types.js'
|
|
3
|
+
import type { Logger } from '@libp2p/interface'
|
|
4
|
+
|
|
1
5
|
function setField (response: Response, name: string, value: string | boolean): void {
|
|
2
6
|
Object.defineProperty(response, name, {
|
|
3
7
|
enumerable: true,
|
|
@@ -23,7 +27,7 @@ export interface ResponseOptions extends ResponseInit {
|
|
|
23
27
|
redirected?: boolean
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
export function okResponse (url: string, body?:
|
|
30
|
+
export function okResponse (url: string, body?: SupportedBodyTypes, init?: ResponseOptions): Response {
|
|
27
31
|
const response = new Response(body, {
|
|
28
32
|
...(init ?? {}),
|
|
29
33
|
status: 200,
|
|
@@ -34,13 +38,27 @@ export function okResponse (url: string, body?: BodyInit | null, init?: Response
|
|
|
34
38
|
setRedirected(response)
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
setType(response, 'basic')
|
|
42
|
+
setUrl(response, url)
|
|
43
|
+
response.headers.set('Accept-Ranges', 'bytes')
|
|
44
|
+
|
|
45
|
+
return response
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function badGatewayResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
49
|
+
const response = new Response(body, {
|
|
50
|
+
...(init ?? {}),
|
|
51
|
+
status: 502,
|
|
52
|
+
statusText: 'Bad Gateway'
|
|
53
|
+
})
|
|
54
|
+
|
|
37
55
|
setType(response, 'basic')
|
|
38
56
|
setUrl(response, url)
|
|
39
57
|
|
|
40
58
|
return response
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
export function notSupportedResponse (url: string, body?:
|
|
61
|
+
export function notSupportedResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
44
62
|
const response = new Response(body, {
|
|
45
63
|
...(init ?? {}),
|
|
46
64
|
status: 501,
|
|
@@ -54,7 +72,7 @@ export function notSupportedResponse (url: string, body?: BodyInit | null, init?
|
|
|
54
72
|
return response
|
|
55
73
|
}
|
|
56
74
|
|
|
57
|
-
export function notAcceptableResponse (url: string, body?:
|
|
75
|
+
export function notAcceptableResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
58
76
|
const response = new Response(body, {
|
|
59
77
|
...(init ?? {}),
|
|
60
78
|
status: 406,
|
|
@@ -67,7 +85,7 @@ export function notAcceptableResponse (url: string, body?: BodyInit | null, init
|
|
|
67
85
|
return response
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
export function badRequestResponse (url: string, body?:
|
|
88
|
+
export function badRequestResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
71
89
|
const response = new Response(body, {
|
|
72
90
|
...(init ?? {}),
|
|
73
91
|
status: 400,
|
|
@@ -96,3 +114,63 @@ export function movedPermanentlyResponse (url: string, location: string, init?:
|
|
|
96
114
|
|
|
97
115
|
return response
|
|
98
116
|
}
|
|
117
|
+
|
|
118
|
+
interface RangeOptions {
|
|
119
|
+
byteRangeContext: ByteRangeContext
|
|
120
|
+
log?: Logger
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function okRangeResponse (url: string, body: SupportedBodyTypes, { byteRangeContext, log }: RangeOptions, init?: ResponseOptions): Response {
|
|
124
|
+
if (!byteRangeContext.isRangeRequest) {
|
|
125
|
+
return okResponse(url, body, init)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!byteRangeContext.isValidRangeRequest) {
|
|
129
|
+
return badRangeResponse(url, body, init)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let response: Response
|
|
133
|
+
try {
|
|
134
|
+
response = new Response(body, {
|
|
135
|
+
...(init ?? {}),
|
|
136
|
+
status: 206,
|
|
137
|
+
statusText: 'Partial Content',
|
|
138
|
+
headers: {
|
|
139
|
+
...(init?.headers ?? {}),
|
|
140
|
+
'content-range': byteRangeContext.contentRangeHeaderValue
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
} catch (e: any) {
|
|
144
|
+
log?.error('failed to create range response', e)
|
|
145
|
+
return badRangeResponse(url, body, init)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (init?.redirected === true) {
|
|
149
|
+
setRedirected(response)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setType(response, 'basic')
|
|
153
|
+
setUrl(response, url)
|
|
154
|
+
response.headers.set('Accept-Ranges', 'bytes')
|
|
155
|
+
|
|
156
|
+
return response
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* We likely need to catch errors handled by upstream helia libraries if range-request throws an error. Some examples:
|
|
161
|
+
* * The range is out of bounds
|
|
162
|
+
* * The range is invalid
|
|
163
|
+
* * The range is not supported for the given type
|
|
164
|
+
*/
|
|
165
|
+
export function badRangeResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
|
|
166
|
+
const response = new Response(body, {
|
|
167
|
+
...(init ?? {}),
|
|
168
|
+
status: 416,
|
|
169
|
+
statusText: 'Requested Range Not Satisfiable'
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
setType(response, 'basic')
|
|
173
|
+
setUrl(response, url)
|
|
174
|
+
|
|
175
|
+
return response
|
|
176
|
+
}
|
package/src/verified-fetch.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { car } from '@helia/car'
|
|
2
|
-
import { ipns as heliaIpns, type
|
|
3
|
-
import {
|
|
4
|
-
import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs'
|
|
2
|
+
import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
|
|
3
|
+
import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs } from '@helia/unixfs'
|
|
5
4
|
import * as ipldDagCbor from '@ipld/dag-cbor'
|
|
6
5
|
import * as ipldDagJson from '@ipld/dag-json'
|
|
7
6
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
@@ -16,19 +15,22 @@ import { CustomProgressEvent } from 'progress-events'
|
|
|
16
15
|
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
|
|
17
16
|
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
18
17
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
18
|
+
import { ByteRangeContext } from './utils/byte-range-context.js'
|
|
19
19
|
import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js'
|
|
20
20
|
import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
|
|
21
21
|
import { getETag } from './utils/get-e-tag.js'
|
|
22
22
|
import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
|
|
23
23
|
import { tarStream } from './utils/get-tar-stream.js'
|
|
24
24
|
import { parseResource } from './utils/parse-resource.js'
|
|
25
|
-
import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse } from './utils/responses.js'
|
|
25
|
+
import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse } from './utils/responses.js'
|
|
26
26
|
import { selectOutputType, queryFormatToAcceptHeader } from './utils/select-output-type.js'
|
|
27
27
|
import { walkPath } from './utils/walk-path.js'
|
|
28
28
|
import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
29
29
|
import type { RequestFormatShorthand } from './types.js'
|
|
30
|
+
import type { ParsedUrlStringResults } from './utils/parse-url-string'
|
|
30
31
|
import type { Helia } from '@helia/interface'
|
|
31
32
|
import type { AbortOptions, Logger, PeerId } from '@libp2p/interface'
|
|
33
|
+
import type { DNSResolver } from '@multiformats/dns/resolvers'
|
|
32
34
|
import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
|
|
33
35
|
import type { CID } from 'multiformats/cid'
|
|
34
36
|
|
|
@@ -126,12 +128,7 @@ export class VerifiedFetch {
|
|
|
126
128
|
constructor ({ helia, ipns, unixfs }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
|
|
127
129
|
this.helia = helia
|
|
128
130
|
this.log = helia.logger.forComponent('helia:verified-fetch')
|
|
129
|
-
this.ipns = ipns ?? heliaIpns(helia
|
|
130
|
-
resolvers: init?.dnsResolvers ?? [
|
|
131
|
-
dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query'),
|
|
132
|
-
dnsJsonOverHttps('https://dns.google/resolve')
|
|
133
|
-
]
|
|
134
|
-
})
|
|
131
|
+
this.ipns = ipns ?? heliaIpns(helia)
|
|
135
132
|
this.unixfs = unixfs ?? heliaUnixFs(helia)
|
|
136
133
|
this.contentTypeParser = init?.contentTypeParser
|
|
137
134
|
this.log.trace('created VerifiedFetch instance')
|
|
@@ -280,17 +277,19 @@ export class VerifiedFetch {
|
|
|
280
277
|
let terminalElement: UnixFSEntry | undefined
|
|
281
278
|
let ipfsRoots: CID[] | undefined
|
|
282
279
|
let redirected = false
|
|
280
|
+
const byteRangeContext = new ByteRangeContext(this.helia.logger, options?.headers)
|
|
283
281
|
|
|
284
282
|
try {
|
|
285
283
|
const pathDetails = await walkPath(this.helia.blockstore, `${cid.toString()}/${path}`, options)
|
|
286
284
|
ipfsRoots = pathDetails.ipfsRoots
|
|
287
285
|
terminalElement = pathDetails.terminalElement
|
|
288
286
|
} catch (err) {
|
|
289
|
-
this.log.error('
|
|
287
|
+
this.log.error('error walking path %s', path, err)
|
|
288
|
+
|
|
289
|
+
return badGatewayResponse('Error walking path')
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
let resolvedCID = terminalElement?.cid ?? cid
|
|
293
|
-
let stat: UnixFSStats
|
|
294
293
|
if (terminalElement?.type === 'directory') {
|
|
295
294
|
const dirCid = terminalElement.cid
|
|
296
295
|
|
|
@@ -312,7 +311,7 @@ export class VerifiedFetch {
|
|
|
312
311
|
const rootFilePath = 'index.html'
|
|
313
312
|
try {
|
|
314
313
|
this.log.trace('found directory at %c/%s, looking for index.html', cid, path)
|
|
315
|
-
stat = await this.unixfs.stat(dirCid, {
|
|
314
|
+
const stat = await this.unixfs.stat(dirCid, {
|
|
316
315
|
path: rootFilePath,
|
|
317
316
|
signal: options?.signal,
|
|
318
317
|
onProgress: options?.onProgress
|
|
@@ -328,30 +327,56 @@ export class VerifiedFetch {
|
|
|
328
327
|
}
|
|
329
328
|
}
|
|
330
329
|
|
|
330
|
+
// we have a validRangeRequest & terminalElement is a file, we know the size and should set it
|
|
331
|
+
if (byteRangeContext.isRangeRequest && byteRangeContext.isValidRangeRequest && terminalElement.type === 'file') {
|
|
332
|
+
byteRangeContext.setFileSize(terminalElement.unixfs.fileSize())
|
|
333
|
+
|
|
334
|
+
this.log.trace('fileSize for rangeRequest %d', byteRangeContext.getFileSize())
|
|
335
|
+
}
|
|
336
|
+
const offset = byteRangeContext.offset
|
|
337
|
+
const length = byteRangeContext.length
|
|
338
|
+
this.log.trace('calling unixfs.cat for %c/%s with offset=%o & length=%o', resolvedCID, path, offset, length)
|
|
331
339
|
const asyncIter = this.unixfs.cat(resolvedCID, {
|
|
332
340
|
signal: options?.signal,
|
|
333
|
-
onProgress: options?.onProgress
|
|
341
|
+
onProgress: options?.onProgress,
|
|
342
|
+
offset,
|
|
343
|
+
length
|
|
334
344
|
})
|
|
335
345
|
this.log('got async iterator for %c/%s', cid, path)
|
|
336
346
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
347
|
+
try {
|
|
348
|
+
const { stream, firstChunk } = await getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
|
|
349
|
+
onProgress: options?.onProgress
|
|
350
|
+
})
|
|
351
|
+
byteRangeContext.setBody(stream)
|
|
352
|
+
// if not a valid range request, okRangeRequest will call okResponse
|
|
353
|
+
const response = okRangeResponse(resource, byteRangeContext.getBody(), { byteRangeContext, log: this.log }, {
|
|
354
|
+
redirected
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
await this.setContentType(firstChunk, path, response)
|
|
358
|
+
|
|
359
|
+
if (ipfsRoots != null) {
|
|
360
|
+
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
|
|
361
|
+
}
|
|
344
362
|
|
|
345
|
-
|
|
346
|
-
|
|
363
|
+
return response
|
|
364
|
+
} catch (err: any) {
|
|
365
|
+
this.log.error('error streaming %c/%s', cid, path, err)
|
|
366
|
+
if (byteRangeContext.isRangeRequest && err.code === 'ERR_INVALID_PARAMS') {
|
|
367
|
+
return badRangeResponse(resource)
|
|
368
|
+
}
|
|
369
|
+
return badGatewayResponse('Unable to stream content')
|
|
347
370
|
}
|
|
348
|
-
|
|
349
|
-
return response
|
|
350
371
|
}
|
|
351
372
|
|
|
352
373
|
private async handleRaw ({ resource, cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
374
|
+
const byteRangeContext = new ByteRangeContext(this.helia.logger, options?.headers)
|
|
353
375
|
const result = await this.helia.blockstore.get(cid, options)
|
|
354
|
-
|
|
376
|
+
byteRangeContext.setBody(result)
|
|
377
|
+
const response = okRangeResponse(resource, byteRangeContext.getBody(), { byteRangeContext, log: this.log }, {
|
|
378
|
+
redirected: false
|
|
379
|
+
})
|
|
355
380
|
|
|
356
381
|
// if the user has specified an `Accept` header that corresponds to a raw
|
|
357
382
|
// type, honour that header, so for example they don't request
|
|
@@ -385,10 +410,10 @@ export class VerifiedFetch {
|
|
|
385
410
|
contentType = parsed
|
|
386
411
|
}
|
|
387
412
|
} catch (err) {
|
|
388
|
-
this.log.error('
|
|
413
|
+
this.log.error('error parsing content type', err)
|
|
389
414
|
}
|
|
390
415
|
}
|
|
391
|
-
|
|
416
|
+
this.log.trace('setting content type to "%s"', contentType)
|
|
392
417
|
response.headers.set('content-type', contentType)
|
|
393
418
|
}
|
|
394
419
|
|
|
@@ -413,7 +438,19 @@ export class VerifiedFetch {
|
|
|
413
438
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { resource }))
|
|
414
439
|
|
|
415
440
|
// resolve the CID/path from the requested resource
|
|
416
|
-
|
|
441
|
+
let cid: ParsedUrlStringResults['cid']
|
|
442
|
+
let path: ParsedUrlStringResults['path']
|
|
443
|
+
let query: ParsedUrlStringResults['query']
|
|
444
|
+
try {
|
|
445
|
+
const result = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
446
|
+
cid = result.cid
|
|
447
|
+
path = result.path
|
|
448
|
+
query = result.query
|
|
449
|
+
} catch (err) {
|
|
450
|
+
this.log.error('error parsing resource %s', resource, err)
|
|
451
|
+
|
|
452
|
+
return badRequestResponse('Invalid resource')
|
|
453
|
+
}
|
|
417
454
|
|
|
418
455
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
|
|
419
456
|
|
|
@@ -466,12 +503,14 @@ export class VerifiedFetch {
|
|
|
466
503
|
query.filename = query.filename ?? `${cid.toString()}.tar`
|
|
467
504
|
response = await this.handleTar(handlerArgs)
|
|
468
505
|
} else {
|
|
506
|
+
this.log.trace('finding handler for cid code "%s" and output type "%s"', cid.code, accept)
|
|
469
507
|
// derive the handler from the CID type
|
|
470
508
|
const codecHandler = this.codecHandlers[cid.code]
|
|
471
509
|
|
|
472
510
|
if (codecHandler == null) {
|
|
473
511
|
return notSupportedResponse(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia/issues/new`)
|
|
474
512
|
}
|
|
513
|
+
this.log.trace('calling handler "%s"', codecHandler.name)
|
|
475
514
|
|
|
476
515
|
response = await codecHandler.call(this, handlerArgs)
|
|
477
516
|
}
|