@helia/verified-fetch 0.0.0-9b1ddf8 → 0.0.0-a04e041
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 +238 -6
- package/dist/index.min.js +4 -4
- package/dist/src/index.d.ts +222 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +219 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.d.ts +2 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/dag-cbor-to-safe-json.d.ts +7 -0
- package/dist/src/utils/dag-cbor-to-safe-json.d.ts.map +1 -0
- package/dist/src/utils/dag-cbor-to-safe-json.js +37 -0
- package/dist/src/utils/dag-cbor-to-safe-json.js.map +1 -0
- package/dist/src/utils/get-content-disposition-filename.d.ts +6 -0
- package/dist/src/utils/get-content-disposition-filename.d.ts.map +1 -0
- package/dist/src/utils/get-content-disposition-filename.js +16 -0
- package/dist/src/utils/get-content-disposition-filename.js.map +1 -0
- package/dist/src/utils/get-e-tag.d.ts +28 -0
- package/dist/src/utils/get-e-tag.d.ts.map +1 -0
- package/dist/src/utils/get-e-tag.js +18 -0
- package/dist/src/utils/get-e-tag.js.map +1 -0
- package/dist/src/utils/parse-url-string.d.ts +7 -1
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +6 -0
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/responses.d.ts +4 -0
- package/dist/src/utils/responses.d.ts.map +1 -0
- package/dist/src/utils/responses.js +21 -0
- package/dist/src/utils/responses.js.map +1 -0
- package/dist/src/utils/select-output-type.d.ts +12 -0
- package/dist/src/utils/select-output-type.d.ts.map +1 -0
- package/dist/src/utils/select-output-type.js +147 -0
- package/dist/src/utils/select-output-type.js.map +1 -0
- package/dist/src/verified-fetch.d.ts +17 -25
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +225 -142
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +27 -15
- package/src/index.ts +223 -4
- package/src/types.ts +1 -0
- package/src/utils/dag-cbor-to-safe-json.ts +44 -0
- package/src/utils/get-content-disposition-filename.ts +18 -0
- package/src/utils/get-e-tag.ts +36 -0
- package/src/utils/parse-url-string.ts +17 -2
- package/src/utils/responses.ts +22 -0
- package/src/utils/select-output-type.ts +166 -0
- package/src/verified-fetch.ts +258 -152
package/src/verified-fetch.ts
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dagJson as heliaDagJson, type DAGJSON } from '@helia/dag-json'
|
|
1
|
+
import { car } from '@helia/car'
|
|
3
2
|
import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
|
|
4
3
|
import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
|
|
5
|
-
import { json as heliaJson, type JSON } from '@helia/json'
|
|
6
4
|
import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs'
|
|
7
|
-
import
|
|
8
|
-
import
|
|
5
|
+
import * as ipldDagCbor from '@ipld/dag-cbor'
|
|
6
|
+
import * as ipldDagJson from '@ipld/dag-json'
|
|
9
7
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
8
|
+
import toBrowserReadableStream from 'it-to-browser-readablestream'
|
|
10
9
|
import { code as jsonCode } from 'multiformats/codecs/json'
|
|
11
|
-
import {
|
|
10
|
+
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
11
|
+
import { identity } from 'multiformats/hashes/identity'
|
|
12
12
|
import { CustomProgressEvent } from 'progress-events'
|
|
13
|
+
import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js'
|
|
14
|
+
import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
|
|
15
|
+
import { getETag } from './utils/get-e-tag.js'
|
|
13
16
|
import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
|
|
14
17
|
import { parseResource } from './utils/parse-resource.js'
|
|
15
|
-
import {
|
|
18
|
+
import { notAcceptableResponse, notSupportedResponse, okResponse } from './utils/responses.js'
|
|
19
|
+
import { selectOutputType, queryFormatToAcceptHeader } from './utils/select-output-type.js'
|
|
20
|
+
import { walkPath } from './utils/walk-path.js'
|
|
16
21
|
import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
22
|
+
import type { RequestFormatShorthand } from './types.js'
|
|
17
23
|
import type { Helia } from '@helia/interface'
|
|
18
24
|
import type { AbortOptions, Logger } from '@libp2p/interface'
|
|
19
25
|
import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
|
|
@@ -23,10 +29,6 @@ interface VerifiedFetchComponents {
|
|
|
23
29
|
helia: Helia
|
|
24
30
|
ipns?: IPNS
|
|
25
31
|
unixfs?: HeliaUnixFs
|
|
26
|
-
dagJson?: DAGJSON
|
|
27
|
-
json?: JSON
|
|
28
|
-
dagCbor?: DAGCBOR
|
|
29
|
-
pathWalker?: PathWalkerFn
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -39,8 +41,13 @@ interface VerifiedFetchInit {
|
|
|
39
41
|
interface FetchHandlerFunctionArg {
|
|
40
42
|
cid: CID
|
|
41
43
|
path: string
|
|
42
|
-
terminalElement?: UnixFSEntry
|
|
43
44
|
options?: Omit<VerifiedFetchOptions, 'signal'> & AbortOptions
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* If present, the user has sent an accept header with this value - if the
|
|
48
|
+
* content cannot be represented in this format a 406 should be returned
|
|
49
|
+
*/
|
|
50
|
+
accept?: string
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
interface FetchHandlerFunction {
|
|
@@ -62,18 +69,48 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
|
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
|
|
72
|
+
/**
|
|
73
|
+
* These are Accept header values that will cause content type sniffing to be
|
|
74
|
+
* skipped and set to these values.
|
|
75
|
+
*/
|
|
76
|
+
const RAW_HEADERS = [
|
|
77
|
+
'application/vnd.ipld.raw',
|
|
78
|
+
'application/octet-stream'
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* if the user has specified an `Accept` header, and it's in our list of
|
|
83
|
+
* allowable "raw" format headers, use that instead of detecting the content
|
|
84
|
+
* type. This avoids the user from receiving something different when they
|
|
85
|
+
* signal that they want to `Accept` a specific mime type.
|
|
86
|
+
*/
|
|
87
|
+
function getOverridenRawContentType (headers?: HeadersInit): string | undefined {
|
|
88
|
+
const acceptHeader = new Headers(headers).get('accept') ?? ''
|
|
89
|
+
|
|
90
|
+
// e.g. "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
|
|
91
|
+
const acceptHeaders = acceptHeader.split(',')
|
|
92
|
+
.map(s => s.split(';')[0])
|
|
93
|
+
.map(s => s.trim())
|
|
94
|
+
|
|
95
|
+
for (const mimeType of acceptHeaders) {
|
|
96
|
+
if (mimeType === '*/*') {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (RAW_HEADERS.includes(mimeType ?? '')) {
|
|
101
|
+
return mimeType
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
65
106
|
export class VerifiedFetch {
|
|
66
107
|
private readonly helia: Helia
|
|
67
108
|
private readonly ipns: IPNS
|
|
68
109
|
private readonly unixfs: HeliaUnixFs
|
|
69
|
-
private readonly dagJson: DAGJSON
|
|
70
|
-
private readonly dagCbor: DAGCBOR
|
|
71
|
-
private readonly json: JSON
|
|
72
|
-
private readonly pathWalker: PathWalkerFn
|
|
73
110
|
private readonly log: Logger
|
|
74
111
|
private readonly contentTypeParser: ContentTypeParser | undefined
|
|
75
112
|
|
|
76
|
-
constructor ({ helia, ipns, unixfs
|
|
113
|
+
constructor ({ helia, ipns, unixfs }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
|
|
77
114
|
this.helia = helia
|
|
78
115
|
this.log = helia.logger.forComponent('helia:verified-fetch')
|
|
79
116
|
this.ipns = ipns ?? heliaIpns(helia, {
|
|
@@ -83,69 +120,128 @@ export class VerifiedFetch {
|
|
|
83
120
|
]
|
|
84
121
|
})
|
|
85
122
|
this.unixfs = unixfs ?? heliaUnixFs(helia)
|
|
86
|
-
this.dagJson = dagJson ?? heliaDagJson(helia)
|
|
87
|
-
this.json = json ?? heliaJson(helia)
|
|
88
|
-
this.dagCbor = dagCbor ?? heliaDagCbor(helia)
|
|
89
|
-
this.pathWalker = pathWalker ?? walkPath
|
|
90
123
|
this.contentTypeParser = init?.contentTypeParser
|
|
91
124
|
this.log.trace('created VerifiedFetch instance')
|
|
92
125
|
}
|
|
93
126
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Accepts an `ipns://...` URL as a string and returns a `Response` containing
|
|
129
|
+
* a raw IPNS record.
|
|
130
|
+
*/
|
|
131
|
+
private async handleIPNSRecord (resource: string, opts?: VerifiedFetchOptions): Promise<Response> {
|
|
132
|
+
return notSupportedResponse('vnd.ipfs.ipns-record support is not implemented')
|
|
99
133
|
}
|
|
100
134
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Accepts a `CID` and returns a `Response` with a body stream that is a CAR
|
|
137
|
+
* of the `DAG` referenced by the `CID`.
|
|
138
|
+
*/
|
|
139
|
+
private async handleCar ({ cid, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
140
|
+
const c = car(this.helia)
|
|
141
|
+
const stream = toBrowserReadableStream(c.stream(cid, options))
|
|
142
|
+
|
|
143
|
+
const response = okResponse(stream)
|
|
144
|
+
response.headers.set('content-type', 'application/vnd.ipld.car; version=1')
|
|
145
|
+
|
|
105
146
|
return response
|
|
106
147
|
}
|
|
107
148
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return response
|
|
149
|
+
/**
|
|
150
|
+
* Accepts a UnixFS `CID` and returns a `.tar` file containing the file or
|
|
151
|
+
* directory structure referenced by the `CID`.
|
|
152
|
+
*/
|
|
153
|
+
private async handleTar ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
154
|
+
if (cid.code !== dagPbCode) {
|
|
155
|
+
return notAcceptableResponse('only dag-pb CIDs can be returned in TAR files')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return notSupportedResponse('application/tar support is not implemented')
|
|
119
159
|
}
|
|
120
160
|
|
|
121
|
-
private async handleJson ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
161
|
+
private async handleJson ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
122
162
|
this.log.trace('fetching %c/%s', cid, path)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
163
|
+
const block = await this.helia.blockstore.get(cid, options)
|
|
164
|
+
let body: string | Uint8Array
|
|
165
|
+
|
|
166
|
+
if (accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
|
|
167
|
+
try {
|
|
168
|
+
// if vnd.ipld.dag-cbor has been specified, convert to the format - note
|
|
169
|
+
// that this supports more data types than regular JSON, the content-type
|
|
170
|
+
// response header is set so the user knows to process it differently
|
|
171
|
+
const obj = ipldDagJson.decode(block)
|
|
172
|
+
body = ipldDagCbor.encode(obj)
|
|
173
|
+
} catch (err) {
|
|
174
|
+
this.log.error('could not transform %c to application/vnd.ipld.dag-cbor', err)
|
|
175
|
+
return notAcceptableResponse()
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
// skip decoding
|
|
179
|
+
body = block
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const response = okResponse(body)
|
|
183
|
+
response.headers.set('content-type', accept ?? 'application/json')
|
|
131
184
|
return response
|
|
132
185
|
}
|
|
133
186
|
|
|
134
|
-
private async handleDagCbor ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
187
|
+
private async handleDagCbor ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
135
188
|
this.log.trace('fetching %c/%s', cid, path)
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
189
|
+
|
|
190
|
+
const block = await this.helia.blockstore.get(cid, options)
|
|
191
|
+
let body: string | Uint8Array
|
|
192
|
+
|
|
193
|
+
if (accept === 'application/octet-stream' || accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
|
|
194
|
+
// skip decoding
|
|
195
|
+
body = block
|
|
196
|
+
} else if (accept === 'application/vnd.ipld.dag-json') {
|
|
197
|
+
try {
|
|
198
|
+
// if vnd.ipld.dag-json has been specified, convert to the format - note
|
|
199
|
+
// that this supports more data types than regular JSON, the content-type
|
|
200
|
+
// response header is set so the user knows to process it differently
|
|
201
|
+
const obj = ipldDagCbor.decode(block)
|
|
202
|
+
body = ipldDagJson.encode(obj)
|
|
203
|
+
} catch (err) {
|
|
204
|
+
this.log.error('could not transform %c to application/vnd.ipld.dag-json', err)
|
|
205
|
+
return notAcceptableResponse()
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
try {
|
|
209
|
+
body = dagCborToSafeJSON(block)
|
|
210
|
+
} catch (err) {
|
|
211
|
+
if (accept === 'application/json') {
|
|
212
|
+
this.log('could not decode DAG-CBOR as JSON-safe, but the client sent "Accept: application/json"', err)
|
|
213
|
+
|
|
214
|
+
return notAcceptableResponse()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.log('could not decode DAG-CBOR as JSON-safe, falling back to `application/octet-stream`', err)
|
|
218
|
+
body = block
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const response = okResponse(body)
|
|
223
|
+
|
|
224
|
+
if (accept == null) {
|
|
225
|
+
accept = body instanceof Uint8Array ? 'application/octet-stream' : 'application/json'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
response.headers.set('content-type', accept)
|
|
229
|
+
|
|
144
230
|
return response
|
|
145
231
|
}
|
|
146
232
|
|
|
147
|
-
private async handleDagPb ({ cid, path, options
|
|
148
|
-
|
|
233
|
+
private async handleDagPb ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
234
|
+
let terminalElement: UnixFSEntry | undefined
|
|
235
|
+
let ipfsRoots: CID[] | undefined
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const pathDetails = await walkPath(this.helia.blockstore, `${cid.toString()}/${path}`, options)
|
|
239
|
+
ipfsRoots = pathDetails.ipfsRoots
|
|
240
|
+
terminalElement = pathDetails.terminalElement
|
|
241
|
+
} catch (err) {
|
|
242
|
+
this.log.error('Error walking path %s', path, err)
|
|
243
|
+
}
|
|
244
|
+
|
|
149
245
|
let resolvedCID = terminalElement?.cid ?? cid
|
|
150
246
|
let stat: UnixFSStats
|
|
151
247
|
if (terminalElement?.type === 'directory') {
|
|
@@ -154,7 +250,6 @@ export class VerifiedFetch {
|
|
|
154
250
|
const rootFilePath = 'index.html'
|
|
155
251
|
try {
|
|
156
252
|
this.log.trace('found directory at %c/%s, looking for index.html', cid, path)
|
|
157
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: dirCid, path: rootFilePath }))
|
|
158
253
|
stat = await this.unixfs.stat(dirCid, {
|
|
159
254
|
path: rootFilePath,
|
|
160
255
|
signal: options?.signal,
|
|
@@ -166,36 +261,45 @@ export class VerifiedFetch {
|
|
|
166
261
|
// terminalElement = stat
|
|
167
262
|
} catch (err: any) {
|
|
168
263
|
this.log('error loading path %c/%s', dirCid, rootFilePath, err)
|
|
169
|
-
return
|
|
264
|
+
return notSupportedResponse('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented')
|
|
170
265
|
} finally {
|
|
171
266
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: dirCid, path: rootFilePath }))
|
|
172
267
|
}
|
|
173
268
|
}
|
|
174
269
|
|
|
175
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: resolvedCID, path: '' }))
|
|
176
270
|
const asyncIter = this.unixfs.cat(resolvedCID, {
|
|
177
271
|
signal: options?.signal,
|
|
178
272
|
onProgress: options?.onProgress
|
|
179
273
|
})
|
|
180
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: resolvedCID, path: '' }))
|
|
181
274
|
this.log('got async iterator for %c/%s', cid, path)
|
|
182
275
|
|
|
183
276
|
const { stream, firstChunk } = await getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
|
|
184
277
|
onProgress: options?.onProgress
|
|
185
278
|
})
|
|
186
|
-
const response =
|
|
279
|
+
const response = okResponse(stream)
|
|
187
280
|
await this.setContentType(firstChunk, path, response)
|
|
188
281
|
|
|
282
|
+
if (ipfsRoots != null) {
|
|
283
|
+
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
|
|
284
|
+
}
|
|
285
|
+
|
|
189
286
|
return response
|
|
190
287
|
}
|
|
191
288
|
|
|
192
289
|
private async handleRaw ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
193
|
-
this.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
290
|
+
const result = await this.helia.blockstore.get(cid, options)
|
|
291
|
+
const response = okResponse(result)
|
|
292
|
+
|
|
293
|
+
// if the user has specified an `Accept` header that corresponds to a raw
|
|
294
|
+
// type, honour that header, so for example they don't request
|
|
295
|
+
// `application/vnd.ipld.raw` but get `application/octet-stream`
|
|
296
|
+
const overriddenContentType = getOverridenRawContentType(options?.headers)
|
|
297
|
+
if (overriddenContentType != null) {
|
|
298
|
+
response.headers.set('content-type', overriddenContentType)
|
|
299
|
+
} else {
|
|
300
|
+
await this.setContentType(result, path, response)
|
|
301
|
+
}
|
|
302
|
+
|
|
199
303
|
return response
|
|
200
304
|
}
|
|
201
305
|
|
|
@@ -226,110 +330,112 @@ export class VerifiedFetch {
|
|
|
226
330
|
}
|
|
227
331
|
|
|
228
332
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
|
|
232
|
-
* @default 'raw'
|
|
233
|
-
*/
|
|
234
|
-
private getFormat ({ headerFormat, queryFormat }: { headerFormat: string | null, queryFormat: string | null }): string | null {
|
|
235
|
-
const formatMap: Record<string, string> = {
|
|
236
|
-
'vnd.ipld.raw': 'raw',
|
|
237
|
-
'vnd.ipld.car': 'car',
|
|
238
|
-
'application/x-tar': 'tar',
|
|
239
|
-
'application/vnd.ipld.dag-json': 'dag-json',
|
|
240
|
-
'application/vnd.ipld.dag-cbor': 'dag-cbor',
|
|
241
|
-
'application/json': 'json',
|
|
242
|
-
'application/cbor': 'cbor',
|
|
243
|
-
'vnd.ipfs.ipns-record': 'ipns-record'
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (headerFormat != null) {
|
|
247
|
-
for (const format in formatMap) {
|
|
248
|
-
if (headerFormat.includes(format)) {
|
|
249
|
-
return formatMap[format]
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
} else if (queryFormat != null) {
|
|
253
|
-
return queryFormat
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return null
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Map of format to specific handlers for that format.
|
|
261
|
-
* These format handlers should adjust the response headers as specified in https://specs.ipfs.tech/http-gateways/path-gateway/#response-headers
|
|
333
|
+
* If the user has not specified an Accept header or format query string arg,
|
|
334
|
+
* use the CID codec to choose an appropriate handler for the block data.
|
|
262
335
|
*/
|
|
263
|
-
private readonly formatHandlers: Record<string, FetchHandlerFunction> = {
|
|
264
|
-
raw: async () => new Response('application/vnd.ipld.raw support is not implemented', { status: 501 }),
|
|
265
|
-
car: this.handleIPLDCar,
|
|
266
|
-
'ipns-record': this.handleIPNSRecord,
|
|
267
|
-
tar: async () => new Response('application/x-tar support is not implemented', { status: 501 }),
|
|
268
|
-
'dag-json': async () => new Response('application/vnd.ipld.dag-json support is not implemented', { status: 501 }),
|
|
269
|
-
'dag-cbor': async () => new Response('application/vnd.ipld.dag-cbor support is not implemented', { status: 501 }),
|
|
270
|
-
json: async () => new Response('application/json support is not implemented', { status: 501 }),
|
|
271
|
-
cbor: async () => new Response('application/cbor support is not implemented', { status: 501 })
|
|
272
|
-
}
|
|
273
|
-
|
|
274
336
|
private readonly codecHandlers: Record<number, FetchHandlerFunction> = {
|
|
275
|
-
[dagJsonCode]: this.handleDagJson,
|
|
276
337
|
[dagPbCode]: this.handleDagPb,
|
|
338
|
+
[ipldDagJson.code]: this.handleJson,
|
|
277
339
|
[jsonCode]: this.handleJson,
|
|
278
|
-
[
|
|
279
|
-
[rawCode]: this.handleRaw
|
|
340
|
+
[ipldDagCbor.code]: this.handleDagCbor,
|
|
341
|
+
[rawCode]: this.handleRaw,
|
|
342
|
+
[identity.code]: this.handleRaw
|
|
280
343
|
}
|
|
281
344
|
|
|
282
345
|
async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
|
|
346
|
+
this.log('fetch %s', resource)
|
|
347
|
+
|
|
283
348
|
const options = convertOptions(opts)
|
|
284
|
-
const { path, query, ...rest } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
285
|
-
const cid = rest.cid
|
|
286
|
-
let response: Response | undefined
|
|
287
349
|
|
|
288
|
-
|
|
350
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { resource }))
|
|
289
351
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const formatHandler = this.formatHandlers[format]
|
|
352
|
+
// resolve the CID/path from the requested resource
|
|
353
|
+
const { path, query, cid } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
293
354
|
|
|
294
|
-
|
|
295
|
-
response = await formatHandler.call(this, { cid, path, options })
|
|
355
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
|
|
296
356
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
357
|
+
const requestHeaders = new Headers(options?.headers)
|
|
358
|
+
const incomingAcceptHeader = requestHeaders.get('accept')
|
|
359
|
+
|
|
360
|
+
if (incomingAcceptHeader != null) {
|
|
361
|
+
this.log('incoming accept header "%s"', incomingAcceptHeader)
|
|
301
362
|
}
|
|
302
363
|
|
|
303
|
-
|
|
304
|
-
let ipfsRoots: CID[] | undefined
|
|
364
|
+
const queryFormatMapping = queryFormatToAcceptHeader(query.format)
|
|
305
365
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
ipfsRoots = pathDetails.ipfsRoots
|
|
309
|
-
terminalElement = pathDetails.terminalElement
|
|
310
|
-
} catch (err) {
|
|
311
|
-
this.log.error('Error walking path %s', path, err)
|
|
312
|
-
// return new Response(`Error walking path: ${(err as Error).message}`, { status: 500 })
|
|
366
|
+
if (query.format != null) {
|
|
367
|
+
this.log('incoming query format "%s", mapped to %s', query.format, queryFormatMapping)
|
|
313
368
|
}
|
|
314
369
|
|
|
315
|
-
|
|
370
|
+
const acceptHeader = incomingAcceptHeader ?? queryFormatMapping
|
|
371
|
+
const accept = selectOutputType(cid, acceptHeader)
|
|
372
|
+
this.log('output type %s', accept)
|
|
373
|
+
|
|
374
|
+
if (acceptHeader != null && accept == null) {
|
|
375
|
+
return notAcceptableResponse()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let response: Response
|
|
379
|
+
let reqFormat: RequestFormatShorthand | undefined
|
|
380
|
+
|
|
381
|
+
if (accept === 'application/vnd.ipfs.ipns-record') {
|
|
382
|
+
// the user requested a raw IPNS record
|
|
383
|
+
reqFormat = 'ipns-record'
|
|
384
|
+
response = await this.handleIPNSRecord(resource.toString(), options)
|
|
385
|
+
} else if (accept === 'application/vnd.ipld.car') {
|
|
386
|
+
// the user requested a CAR file
|
|
387
|
+
reqFormat = 'car'
|
|
388
|
+
query.download = true
|
|
389
|
+
query.filename = query.filename ?? `${cid.toString()}.car`
|
|
390
|
+
response = await this.handleCar({ cid, path, options })
|
|
391
|
+
} else if (accept === 'application/vnd.ipld.raw') {
|
|
392
|
+
// the user requested a raw block
|
|
393
|
+
reqFormat = 'raw'
|
|
394
|
+
query.download = true
|
|
395
|
+
query.filename = query.filename ?? `${cid.toString()}.bin`
|
|
396
|
+
response = await this.handleRaw({ cid, path, options })
|
|
397
|
+
} else if (accept === 'application/x-tar') {
|
|
398
|
+
// the user requested a TAR file
|
|
399
|
+
reqFormat = 'tar'
|
|
400
|
+
response = await this.handleTar({ cid, path, options })
|
|
401
|
+
} else {
|
|
402
|
+
// derive the handler from the CID type
|
|
316
403
|
const codecHandler = this.codecHandlers[cid.code]
|
|
317
404
|
|
|
318
|
-
if (codecHandler
|
|
319
|
-
|
|
320
|
-
} else {
|
|
321
|
-
return new Response(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia/issues/new`, { status: 501 })
|
|
405
|
+
if (codecHandler == null) {
|
|
406
|
+
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`)
|
|
322
407
|
}
|
|
408
|
+
|
|
409
|
+
response = await codecHandler.call(this, { cid, path, accept, options })
|
|
323
410
|
}
|
|
324
411
|
|
|
325
|
-
response.headers.set('etag', cid
|
|
326
|
-
response.headers.set('cache-
|
|
327
|
-
|
|
412
|
+
response.headers.set('etag', getETag({ cid, reqFormat, weak: false }))
|
|
413
|
+
response.headers.set('cache-control', 'public, max-age=29030400, immutable')
|
|
414
|
+
// https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
|
|
415
|
+
response.headers.set('X-Ipfs-Path', resource.toString())
|
|
328
416
|
|
|
329
|
-
|
|
330
|
-
|
|
417
|
+
// set Content-Disposition header
|
|
418
|
+
let contentDisposition: string | undefined
|
|
419
|
+
|
|
420
|
+
// force download if requested
|
|
421
|
+
if (query.download === true) {
|
|
422
|
+
contentDisposition = 'attachment'
|
|
331
423
|
}
|
|
332
|
-
|
|
424
|
+
|
|
425
|
+
// override filename if requested
|
|
426
|
+
if (query.filename != null) {
|
|
427
|
+
if (contentDisposition == null) {
|
|
428
|
+
contentDisposition = 'inline'
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(query.filename)}`
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (contentDisposition != null) {
|
|
435
|
+
response.headers.set('Content-Disposition', contentDisposition)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
333
439
|
|
|
334
440
|
return response
|
|
335
441
|
}
|