From 32ca87f74840410a435412da64c8e22208fa2ec2 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:25:22 -0700 Subject: [PATCH] fix: unixfs dir redirect (#33) * fix: check redirect trailing slash if path is empty * test: add interop test for unixfs directory redirect * chore: add gateway-conformance-fixtures car file * test: add verified-fetch unit test for root directory redirect --- .../data/gateway-conformance-fixtures.car | Bin 0 -> 468 bytes packages/interop/src/unixfs-dir.spec.ts | 19 ++++++++++++ packages/verified-fetch/src/verified-fetch.ts | 3 +- .../test/verified-fetch.spec.ts | 27 ++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/interop/src/fixtures/data/gateway-conformance-fixtures.car diff --git a/packages/interop/src/fixtures/data/gateway-conformance-fixtures.car b/packages/interop/src/fixtures/data/gateway-conformance-fixtures.car new file mode 100644 index 0000000000000000000000000000000000000000..43e570e1d6acb0a03531b47feb540318f5027204 GIT binary patch literal 468 zcmcColvx?P5~W2SVzij7-;r*+x4)dz{ z2?vnLaUeGn)L{a2bEuF)#Kf&zm9F0B$mRdAQQ-H@OVV5Z6eh1XxzN { await verifiedFetch.stop() }) + describe('unixfs-dir-redirect', () => { + before(async () => { + await loadFixtureDataCar(controller, 'gateway-conformance-fixtures.car') + }); + + [ + 'https://example.com/ipfs/bafybeifq2rzpqnqrsdupncmkmhs3ckxxjhuvdcbvydkgvch3ms24k5lo7q', + 'ipfs://bafybeifq2rzpqnqrsdupncmkmhs3ckxxjhuvdcbvydkgvch3ms24k5lo7q', + 'http://example.com/ipfs/bafybeifq2rzpqnqrsdupncmkmhs3ckxxjhuvdcbvydkgvch3ms24k5lo7q' + ].forEach((url: string) => { + it(`request to unixfs directory with ${url} should return a 301 with a trailing slash`, async () => { + const response = await verifiedFetch(url, { redirect: 'manual' }) + expect(response).to.be.ok() + expect(response.status).to.equal(301) + expect(response.headers.get('location')).to.equal(`${url}/`) + }) + }) + }) + describe('XKCD Barrel Part 1', () => { before(async () => { // This is the content of https://explore.ipld.io/#/explore/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1%20-%20Barrel%20-%20Part%201 diff --git a/packages/verified-fetch/src/verified-fetch.ts b/packages/verified-fetch/src/verified-fetch.ts index 4166d914..304ad6f6 100644 --- a/packages/verified-fetch/src/verified-fetch.ts +++ b/packages/verified-fetch/src/verified-fetch.ts @@ -299,9 +299,10 @@ export class VerifiedFetch { let resolvedCID = terminalElement?.cid ?? cid if (terminalElement?.type === 'directory') { const dirCid = terminalElement.cid + const redirectCheckNeeded = path === '' ? !resource.toString().endsWith('/') : !path.endsWith('/') // https://specs.ipfs.tech/http-gateways/path-gateway/#use-in-directory-url-normalization - if (path !== '' && !path.endsWith('/')) { + if (redirectCheckNeeded) { if (options?.redirect === 'error') { this.log('could not redirect to %s/ as redirect option was set to "error"', resource) throw new TypeError('Failed to fetch') diff --git a/packages/verified-fetch/test/verified-fetch.spec.ts b/packages/verified-fetch/test/verified-fetch.spec.ts index bf0263af..64cbc84e 100644 --- a/packages/verified-fetch/test/verified-fetch.spec.ts +++ b/packages/verified-fetch/test/verified-fetch.spec.ts @@ -160,6 +160,33 @@ describe('@helia/verifed-fetch', () => { expect(ipfsResponse.url).to.equal(`ipfs://${res.cid}/foo`) }) + it('should return a 301 with a trailing slash when a root directory is requested without a trailing slash', async () => { + const finalRootFileContent = new Uint8Array([0x01, 0x02, 0x03]) + + const fs = unixfs(helia) + const res = await last(fs.addAll([{ + path: '/index.html', + content: finalRootFileContent + }], { + wrapWithDirectory: true + })) + + if (res == null) { + throw new Error('Import failed') + } + + const stat = await fs.stat(res.cid) + expect(stat.type).to.equal('directory') + + const ipfsResponse = await verifiedFetch.fetch(`https://ipfs.local/ipfs/${res.cid}`, { + redirect: 'manual' + }) + expect(ipfsResponse).to.be.ok() + expect(ipfsResponse.status).to.equal(301) + expect(ipfsResponse.headers.get('location')).to.equal(`https://ipfs.local/ipfs/${res.cid}/`) + expect(ipfsResponse.url).to.equal(`https://ipfs.local/ipfs/${res.cid}`) + }) + it('should return a 301 with a trailing slash when a gateway directory is requested without a trailing slash', async () => { const finalRootFileContent = new Uint8Array([0x01, 0x02, 0x03])