Skip to content

Commit

Permalink
fix: improve testGatewaySubdomains gwc coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
SgtPooki committed May 25, 2024
1 parent d5e4ecb commit c5b0e92
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 55 deletions.
2 changes: 1 addition & 1 deletion packages/gateway-conformance/src/conformance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ const tests: TestConfig[] = [
skip: [
'TestGatewaySubdomains/.*HTTP_proxy_tunneling_via_CONNECT' // verified fetch should not be doing HTTP proxy tunneling.
],
successRate: 35.44
successRate: 47.26
},
{
name: 'TestUnixFSDirectoryListingOnSubdomainGateway',
Expand Down
48 changes: 20 additions & 28 deletions packages/verified-fetch/src/utils/handle-redirects.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type AbortOptions, type ComponentLogger } from '@libp2p/interface'
import { CodeError, type AbortOptions, type ComponentLogger } from '@libp2p/interface'
import { type VerifiedFetchInit, type Resource } from '../index.js'
import { matchURLString } from './parse-url-string.js'
import type { CID } from 'multiformats/cid'
Expand All @@ -8,11 +8,6 @@ interface GetRedirectResponseOptions {
resource: Resource
options?: Omit<VerifiedFetchInit, 'signal'> & AbortOptions
logger: ComponentLogger

/**
* Only used in testing.
*/
fetch?: typeof globalThis.fetch
}

interface GetSubdomainRedirectOptions extends GetRedirectResponseOptions {
Expand Down Expand Up @@ -58,21 +53,28 @@ export function getSpecCompliantPath (resource: string): string {
/**
* Handles determining if a redirect to subdomain is needed.
*/
export async function getRedirectUrl ({ resource, options, logger, cid, fetch = globalThis.fetch }: GetSubdomainRedirectOptions): Promise<string> {
export async function getRedirectUrl ({ resource, options, logger, cid }: GetSubdomainRedirectOptions): Promise<string> {
const log = logger.forComponent('helia:verified-fetch:get-subdomain-redirect')
const headers = new Headers(options?.headers)
const forwardedHost = headers.get('x-forwarded-host')
const headerHost = headers.get('host')
const forwardedFor = headers.get('x-forwarded-for')

if (forwardedFor == null && forwardedHost == null && headerHost == null) {
log.trace('no redirect info found in headers')
return resource
}
const forwardedProto = headers.get('x-forwarded-proto')

try {
const urlParts = matchURLString(resource)
if (urlParts.cidOrPeerIdOrDnsLink.length > 63) {
if (urlParts.protocol === 'ipfs') {
throw new CodeError('CID incompatible with DNS label length limit of 63', 'DNS_LABEL_INCOMPATIBLE_CID_SUBDOMAIN')
}
throw new CodeError('PeerId or DNSLink incompatible with DNS label length limit of 63', 'DNS_LABEL_INCOMPATIBLE_SUBDOMAIN')
}

if (forwardedHost == null && forwardedProto == null) {
log.trace('no redirect info found in headers')
throw new CodeError('No redirect info found in headers', 'NO_REDIRECT_INFO_FOUND')
}
const reqUrl = new URL(resource)
reqUrl.protocol = forwardedProto ?? reqUrl.protocol
const actualHost = forwardedHost ?? reqUrl.host
const subdomain = `${urlParts.cidOrPeerIdOrDnsLink}.${urlParts.protocol}`
if (actualHost.includes(subdomain)) {
Expand All @@ -93,23 +95,13 @@ export async function getRedirectUrl ({ resource, options, logger, cid, fetch =
log.trace('request was for a subdomain already. Returning requested resource.')
return resource
}
// try to query subdomain with HEAD request to see if it's supported
try {
const subdomainTest = await fetch(subdomainUrl, { method: 'HEAD' })
if (subdomainTest.ok) {
log('subdomain supported, redirecting to subdomain')
return subdomainUrl.toString()
} else {
log('subdomain not supported, subdomain failed with status %s %s', subdomainTest.status, subdomainTest.statusText)
throw new Error('subdomain not supported')
}
} catch (err: any) {
log('subdomain not supported', err)

return resource
}
} catch (err) {
return subdomainUrl.toString()
} catch (err: any) {
log.error('error while checking for subdomain support', err)
if (err.code != null) {
throw err
}
}

return resource
Expand Down
6 changes: 5 additions & 1 deletion packages/verified-fetch/src/verified-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ export class VerifiedFetch {
* TODO: move operations called by fetch to a queue of operations where we can
* always exit early (and cleanly) if a given signal is aborted
*/
// eslint-disable-next-line complexity
async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
this.log('fetch %s', resource)

Expand Down Expand Up @@ -531,7 +532,10 @@ export class VerifiedFetch {
this.log.trace('returning 301 permanent redirect to %s', redirectUrl)
return movedPermanentlyResponse(resource.toString(), redirectUrl)
}
} catch {
} catch (err: any) {
if (err.code.startsWith('DNS_LABEL_INCOMPATIBLE') === true) {
return badRequestResponse(resource, err)
}
// ignore
}
}
Expand Down
50 changes: 25 additions & 25 deletions packages/verified-fetch/test/utils/handle-redirects.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { prefixLogger } from '@libp2p/logger'
import { expect } from 'aegir/chai'
import { CID } from 'multiformats/cid'
import Sinon from 'sinon'
import { getRedirectUrl, getSpecCompliantPath } from '../../src/utils/handle-redirects.js'

const logger = prefixLogger('test:handle-redirects')

describe('handle-redirects', () => {
const sandbox = Sinon.createSandbox()
const cid = CID.parse('bafkqabtimvwgy3yk')

let fetchStub: Sinon.SinonStub

describe('getSpecCompliantPath', () => {
// the below are all assuming the above identity CID is a unixFS directory CID
it('should handle ipfs:// urls', () => {
Expand All @@ -36,40 +32,44 @@ describe('handle-redirects', () => {
})

describe('getRedirectUrl', () => {
beforeEach(() => {
fetchStub = sandbox.stub(globalThis, 'fetch')
})

afterEach(() => {
sandbox.restore()
})
it('returns path gateway url if HEAD fetch fails', async () => {
it('returns path gateway url if headers is empty', async () => {
const resource = 'http://ipfs.io/ipfs/bafkqabtimvwgy3yk'
const options = { headers: new Headers({ host: 'ipfs.io' }) }
fetchStub.returns(Promise.resolve(new Response(null, { status: 404 })))
const options = { headers: new Headers() }

const url = await getRedirectUrl({ resource, options, logger, cid, fetch: fetchStub })
expect(fetchStub.calledOnce).to.be.true()
const url = await getRedirectUrl({ resource, options, logger, cid })
expect(url).to.equal('http://ipfs.io/ipfs/bafkqabtimvwgy3yk')
})
it('returns subdomain gateway url if HEAD fetch is successful', async () => {

it('returns subdomain gateway url if host is passed', async () => {
const resource = 'http://ipfs.io/ipfs/bafkqabtimvwgy3yk'
const options = { headers: new Headers({ host: 'ipfs.io' }) }
fetchStub.returns(Promise.resolve(new Response(null, { status: 200 })))

const url = await getRedirectUrl({ resource, options, logger, cid, fetch: fetchStub })
expect(fetchStub.calledOnce).to.be.true()
const url = await getRedirectUrl({ resource, options, logger, cid })
expect(url).to.equal('http://bafkqabtimvwgy3yk.ipfs.ipfs.io/')
})

it('returns subdomain gateway url if x-forwarded-host is passed', async () => {
const resource = 'http://ipfs.io/ipfs/bafkqabtimvwgy3yk'
const options = { headers: new Headers({ 'x-forwarded-host': 'dweb.link' }) }

const url = await getRedirectUrl({ resource, options, logger, cid })
expect(url).to.equal('http://bafkqabtimvwgy3yk.ipfs.dweb.link/')
})

it('returns https subdomain gateway url if proto & host are passed', async () => {
const resource = 'http://ipfs.io/ipfs/bafkqabtimvwgy3yk'
const options = { headers: new Headers({ host: 'ipfs.io', 'x-forwarded-proto': 'https' }) }

const url = await getRedirectUrl({ resource, options, logger, cid })
expect(url).to.equal('https://bafkqabtimvwgy3yk.ipfs.ipfs.io/')
})

it('returns the given subdomain gateway url given a subdomain gateway url', async () => {
const resource = 'http://bafkqabtimvwgy3yk.ipfs.inbrowser.dev'
const resource = 'https://bafkqabtimvwgy3yk.ipfs.inbrowser.dev'
const options = { headers: new Headers({ host: 'bafkqabtimvwgy3yk.ipfs.inbrowser.dev' }) }
fetchStub.returns(Promise.resolve(new Response(null, { status: 200 })))

const url = await getRedirectUrl({ resource, options, logger, cid, fetch: fetchStub })
expect(fetchStub.calledOnce).to.be.false()
expect(url).to.equal('http://bafkqabtimvwgy3yk.ipfs.inbrowser.dev')
const url = await getRedirectUrl({ resource, options, logger, cid })
expect(url).to.equal('https://bafkqabtimvwgy3yk.ipfs.inbrowser.dev')
})
})
})

0 comments on commit c5b0e92

Please sign in to comment.