1+ import { DEFAULT_SESSION_MIN_PROVIDERS , DEFAULT_SESSION_MAX_PROVIDERS , DEFAULT_SESSION_PROVIDER_QUERY_CONCURRENCY , DEFAULT_SESSION_PROVIDER_QUERY_TIMEOUT } from '@helia/interface'
2+ import { PeerQueue } from '@libp2p/utils/peer-queue'
3+ import { isPrivateIp } from '@libp2p/utils/private-ip'
4+ import { DNS , HTTP , HTTPS } from '@multiformats/multiaddr-matcher'
5+ import { multiaddrToUri } from '@multiformats/multiaddr-to-uri'
6+ import pDefer from 'p-defer'
17import { TrustlessGateway } from './trustless-gateway.js'
28import { DEFAULT_TRUSTLESS_GATEWAYS } from './index.js'
39import type { TrustlessGatewayBlockBrokerInit , TrustlessGatewayComponents , TrustlessGatewayGetBlockProgressEvents } from './index.js'
4- import type { BlockRetrievalOptions , BlockBroker } from '@helia/interface/blocks '
10+ import type { Routing , BlockRetrievalOptions , BlockBroker , CreateSessionOptions } from '@helia/interface'
511import type { Logger } from '@libp2p/interface'
612import type { CID } from 'multiformats/cid'
713
14+ export interface CreateTrustlessGatewaySessionOptions extends CreateSessionOptions < TrustlessGatewayGetBlockProgressEvents > {
15+ /**
16+ * Specify the cache control header to send to the remote. 'only-if-cached'
17+ * will prevent the gateway from fetching the content if they don't have it.
18+ *
19+ * @default only-if-cached
20+ */
21+ cacheControl ?: string
22+
23+ /**
24+ * By default we will only connect to peers with HTTPS addresses, pass true
25+ * to also connect to HTTP addresses.
26+ *
27+ * @default false
28+ */
29+ allowInsecure ?: boolean
30+
31+ /**
32+ * By default we will only connect to peers with public or DNS addresses, pass
33+ * true to also connect to private addresses.
34+ *
35+ * @default false
36+ */
37+ allowLocal ?: boolean
38+ }
39+
840/**
941 * A class that accepts a list of trustless gateways that are queried
1042 * for blocks.
1143 */
1244export class TrustlessGatewayBlockBroker implements BlockBroker < TrustlessGatewayGetBlockProgressEvents > {
45+ private readonly components : TrustlessGatewayComponents
1346 private readonly gateways : TrustlessGateway [ ]
47+ private readonly routing : Routing
1448 private readonly log : Logger
1549
1650 constructor ( components : TrustlessGatewayComponents , init : TrustlessGatewayBlockBrokerInit = { } ) {
51+ this . components = components
1752 this . log = components . logger . forComponent ( 'helia:trustless-gateway-block-broker' )
53+ this . routing = components . routing
1854 this . gateways = ( init . gateways ?? DEFAULT_TRUSTLESS_GATEWAYS )
1955 . map ( ( gatewayOrUrl ) => {
20- return new TrustlessGateway ( gatewayOrUrl )
56+ return new TrustlessGateway ( gatewayOrUrl , components . logger )
2157 } )
2258 }
2359
60+ addGateway ( gatewayOrUrl : string ) : void {
61+ this . gateways . push ( new TrustlessGateway ( gatewayOrUrl , this . components . logger ) )
62+ }
63+
2464 async retrieve ( cid : CID , options : BlockRetrievalOptions < TrustlessGatewayGetBlockProgressEvents > = { } ) : Promise < Uint8Array > {
2565 // Loop through the gateways until we get a block or run out of gateways
2666 // TODO: switch to toSorted when support is better
@@ -38,7 +78,7 @@ export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGateway
3878 this . log . error ( 'failed to validate block for %c from %s' , cid , gateway . url , err )
3979 gateway . incrementInvalidBlocks ( )
4080
41- throw new Error ( `unable to validate block for CID ${ cid } from gateway ${ gateway . url } ` )
81+ throw new Error ( `Block for CID ${ cid } from gateway ${ gateway . url } failed validation ` )
4282 }
4383
4484 return block
@@ -47,7 +87,7 @@ export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGateway
4787 if ( err instanceof Error ) {
4888 aggregateErrors . push ( err )
4989 } else {
50- aggregateErrors . push ( new Error ( `unable to fetch raw block for CID ${ cid } from gateway ${ gateway . url } ` ) )
90+ aggregateErrors . push ( new Error ( `Unable to fetch raw block for CID ${ cid } from gateway ${ gateway . url } ` ) )
5191 }
5292 // if signal was aborted, exit the loop
5393 if ( options . signal ?. aborted === true ) {
@@ -57,6 +97,106 @@ export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGateway
5797 }
5898 }
5999
60- throw new AggregateError ( aggregateErrors , `unable to fetch raw block for CID ${ cid } from any gateway` )
100+ if ( aggregateErrors . length > 0 ) {
101+ throw new AggregateError ( aggregateErrors , `Unable to fetch raw block for CID ${ cid } from any gateway` )
102+ } else {
103+ throw new Error ( `Unable to fetch raw block for CID ${ cid } from any gateway` )
104+ }
105+ }
106+
107+ async createSession ( root : CID , options : CreateTrustlessGatewaySessionOptions = { } ) : Promise < BlockBroker < TrustlessGatewayGetBlockProgressEvents > > {
108+ const gateways : string [ ] = [ ]
109+ const minProviders = options . minProviders ?? DEFAULT_SESSION_MIN_PROVIDERS
110+ const maxProviders = options . minProviders ?? DEFAULT_SESSION_MAX_PROVIDERS
111+ const deferred = pDefer < BlockBroker < TrustlessGatewayGetBlockProgressEvents > > ( )
112+ const broker = new TrustlessGatewayBlockBroker ( this . components , {
113+ gateways
114+ } )
115+
116+ this . log ( 'finding transport-ipfs-gateway-http providers for cid %c' , root )
117+
118+ const queue = new PeerQueue ( {
119+ concurrency : options . providerQueryConcurrency ?? DEFAULT_SESSION_PROVIDER_QUERY_CONCURRENCY
120+ } )
121+
122+ Promise . resolve ( ) . then ( async ( ) => {
123+ for await ( const provider of this . routing . findProviders ( root , options ) ) {
124+ const httpAddresses = provider . multiaddrs . filter ( ma => {
125+ if ( HTTPS . matches ( ma ) || ( options . allowInsecure === true && HTTP . matches ( ma ) ) ) {
126+ if ( options . allowLocal === true ) {
127+ return true
128+ }
129+
130+ if ( DNS . matches ( ma ) ) {
131+ return true
132+ }
133+
134+ return isPrivateIp ( ma . toOptions ( ) . host ) === false
135+ }
136+
137+ return false
138+ } )
139+
140+ if ( httpAddresses . length === 0 ) {
141+ continue
142+ }
143+
144+ this . log ( 'found transport-ipfs-gateway-http provider %p for cid %c' , provider . id , root )
145+
146+ void queue . add ( async ( ) => {
147+ for ( const ma of httpAddresses ) {
148+ let uri : string | undefined
149+
150+ try {
151+ // /ip4/x.x.x.x/tcp/31337/http
152+ // /ip4/x.x.x.x/tcp/31337/https
153+ // etc
154+ uri = multiaddrToUri ( ma )
155+
156+ const resource = `${ uri } /ipfs/${ root . toString ( ) } ?format=raw`
157+
158+ // make sure the peer is available - HEAD support doesn't seem to
159+ // be very widely implemented so as long as the remote responds
160+ // we are happy they are valid
161+ // https://specs.ipfs.tech/http-gateways/trustless-gateway/#head-ipfs-cid-path-params
162+
163+ // in the future we should be able to request `${uri}/.well-known/libp2p-http
164+ // and discover an IPFS gateway from $.protocols['/ipfs/gateway'].path
165+ // in the response
166+ // https://github.com/libp2p/specs/pull/508/files
167+ const response = await fetch ( resource , {
168+ method : 'HEAD' ,
169+ headers : {
170+ Accept : 'application/vnd.ipld.raw' ,
171+ 'Cache-Control' : options . cacheControl ?? 'only-if-cached'
172+ } ,
173+ signal : AbortSignal . timeout ( options . providerQueryTimeout ?? DEFAULT_SESSION_PROVIDER_QUERY_TIMEOUT )
174+ } )
175+
176+ this . log ( 'HEAD %s %d' , resource , response . status )
177+ gateways . push ( uri )
178+ broker . addGateway ( uri )
179+
180+ this . log ( 'found %d transport-ipfs-gateway-http providers for cid %c' , gateways . length , root )
181+
182+ if ( gateways . length === minProviders ) {
183+ deferred . resolve ( broker )
184+ }
185+
186+ if ( gateways . length === maxProviders ) {
187+ queue . clear ( )
188+ }
189+ } catch ( err : any ) {
190+ this . log . error ( 'could not fetch %c from %a' , root , uri ?? ma , err )
191+ }
192+ }
193+ } )
194+ }
195+ } )
196+ . catch ( err => {
197+ this . log . error ( 'error creating session for %c' , root , err )
198+ } )
199+
200+ return deferred . promise
61201 }
62202}
0 commit comments