Skip to content

Commit c30629a

Browse files
committed
feat: support provider query parameter
1 parent 07a0e6a commit c30629a

File tree

7 files changed

+315
-8
lines changed

7 files changed

+315
-8
lines changed

packages/verified-fetch/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@
181181
"@libp2p/webrtc": "^5.2.14",
182182
"@libp2p/websockets": "^9.2.12",
183183
"@multiformats/dns": "^1.0.6",
184+
"@multiformats/multiaddr": "^12.4.0",
184185
"cborg": "^4.2.11",
185186
"file-type": "^20.5.0",
186187
"helia": "^5.4.1",

packages/verified-fetch/src/plugins/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ContentTypeParser, RequestFormatShorthand } from '../types.js'
44
import type { ByteRangeContext } from '../utils/byte-range-context.js'
55
import type { ParsedUrlStringResults } from '../utils/parse-url-string.js'
66
import type { PathWalkerResponse } from '../utils/walk-path.js'
7+
import type { ProviderOptions } from '@helia/interface'
78
import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
89
import type { Helia } from 'helia'
910
import type { Blockstore } from 'interface-blockstore'
@@ -18,7 +19,7 @@ import type { CustomProgressEvent } from 'progress-events'
1819
*/
1920
export interface PluginOptions {
2021
logger: ComponentLogger
21-
getBlockstore(cid: CID, resource: string | CID, useSession?: boolean, options?: AbortOptions): Blockstore
22+
getBlockstore(cid: CID, resource: string | CID, useSession?: boolean, options?: AbortOptions & ProviderOptions): Blockstore
2223
handleServerTiming<T>(name: string, description: string, fn: () => Promise<T>, withServerTiming: boolean): Promise<T>
2324
contentTypeParser?: ContentTypeParser
2425
helia: Helia
@@ -42,7 +43,7 @@ export interface PluginContext extends ParsedUrlStringResults {
4243
modified: number
4344
withServerTiming?: boolean
4445
onProgress?(evt: CustomProgressEvent<any>): void
45-
options?: Omit<VerifiedFetchInit, 'signal'> & AbortOptions
46+
options?: Omit<VerifiedFetchInit, 'signal'> & AbortOptions & ProviderOptions
4647
isDirectory?: boolean
4748
directoryEntries?: UnixFSEntry[]
4849
errors?: PluginError[]

packages/verified-fetch/src/utils/parse-resource.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export async function parseResource (resource: Resource, { ipns, logger }: Parse
3434
query: {},
3535
ipfsPath: `/ipfs/${cid.toString()}`,
3636
ttl: 29030400, // 1 year for ipfs content
37-
serverTimings: []
37+
serverTimings: [],
38+
providers: []
3839
} satisfies ParsedUrlStringResults
3940
}
4041

packages/verified-fetch/src/utils/parse-url-string.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AbortError } from '@libp2p/interface'
2+
import { multiaddr } from '@multiformats/multiaddr'
23
import { CID } from 'multiformats/cid'
34
import { getPeerIdFromString } from './get-peer-id-from-string.js'
45
import { serverTiming } from './server-timing.js'
@@ -7,6 +8,7 @@ import type { ServerTimingResult } from './server-timing.js'
78
import type { RequestFormatShorthand } from '../types.js'
89
import type { DNSLinkResolveResult, IPNS, IPNSResolveResult, IPNSRoutingEvents, ResolveDNSLinkProgressEvents, ResolveProgressEvents, ResolveResult } from '@helia/ipns'
910
import type { AbortOptions, ComponentLogger, PeerId } from '@libp2p/interface'
11+
import type { Multiaddr } from '@multiformats/multiaddr'
1012
import type { ProgressOptions } from 'progress-events'
1113

1214
const ipnsCache = new TLRU<DNSLinkResolveResult | IPNSResolveResult>(1000)
@@ -50,6 +52,11 @@ export interface ParsedUrlStringResults extends ResolveResult {
5052
* serverTiming items
5153
*/
5254
serverTimings: Array<ServerTimingResult<any>>
55+
56+
/**
57+
* The providers hinted in the URL.
58+
*/
59+
providers: Array<Multiaddr>
5360
}
5461

5562
const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
@@ -286,12 +293,28 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
286293

287294
// parse query string
288295
const query: Record<string, any> = {}
296+
const providers: Array<Multiaddr> = []
289297

290298
if (queryString != null && queryString.length > 0) {
291299
const queryParts = queryString.split('&')
292300
for (const part of queryParts) {
293301
const [key, value] = part.split('=')
294-
query[key] = decodeURIComponent(value)
302+
// see https://github.com/vasco-santos/provider-hinted-uri
303+
// provider is a special case, the parameter MAY be repeated
304+
if (key === 'provider') {
305+
if (query[key] == null) {
306+
query[key] = []
307+
}
308+
const decodedValue = decodeURIComponent(value)
309+
try {
310+
// Must be a multiaddr to be used as Hint
311+
const m = multiaddr(decodedValue)
312+
providers.push(m)
313+
;(query[key] as string[]).push(decodedValue)
314+
} catch {}
315+
} else {
316+
query[key] = decodeURIComponent(value)
317+
}
295318
}
296319

297320
if (query.download != null) {
@@ -310,6 +333,7 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
310333
query,
311334
ttl,
312335
ipfsPath: `/${protocol}/${cidOrPeerIdOrDnsLink}${urlPath != null && urlPath !== '' ? `/${urlPath}` : ''}`,
336+
providers,
313337
serverTimings
314338
} satisfies ParsedUrlStringResults
315339
}

packages/verified-fetch/src/verified-fetch.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { serverTiming } from './utils/server-timing.js'
2626
import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
2727
import type { VerifiedFetchPlugin, PluginContext, PluginOptions } from './plugins/types.js'
2828
import type { ParsedUrlStringResults } from './utils/parse-url-string.js'
29-
import type { Helia, SessionBlockstore } from '@helia/interface'
29+
import type { Helia, SessionBlockstore, ProviderOptions } from '@helia/interface'
3030
import type { IPNS } from '@helia/ipns'
3131
import type { AbortOptions, Logger } from '@libp2p/interface'
3232
import type { Blockstore } from 'interface-blockstore'
@@ -121,7 +121,7 @@ export class VerifiedFetch {
121121
this.log.trace('created VerifiedFetch instance')
122122
}
123123

124-
private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions = {}): Blockstore {
124+
private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions & ProviderOptions = {}): Blockstore {
125125
const key = resourceToSessionCacheKey(resource)
126126
if (!useSession) {
127127
return this.helia.blockstore
@@ -374,7 +374,10 @@ export class VerifiedFetch {
374374
...parsedResult,
375375
resource: resource.toString(),
376376
accept,
377-
options,
377+
options: {
378+
...options,
379+
providers: parsedResult.providers
380+
},
378381
withServerTiming,
379382
onProgress: options?.onProgress,
380383
modified: 0

packages/verified-fetch/test/utils/parse-url-string.spec.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,60 @@ describe('parseUrlString', () => {
151151
}
152152
)
153153
})
154+
155+
it('can parse URL with CID+queryString where query string has providers', async () => {
156+
const providers = [
157+
'/dns4/provider-server.io/tcp/443/https',
158+
'/dns4/provider-server.io/tcp/8000'
159+
]
160+
await assertMatchUrl(
161+
`ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm?format=tar&provider=${providers[0]}&provider=${providers[1]}`, {
162+
protocol: 'ipfs',
163+
cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
164+
path: '',
165+
query: {
166+
format: 'tar',
167+
provider: providers
168+
}
169+
}
170+
)
171+
})
172+
173+
it('can parse URL with CID+path+queryString where query string has providers', async () => {
174+
const providers = [
175+
'/dns4/provider-server.io/tcp/443/https',
176+
'/dns4/provider-server.io/tcp/8000'
177+
]
178+
await assertMatchUrl(
179+
`ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar&provider=${providers[0]}&provider=${providers[1]}`, {
180+
protocol: 'ipfs',
181+
cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
182+
path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt',
183+
query: {
184+
format: 'tar',
185+
provider: providers
186+
}
187+
}
188+
)
189+
})
190+
191+
it('can parse URL with CID+queryString where query string has providers, but one is not valid', async () => {
192+
const providers = [
193+
'/dns4/provider-server.io/tcp/443/https',
194+
'not-a-multiaddr'
195+
]
196+
await assertMatchUrl(
197+
`ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm?format=tar&provider=${providers[0]}&provider=${providers[1]}`, {
198+
protocol: 'ipfs',
199+
cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
200+
path: '',
201+
query: {
202+
format: 'tar',
203+
provider: [providers[0]]
204+
}
205+
}
206+
)
207+
})
154208
})
155209

156210
describe('ipns://<dnsLinkDomain> URLs', () => {
@@ -357,6 +411,42 @@ describe('parseUrlString', () => {
357411
}
358412
)
359413
})
414+
415+
it('can parse an IPFS path with CID+queryString where query string has providers', async () => {
416+
const providers = [
417+
'/dns4/provider-server.io/tcp/443/https',
418+
'/dns4/provider-server.io/tcp/8000'
419+
]
420+
await assertMatchUrl(
421+
`/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm?format=tar&provider=${providers[0]}&provider=${providers[1]}`, {
422+
protocol: 'ipfs',
423+
cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
424+
path: '',
425+
query: {
426+
format: 'tar',
427+
provider: providers
428+
}
429+
}
430+
)
431+
})
432+
433+
it('can parse an IPFS path with CID+path+queryString where query string has providers', async () => {
434+
const providers = [
435+
'/dns4/provider-server.io/tcp/443/https',
436+
'/dns4/provider-server.io/tcp/8000'
437+
]
438+
await assertMatchUrl(
439+
`/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar&provider=${providers[0]}&provider=${providers[1]}`, {
440+
protocol: 'ipfs',
441+
cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
442+
path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt',
443+
query: {
444+
format: 'tar',
445+
provider: providers
446+
}
447+
}
448+
)
449+
})
360450
})
361451

362452
describe('http://example.com/ipfs/<CID> URLs', () => {
@@ -407,6 +497,42 @@ describe('parseUrlString', () => {
407497
}
408498
)
409499
})
500+
501+
it('can parse an IPFS Gateway URL with CID+queryString where query string has providers', async () => {
502+
const providers = [
503+
'/dns4/provider-server.io/tcp/443/https',
504+
'/dns4/provider-server.io/tcp/8000'
505+
]
506+
await assertMatchUrl(
507+
`http://example.com/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm?format=tar&provider=${providers[0]}&provider=${providers[1]}`, {
508+
protocol: 'ipfs',
509+
cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
510+
path: '',
511+
query: {
512+
format: 'tar',
513+
provider: providers
514+
}
515+
}
516+
)
517+
})
518+
519+
it('can parse an IPFS Gateway URL with CID+path+queryString where query string has providers', async () => {
520+
const providers = [
521+
'/dns4/provider-server.io/tcp/443/https',
522+
'/dns4/provider-server.io/tcp/8000'
523+
]
524+
await assertMatchUrl(
525+
`http://example.com/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar&provider=${providers[0]}&provider=${providers[1]}`, {
526+
protocol: 'ipfs',
527+
cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
528+
path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt',
529+
query: {
530+
format: 'tar',
531+
provider: providers
532+
}
533+
}
534+
)
535+
})
410536
})
411537

412538
describe('http://<CID>.ipfs.example.com URLs', () => {
@@ -457,6 +583,42 @@ describe('parseUrlString', () => {
457583
}
458584
)
459585
})
586+
587+
it('can parse an IPFS Subdomain Gateway URL with CID+queryString where query string has providers', async () => {
588+
const providers = [
589+
'/dns4/provider-server.io/tcp/443/https',
590+
'/dns4/provider-server.io/tcp/8000'
591+
]
592+
await assertMatchUrl(
593+
`http://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm.ipfs.example.com?format=tar&provider=${providers[0]}&provider=${providers[1]}`, {
594+
protocol: 'ipfs',
595+
cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
596+
path: '',
597+
query: {
598+
format: 'tar',
599+
provider: providers
600+
}
601+
}
602+
)
603+
})
604+
605+
it('can parse an IPFS Subdomain Gateway URL with CID+path+queryString where query string has providers', async () => {
606+
const providers = [
607+
'/dns4/provider-server.io/tcp/443/https',
608+
'/dns4/provider-server.io/tcp/8000'
609+
]
610+
await assertMatchUrl(
611+
`http://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm.ipfs.example.com/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar&provider=${providers[0]}&provider=${providers[1]}`, {
612+
protocol: 'ipfs',
613+
cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
614+
path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt',
615+
query: {
616+
format: 'tar',
617+
provider: providers
618+
}
619+
}
620+
)
621+
})
460622
})
461623

462624
describe('ipns://<peerId> URLs', () => {

0 commit comments

Comments
 (0)