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