Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add explicit support for subdomain gateways #439

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/block-brokers/src/trustless-gateway/broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ ProgressOptions<TrustlessGatewayGetBlockProgressEvents>
constructor (components: TrustlessGatewayComponents, init: TrustlessGatewayBlockBrokerInit = {}) {
this.log = components.logger.forComponent('helia:trustless-gateway-block-broker')
this.gateways = (init.gateways ?? DEFAULT_TRUSTLESS_GATEWAYS)
.map((gatewayOrUrl) => {
return new TrustlessGateway(gatewayOrUrl)
.map((gw) => {
return new TrustlessGateway(gw.url, gw.isSubdomain)
})
}

Expand Down
21 changes: 13 additions & 8 deletions packages/block-brokers/src/trustless-gateway/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@ import type { BlockRetriever } from '@helia/interface/src/blocks.js'
import type { ComponentLogger } from '@libp2p/interface'
import type { ProgressEvent } from 'progress-events'

export const DEFAULT_TRUSTLESS_GATEWAYS = [
// 2023-10-03: IPNS, Origin, and Block/CAR support from https://ipfs-public-gateway-checker.on.fleek.co/
'https://trustless-gateway.link',
export const DEFAULT_TRUSTLESS_GATEWAYS: TrustlessGatewayUrl[] = [
// 2024-02-20: IPNS and Block/CAR support from https://ipfs.github.io/public-gateway-checker/
{ url: 'https://trustless-gateway.link', isSubdomain: false },

// 2023-10-03: IPNS, Origin, and Block/CAR support from https://ipfs-public-gateway-checker.on.fleek.co/
'https://cloudflare-ipfs.com',
// 2024-02-20: IPNS and Block/CAR support from https://ipfs.github.io/public-gateway-checker/
{ url: 'https://cloudflare-ipfs.com', isSubdomain: false },

// 2023-10-03: IPNS, Origin, and Block/CAR support from https://ipfs-public-gateway-checker.on.fleek.co/
'https://4everland.io'
// 2024-02-20: IPNS, Origin, and Block/CAR support from https://ipfs.github.io/public-gateway-checker/
{ url: 'https://4everland.io', isSubdomain: true },
]

interface TrustlessGatewayUrl {
url: string | URL
isSubdomain: boolean
}

export type TrustlessGatewayGetBlockProgressEvents =
ProgressEvent<'trustless-gateway:get-block:fetch', URL>

export interface TrustlessGatewayBlockBrokerInit {
gateways?: Array<string | URL>
gateways?: Array<TrustlessGatewayUrl>
2color marked this conversation as resolved.
Show resolved Hide resolved
}

export interface TrustlessGatewayComponents {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CID } from 'multiformats/cid'
import { base32 } from 'multiformats/bases/base32'

/**
* A `TrustlessGateway` keeps track of the number of attempts, errors, and
Expand All @@ -8,6 +9,12 @@ import type { CID } from 'multiformats/cid'
*/
export class TrustlessGateway {
public readonly url: URL

/**
* Whether this gateway is a subdomain resolution style gateway
*/
public isSubdomain: boolean

/**
* The number of times this gateway has been attempted to be used to fetch a
* block. This includes successful, errored, and aborted attempts. By counting
Expand Down Expand Up @@ -36,34 +43,36 @@ export class TrustlessGateway {
*/
#successes = 0

constructor (url: URL | string) {
constructor(url: URL | string, isSubdomain: boolean = false) {
2color marked this conversation as resolved.
Show resolved Hide resolved
this.url = url instanceof URL ? url : new URL(url)
this.isSubdomain = isSubdomain
}

/**
* Fetch a raw block from `this.url` following the specification defined at
* https://specs.ipfs.tech/http-gateways/trustless-gateway/
*/
async getRawBlock (cid: CID, signal?: AbortSignal): Promise<Uint8Array> {
const gwUrl = this.url
gwUrl.pathname = `/ipfs/${cid.toString()}`
async getRawBlock(cid: CID, signal?: AbortSignal): Promise<Uint8Array> {
const gwUrl = this.getGwUrl(cid)

// necessary as not every gateway supports dag-cbor, but every should support
// sending raw block as-is
gwUrl.search = '?format=raw'

if (signal?.aborted === true) {
throw new Error(`Signal to fetch raw block for CID ${cid} from gateway ${this.url} was aborted prior to fetch`)
throw new Error(
`Signal to fetch raw block for CID ${cid} from gateway ${this.url} was aborted prior to fetch`,
)
2color marked this conversation as resolved.
Show resolved Hide resolved
}

try {
this.#attempts++
const res = await fetch(gwUrl.toString(), {
signal,
headers: {
// also set header, just in case ?format= is filtered out by some
// reverse proxy
Accept: 'application/vnd.ipld.raw'
// also set header, just in case ?format= is filtered out by some
// reverse proxy
Accept: 'application/vnd.ipld.raw',
2color marked this conversation as resolved.
Show resolved Hide resolved
},
cache: 'force-cache'
})
Expand All @@ -84,6 +93,20 @@ export class TrustlessGateway {
}
}

/**
* Construct the Gateway URL for a CID
*/
getGwUrl(cid: CID): URL {
const gwUrl = new URL(this.url)

if (this.isSubdomain) {
gwUrl.hostname = `${cid.toString(base32)}.ipfs.${gwUrl.hostname}`
} else {
gwUrl.pathname = `/ipfs/${cid.toString()}`
}
return gwUrl
}

/**
* Encapsulate the logic for determining whether a gateway is considered
* reliable, for prioritization. This is based on the number of successful attempts made
Expand Down
15 changes: 14 additions & 1 deletion packages/block-brokers/test/trustless-gateway.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('trustless-gateway-block-broker', () => {

gateways = [
stubConstructor(TrustlessGateway, 'http://localhost:8080'),
stubConstructor(TrustlessGateway, 'http://localhost:8081'),
stubConstructor(TrustlessGateway, 'http://localhost:8081', true),
stubConstructor(TrustlessGateway, 'http://localhost:8082'),
stubConstructor(TrustlessGateway, 'http://localhost:8083')
]
Expand Down Expand Up @@ -150,4 +150,17 @@ describe('trustless-gateway-block-broker', () => {
expect(gateways[1].getRawBlock.calledWith(cid1, Sinon.match.any)).to.be.false()
expect(gateways[2].getRawBlock.calledWith(cid1, Sinon.match.any)).to.be.false()
})

it('constructs the gateway url for the cid for both path and subdomain gateways', async () => {
const pathGw = new TrustlessGateway('http://localhost:8080')
const subdomainGw = new TrustlessGateway('https://dweb.link', true)

expect(pathGw.getGwUrl(blocks[0].cid).hostname).to.equal(`localhost`)
expect(pathGw.getGwUrl(blocks[0].cid).toString()).to.equal(`http://localhost:8080/ipfs/bafkreiefnkxuhnq3536qo2i2w3tazvifek4mbbzb6zlq3ouhprjce5c3aq`)
expect(pathGw.getGwUrl(blocks[1].cid).toString()).to.equal(`http://localhost:8080/ipfs/${blocks[1].cid.toString()}`)

expect(subdomainGw.getGwUrl(blocks[0].cid).hostname).to.equal(`bafkreiefnkxuhnq3536qo2i2w3tazvifek4mbbzb6zlq3ouhprjce5c3aq.ipfs.dweb.link`)
expect(subdomainGw.getGwUrl(blocks[0].cid).toString()).to.equal(`https://bafkreiefnkxuhnq3536qo2i2w3tazvifek4mbbzb6zlq3ouhprjce5c3aq.ipfs.dweb.link/`)
expect(subdomainGw.getGwUrl(blocks[1].cid).toString()).to.equal(`https://${blocks[1].cid.toString()}.ipfs.dweb.link/`)
})
})
Loading