Skip to content

Commit 9a4991e

Browse files
committed
feat: support provider query parameter
1 parent 95397aa commit 9a4991e

File tree

6 files changed

+318
-8
lines changed

6 files changed

+318
-8
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import type { ParsedUrlStringResults } from '../utils/parse-url-string.js'
66
import type { PathWalkerResponse } from '../utils/walk-path.js'
77
import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
88
import type { Helia } from 'helia'
9+
import type { ProviderOptions } from '@helia/interface'
910
import type { Blockstore } from 'interface-blockstore'
1011
import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
12+
import type { Multiaddr } from '@multiformats/multiaddr'
1113
import type { CID } from 'multiformats/cid'
1214
import type { CustomProgressEvent } from 'progress-events'
1315

@@ -18,7 +20,7 @@ import type { CustomProgressEvent } from 'progress-events'
1820
*/
1921
export interface PluginOptions {
2022
logger: ComponentLogger
21-
getBlockstore(cid: CID, resource: string | CID, useSession?: boolean, options?: AbortOptions): Blockstore
23+
getBlockstore(cid: CID, resource: string | CID, useSession?: boolean, options?: AbortOptions & ProviderOptions): Blockstore
2224
handleServerTiming<T>(name: string, description: string, fn: () => Promise<T>, withServerTiming: boolean): Promise<T>
2325
contentTypeParser?: ContentTypeParser
2426
helia: Helia
@@ -42,7 +44,7 @@ export interface PluginContext extends ParsedUrlStringResults {
4244
modified: number
4345
withServerTiming?: boolean
4446
onProgress?(evt: CustomProgressEvent<any>): void
45-
options?: Omit<VerifiedFetchInit, 'signal'> & AbortOptions
47+
options?: Omit<VerifiedFetchInit, 'signal'> & AbortOptions & ProviderOptions
4648
isDirectory?: boolean
4749
directoryEntries?: UnixFSEntry[]
4850
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: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CID } from 'multiformats/cid'
2+
import { multiaddr, type Multiaddr } from '@multiformats/multiaddr'
23
import { getPeerIdFromString } from './get-peer-id-from-string.js'
34
import { serverTiming } from './server-timing.js'
45
import { TLRU } from './tlru.js'
@@ -49,6 +50,11 @@ export interface ParsedUrlStringResults extends ResolveResult {
4950
* serverTiming items
5051
*/
5152
serverTimings: Array<ServerTimingResult<any>>
53+
54+
/**
55+
* The providers hinted in the URL.
56+
*/
57+
providers: Array<Multiaddr>
5258
}
5359

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

281287
// parse query string
282288
const query: Record<string, any> = {}
289+
let providers: Array<Multiaddr> = []
283290

284291
if (queryString != null && queryString.length > 0) {
285292
const queryParts = queryString.split('&')
293+
let providersParameters: string[] = []
286294
for (const part of queryParts) {
287295
const [key, value] = part.split('=')
288-
query[key] = decodeURIComponent(value)
296+
// see https://github.com/vasco-santos/provider-hinted-uri
297+
// provider is a special case, the parameter MAY be repeated
298+
if (key === 'provider') {
299+
if (query[key] == null) {
300+
query[key] = []
301+
}
302+
if (Array.isArray(query[key])) {
303+
const decodedValue = decodeURIComponent(value)
304+
try {
305+
// Must be a multiaddr to be used as Hint
306+
const m = multiaddr(decodedValue)
307+
providers.push(m)
308+
;(query[key] as string[]).push(decodedValue)
309+
} catch {
310+
console.warn(`${decodedValue} is not a valid multiaddr`)
311+
}
312+
}
313+
} else {
314+
query[key] = decodeURIComponent(value)
315+
}
289316
}
290317

291318
if (query.download != null) {
@@ -304,6 +331,7 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
304331
query,
305332
ttl,
306333
ipfsPath: `/${protocol}/${cidOrPeerIdOrDnsLink}${urlPath != null && urlPath !== '' ? `/${urlPath}` : ''}`,
334+
providers,
307335
serverTimings
308336
} satisfies ParsedUrlStringResults
309337
}

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

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

123-
private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions = {}): Blockstore {
123+
private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions & ProviderOptions = {}): Blockstore {
124124
const key = resourceToSessionCacheKey(resource)
125125
if (!useSession) {
126126
return this.helia.blockstore
@@ -369,7 +369,10 @@ export class VerifiedFetch {
369369
...parsedResult,
370370
resource: resource.toString(),
371371
accept,
372-
options,
372+
options: {
373+
...options,
374+
providers: parsedResult.providers,
375+
},
373376
withServerTiming,
374377
onProgress: options?.onProgress,
375378
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)