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: handle redirects via option default=true #51

Merged
Merged
Changes from all commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@ $ docker run -it -p 8080:8080 -e DEBUG="helia-http-gateway" helia
| `USE_TRUSTLESS_GATEWAYS` | Whether to fetch content from trustless-gateways or not | `true` |
| `TRUSTLESS_GATEWAYS` | Comma separated list of trusted gateways to fetch content from | [Defined in Helia](https://github.com/ipfs/helia/blob/main/packages/helia/src/block-brokers/trustless-gateway/index.ts) |
| `USE_LIBP2P` | Whether to use libp2p networking | `true` |
| `RESOLVE_REDIRECTS` | Whether to resolve redirects before looking up dnslink entries | `true` |

<!--
TODO: currently broken when used in docker, but they work when running locally (you can cache datastore and blockstore locally to speed things up if you want)
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ export const DEBUG = process.env.DEBUG ?? ''

export const USE_SUBDOMAINS = process.env.USE_SUBDOMAINS === 'true'

export const RESOLVE_REDIRECTS = process.env.RESOLVE_REDIRECTS !== 'false'

/**
* If set to any value other than 'true', we will disable prometheus metrics.
*
50 changes: 48 additions & 2 deletions src/heliaFetch.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,10 @@ interface HeliaPathParts {
relativePath: string
}

interface HeliaFetchConfig {
resolveRedirects: boolean
}

/**
* Fetches files from IPFS or IPNS
*/
@@ -31,6 +35,7 @@ export class HeliaFetch {
private readonly rootFilePatterns: string[]
public node!: Helia
public ready: Promise<void>
private readonly config: HeliaFetchConfig
private readonly ipnsResolutionCache = new LRUCache<string, string>({
max: 10000,
ttl: 1000 * 60 * 60 * 24
@@ -39,11 +44,13 @@ export class HeliaFetch {
constructor ({
node,
rootFilePatterns = ROOT_FILE_PATTERNS,
logger
logger,
config = {}
}: {
node?: Helia
rootFilePatterns?: string[]
logger?: debug.Debugger
config?: Partial<HeliaFetchConfig>
} = {}) {
// setup a logger
if (logger !== undefined) {
@@ -55,6 +62,10 @@ export class HeliaFetch {
if (node !== undefined) {
this.node = node
}
this.config = {
resolveRedirects: true,
...config
}
this.rootFilePatterns = rootFilePatterns
this.ready = this.init()
this.log('Initialized')
@@ -151,17 +162,52 @@ export class HeliaFetch {
}
}

/**
* Checks if a redirect is needed and returns either the original address or the redirect address.
*/
private async checkForRedirect (address: string, options?: Parameters<UnixFS['cat']>[1]): Promise<string> {
if (!this.config.resolveRedirects) {
return address
}
try {
this.log('Checking for redirect of:', address)
const redirectCheckResponse = await fetch(`http://${address}`, { method: 'HEAD', redirect: 'manual', ...options })
if ([301, 302, 307, 308].includes(redirectCheckResponse.status)) {
this.log('Redirect statuscode :', redirectCheckResponse.status)
const redirectText = redirectCheckResponse.headers.get('location')
if (redirectText == null) {
this.log('No location header')
return address
} else {
const redirectUrl = new URL(redirectText)
this.log('Redirect found:', redirectUrl.host)
return redirectUrl.host
}
}
} catch {
// ignore errors on redirect checks
this.log('Error checking for redirect for url')
}
return address
}

/**
* Fetch IPNS content.
*/
private async fetchIpns (address: string, options?: Parameters<UnixFS['cat']>[1]): Promise<AsyncIterable<Uint8Array>> {
if (!this.ipnsResolutionCache.has(address)) {
this.log('Fetching from DNS over HTTP:', address)
const txtRecords = await this.dohResolver.resolveTxt(`_dnslink.${address}`)
const redirectAddress = await this.checkForRedirect(address)
const txtRecords = await this.dohResolver.resolveTxt(`_dnslink.${redirectAddress}`)
const pathEntry = txtRecords.find(([record]) => record.startsWith('dnslink='))
const path = pathEntry?.[0].replace('dnslink=', '')
this.log('Got Path from DNS over HTTP:', path)
this.ipnsResolutionCache.set(address, path ?? 'not-found')
if (redirectAddress !== address) {
this.ipnsResolutionCache.set(redirectAddress, path ?? 'not-found')
}
// we don't do anything with this, but want to fail if it is not a valid path
this.parsePath(path ?? '')
}
if (this.ipnsResolutionCache.get(address) === 'not-found') {
this.log('No Path found:', address)
7 changes: 5 additions & 2 deletions src/heliaServer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type FastifyReply, type FastifyRequest, type RouteGenericInterface } from 'fastify'
import { CID } from 'multiformats'
import { USE_SUBDOMAINS } from './constants.js'
import { USE_SUBDOMAINS, RESOLVE_REDIRECTS } from './constants.js'
import { DEFAULT_MIME_TYPE, parseContentType } from './contentType.js'
import { getCustomHelia } from './getCustomHelia.js'
import { HeliaFetch } from './heliaFetch.js'
@@ -53,7 +53,10 @@ export class HeliaServer {
async init (): Promise<void> {
this.heliaFetch = new HeliaFetch({
logger: this.log,
node: await getCustomHelia()
node: await getCustomHelia(),
config: {
resolveRedirects: RESOLVE_REDIRECTS
}
})
await this.heliaFetch.ready
// eslint-disable-next-line no-console