@helia/verified-fetch 0.0.0 → 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 +353 -56
- package/dist/index.min.js +7 -29
- package/dist/src/index.d.ts +384 -69
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +345 -77
- package/dist/src/index.js.map +1 -1
- package/dist/src/singleton.d.ts +3 -0
- package/dist/src/singleton.d.ts.map +1 -0
- package/dist/src/singleton.js +15 -0
- package/dist/src/singleton.js.map +1 -0
- 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-stream-from-async-iterable.d.ts +10 -0
- package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -0
- package/dist/src/utils/{get-stream-and-content-type.js → get-stream-from-async-iterable.js} +11 -11
- package/dist/src/utils/get-stream-from-async-iterable.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-resource.d.ts +6 -1
- package/dist/src/utils/parse-resource.d.ts.map +1 -1
- package/dist/src/utils/parse-resource.js +2 -2
- package/dist/src/utils/parse-resource.js.map +1 -1
- package/dist/src/utils/parse-url-string.d.ts +10 -3
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +8 -4
- 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/utils/walk-path.d.ts +2 -1
- package/dist/src/utils/walk-path.d.ts.map +1 -1
- package/dist/src/utils/walk-path.js +1 -3
- package/dist/src/utils/walk-path.js.map +1 -1
- package/dist/src/verified-fetch.d.ts +24 -27
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +297 -150
- package/dist/src/verified-fetch.js.map +1 -1
- package/dist/typedoc-urls.json +25 -18
- package/package.json +58 -116
- package/src/index.ts +391 -72
- package/src/singleton.ts +20 -0
- 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-stream-and-content-type.ts → get-stream-from-async-iterable.ts} +10 -9
- package/src/utils/get-tar-stream.ts +68 -0
- package/src/utils/parse-url-string.ts +17 -3
- package/src/utils/responses.ts +29 -0
- package/src/utils/select-output-type.ts +167 -0
- package/src/utils/walk-path.ts +4 -5
- package/src/verified-fetch.ts +340 -153
- package/dist/src/utils/get-content-type.d.ts +0 -11
- package/dist/src/utils/get-content-type.d.ts.map +0 -1
- package/dist/src/utils/get-content-type.js +0 -43
- package/dist/src/utils/get-content-type.js.map +0 -1
- package/dist/src/utils/get-stream-and-content-type.d.ts +0 -9
- package/dist/src/utils/get-stream-and-content-type.d.ts.map +0 -1
- package/dist/src/utils/get-stream-and-content-type.js.map +0 -1
- package/src/utils/get-content-type.ts +0 -55
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'
|
|
13
|
-
import {
|
|
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'
|
|
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 {
|
|
16
|
-
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'
|
|
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,25 +36,31 @@ 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
|
/**
|
|
33
42
|
* Potential future options for the VerifiedFetch constructor.
|
|
34
43
|
*/
|
|
35
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
36
44
|
interface VerifiedFetchInit {
|
|
37
|
-
|
|
45
|
+
contentTypeParser?: ContentTypeParser
|
|
46
|
+
dnsResolvers?: DNSResolver[]
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
interface FetchHandlerFunctionArg {
|
|
41
50
|
cid: CID
|
|
42
51
|
path: string
|
|
43
|
-
terminalElement?: UnixFSEntry
|
|
44
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
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
interface FetchHandlerFunction {
|
|
@@ -63,88 +82,212 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
|
|
|
63
82
|
}
|
|
64
83
|
}
|
|
65
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
|
+
|
|
66
119
|
export class VerifiedFetch {
|
|
67
120
|
private readonly helia: Helia
|
|
68
121
|
private readonly ipns: IPNS
|
|
69
122
|
private readonly unixfs: HeliaUnixFs
|
|
70
|
-
private readonly dagJson: DAGJSON
|
|
71
|
-
private readonly dagCbor: DAGCBOR
|
|
72
|
-
private readonly json: JSON
|
|
73
|
-
private readonly pathWalker: PathWalkerFn
|
|
74
123
|
private readonly log: Logger
|
|
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.
|
|
87
|
-
this.json = json ?? heliaJson(helia)
|
|
88
|
-
this.dagCbor = dagCbor ?? heliaDagCbor(helia)
|
|
89
|
-
this.pathWalker = pathWalker ?? walkPath
|
|
136
|
+
this.contentTypeParser = init?.contentTypeParser
|
|
90
137
|
this.log.trace('created VerifiedFetch instance')
|
|
91
138
|
}
|
|
92
139
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
|
|
97
173
|
return response
|
|
98
174
|
}
|
|
99
175
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
|
|
104
187
|
return response
|
|
105
188
|
}
|
|
106
189
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
|
|
117
204
|
return response
|
|
118
205
|
}
|
|
119
206
|
|
|
120
|
-
private async handleJson ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
207
|
+
private async handleJson ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
121
208
|
this.log.trace('fetching %c/%s', cid, path)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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')
|
|
130
230
|
return response
|
|
131
231
|
}
|
|
132
232
|
|
|
133
|
-
private async handleDagCbor ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
233
|
+
private async handleDagCbor ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
134
234
|
this.log.trace('fetching %c/%s', cid, path)
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
|
|
143
276
|
return response
|
|
144
277
|
}
|
|
145
278
|
|
|
146
|
-
private async handleDagPb ({ cid, path, options
|
|
147
|
-
|
|
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
|
+
|
|
148
291
|
let resolvedCID = terminalElement?.cid ?? cid
|
|
149
292
|
let stat: UnixFSStats
|
|
150
293
|
if (terminalElement?.type === 'directory') {
|
|
@@ -153,7 +296,6 @@ export class VerifiedFetch {
|
|
|
153
296
|
const rootFilePath = 'index.html'
|
|
154
297
|
try {
|
|
155
298
|
this.log.trace('found directory at %c/%s, looking for index.html', cid, path)
|
|
156
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: dirCid.toString(), path: rootFilePath }))
|
|
157
299
|
stat = await this.unixfs.stat(dirCid, {
|
|
158
300
|
path: rootFilePath,
|
|
159
301
|
signal: options?.signal,
|
|
@@ -165,144 +307,185 @@ export class VerifiedFetch {
|
|
|
165
307
|
// terminalElement = stat
|
|
166
308
|
} catch (err: any) {
|
|
167
309
|
this.log('error loading path %c/%s', dirCid, rootFilePath, err)
|
|
168
|
-
return
|
|
310
|
+
return notSupportedResponse('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented')
|
|
169
311
|
} finally {
|
|
170
|
-
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 }))
|
|
171
313
|
}
|
|
172
314
|
}
|
|
173
315
|
|
|
174
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: resolvedCID.toString(), path: '' }))
|
|
175
316
|
const asyncIter = this.unixfs.cat(resolvedCID, {
|
|
176
317
|
signal: options?.signal,
|
|
177
318
|
onProgress: options?.onProgress
|
|
178
319
|
})
|
|
179
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: resolvedCID.toString(), path: '' }))
|
|
180
320
|
this.log('got async iterator for %c/%s', cid, path)
|
|
181
321
|
|
|
182
|
-
const {
|
|
322
|
+
const { stream, firstChunk } = await getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
|
|
183
323
|
onProgress: options?.onProgress
|
|
184
324
|
})
|
|
185
|
-
const response =
|
|
186
|
-
|
|
325
|
+
const response = okResponse(stream)
|
|
326
|
+
await this.setContentType(firstChunk, path, response)
|
|
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
|
+
}
|
|
187
331
|
|
|
188
332
|
return response
|
|
189
333
|
}
|
|
190
334
|
|
|
191
335
|
private async handleRaw ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
192
|
-
this.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
|
|
198
349
|
return response
|
|
199
350
|
}
|
|
200
351
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
*
|
|
204
|
-
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
|
|
205
|
-
* @default 'raw'
|
|
206
|
-
*/
|
|
207
|
-
private getFormat ({ headerFormat, queryFormat }: { headerFormat: string | null, queryFormat: string | null }): string | null {
|
|
208
|
-
const formatMap: Record<string, string> = {
|
|
209
|
-
'vnd.ipld.raw': 'raw',
|
|
210
|
-
'vnd.ipld.car': 'car',
|
|
211
|
-
'application/x-tar': 'tar',
|
|
212
|
-
'application/vnd.ipld.dag-json': 'dag-json',
|
|
213
|
-
'application/vnd.ipld.dag-cbor': 'dag-cbor',
|
|
214
|
-
'application/json': 'json',
|
|
215
|
-
'application/cbor': 'cbor',
|
|
216
|
-
'vnd.ipfs.ipns-record': 'ipns-record'
|
|
217
|
-
}
|
|
352
|
+
private async setContentType (bytes: Uint8Array, path: string, response: Response): Promise<void> {
|
|
353
|
+
let contentType = 'application/octet-stream'
|
|
218
354
|
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
355
|
+
if (this.contentTypeParser != null) {
|
|
356
|
+
try {
|
|
357
|
+
let fileName = path.split('/').pop()?.trim()
|
|
358
|
+
fileName = fileName === '' ? undefined : fileName
|
|
359
|
+
const parsed = this.contentTypeParser(bytes, fileName)
|
|
360
|
+
|
|
361
|
+
if (isPromise(parsed)) {
|
|
362
|
+
const result = await parsed
|
|
363
|
+
|
|
364
|
+
if (result != null) {
|
|
365
|
+
contentType = result
|
|
366
|
+
}
|
|
367
|
+
} else if (parsed != null) {
|
|
368
|
+
contentType = parsed
|
|
223
369
|
}
|
|
370
|
+
} catch (err) {
|
|
371
|
+
this.log.error('Error parsing content type', err)
|
|
224
372
|
}
|
|
225
|
-
} else if (queryFormat != null) {
|
|
226
|
-
return queryFormat
|
|
227
373
|
}
|
|
228
374
|
|
|
229
|
-
|
|
375
|
+
response.headers.set('content-type', contentType)
|
|
230
376
|
}
|
|
231
377
|
|
|
232
378
|
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
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.
|
|
235
381
|
*/
|
|
236
|
-
private readonly formatHandlers: Record<string, FetchHandlerFunction> = {
|
|
237
|
-
raw: async () => new Response('application/vnd.ipld.raw support is not implemented', { status: 501 }),
|
|
238
|
-
car: this.handleIPLDCar,
|
|
239
|
-
'ipns-record': this.handleIPNSRecord,
|
|
240
|
-
tar: async () => new Response('application/x-tar support is not implemented', { status: 501 }),
|
|
241
|
-
'dag-json': async () => new Response('application/vnd.ipld.dag-json support is not implemented', { status: 501 }),
|
|
242
|
-
'dag-cbor': async () => new Response('application/vnd.ipld.dag-cbor support is not implemented', { status: 501 }),
|
|
243
|
-
json: async () => new Response('application/json support is not implemented', { status: 501 }),
|
|
244
|
-
cbor: async () => new Response('application/cbor support is not implemented', { status: 501 })
|
|
245
|
-
}
|
|
246
|
-
|
|
247
382
|
private readonly codecHandlers: Record<number, FetchHandlerFunction> = {
|
|
248
|
-
[dagJsonCode]: this.handleDagJson,
|
|
249
383
|
[dagPbCode]: this.handleDagPb,
|
|
384
|
+
[ipldDagJson.code]: this.handleJson,
|
|
250
385
|
[jsonCode]: this.handleJson,
|
|
251
|
-
[
|
|
252
|
-
[rawCode]: this.handleRaw
|
|
386
|
+
[ipldDagCbor.code]: this.handleDagCbor,
|
|
387
|
+
[rawCode]: this.handleRaw,
|
|
388
|
+
[identity.code]: this.handleRaw
|
|
253
389
|
}
|
|
254
390
|
|
|
255
391
|
async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
|
|
392
|
+
this.log('fetch %s', resource)
|
|
393
|
+
|
|
256
394
|
const options = convertOptions(opts)
|
|
257
|
-
const { path, query, ...rest } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
258
|
-
const cid = rest.cid
|
|
259
|
-
let response: Response | undefined
|
|
260
395
|
|
|
261
|
-
|
|
396
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { resource }))
|
|
262
397
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
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)
|
|
266
400
|
|
|
267
|
-
|
|
268
|
-
response = await formatHandler.call(this, { cid, path, options })
|
|
401
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
|
|
269
402
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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)
|
|
274
408
|
}
|
|
275
409
|
|
|
276
|
-
|
|
277
|
-
let ipfsRoots: string | undefined
|
|
410
|
+
const queryFormatMapping = queryFormatToAcceptHeader(query.format)
|
|
278
411
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
ipfsRoots = pathDetails.ipfsRoots.join(',')
|
|
282
|
-
terminalElement = pathDetails.terminalElement
|
|
283
|
-
} catch (err) {
|
|
284
|
-
this.log.error('Error walking path %s', path, err)
|
|
285
|
-
// return new Response(`Error walking path: ${(err as Error).message}`, { status: 500 })
|
|
412
|
+
if (query.format != null) {
|
|
413
|
+
this.log('incoming query format "%s", mapped to %s', query.format, queryFormatMapping)
|
|
286
414
|
}
|
|
287
415
|
|
|
288
|
-
|
|
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()
|
|
422
|
+
}
|
|
423
|
+
|
|
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
|
|
289
453
|
const codecHandler = this.codecHandlers[cid.code]
|
|
290
454
|
|
|
291
|
-
if (codecHandler
|
|
292
|
-
|
|
293
|
-
} else {
|
|
294
|
-
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`)
|
|
295
457
|
}
|
|
458
|
+
|
|
459
|
+
response = await codecHandler.call(this, handlerArgs)
|
|
296
460
|
}
|
|
297
461
|
|
|
298
|
-
response.headers.set('etag', cid
|
|
299
|
-
response.headers.set('cache-
|
|
300
|
-
|
|
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())
|
|
301
466
|
|
|
302
|
-
|
|
303
|
-
|
|
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)
|
|
304
486
|
}
|
|
305
|
-
|
|
487
|
+
|
|
488
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
306
489
|
|
|
307
490
|
return response
|
|
308
491
|
}
|
|
@@ -321,3 +504,7 @@ export class VerifiedFetch {
|
|
|
321
504
|
await this.helia.stop()
|
|
322
505
|
}
|
|
323
506
|
}
|
|
507
|
+
|
|
508
|
+
function isPromise <T> (p?: any): p is Promise<T> {
|
|
509
|
+
return p?.then != null
|
|
510
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
interface TestInput {
|
|
2
|
-
bytes: Uint8Array;
|
|
3
|
-
path: string;
|
|
4
|
-
}
|
|
5
|
-
export declare const DEFAULT_MIME_TYPE = "application/octet-stream";
|
|
6
|
-
/**
|
|
7
|
-
* Get the content type from the input based on the tests.
|
|
8
|
-
*/
|
|
9
|
-
export declare function getContentType(input: TestInput): Promise<string>;
|
|
10
|
-
export {};
|
|
11
|
-
//# sourceMappingURL=get-content-type.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get-content-type.d.ts","sourceRoot":"","sources":["../../../src/utils/get-content-type.ts"],"names":[],"mappings":"AAEA,UAAU,SAAS;IACjB,KAAK,EAAE,UAAU,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;CACb;AAID,eAAO,MAAM,iBAAiB,6BAA6B,CAAA;AAkC3D;;GAEG;AACH,wBAAsB,cAAc,CAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAQvE"}
|