From e7f18165937e3eb9b034c60cd7ed4db22801a022 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Sun, 3 Mar 2024 00:02:16 +0000 Subject: [PATCH] feat: support IPFS/IPNS paths, Gateways, etc (#4) * feat: support IPFS/IPNS paths, Gateways, etc Adds support for fetching resources in the form: - IPFS Path, e.g. `/ipfs/` - IPNS Path, e.g. `/ipns/` - IPFS Gateway Path, e.g. `http://example.com/ipfs/` - IPNS Gateway Path, e.g. `http://example.com/ipns/` - IPFS Subdomain Gateway Path, e.g. `http://.ipfs.example.com` - IPNS Subdomain Gateway Path, e.g. `http://.ipns.example.com` * chore: simplify regex and test https urls too * chore: apply suggestions from code review Co-authored-by: Daniel Norman <1992255+2color@users.noreply.github.com> * chore: fix tests * chore: more tests * chore: remove $ * chore: https? test group name --------- Co-authored-by: Daniel Norman <1992255+2color@users.noreply.github.com> Co-authored-by: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> --- .../src/utils/parse-url-string.ts | 25 +- .../test/utils/parse-url-string.spec.ts | 747 ++++++++++++++---- 2 files changed, 625 insertions(+), 147 deletions(-) diff --git a/packages/verified-fetch/src/utils/parse-url-string.ts b/packages/verified-fetch/src/utils/parse-url-string.ts index 1d20f21..115b250 100644 --- a/packages/verified-fetch/src/utils/parse-url-string.ts +++ b/packages/verified-fetch/src/utils/parse-url-string.ts @@ -30,7 +30,22 @@ export interface ParsedUrlStringResults { query: ParsedUrlQuery } -const URL_REGEX = /^(?ip[fn]s):\/\/(?[^/$?]+)\/?(?[^$?]*)\??(?.*)$/ +const URL_REGEX = /^(?ip[fn]s):\/\/(?[^/?]+)\/?(?[^?]*)\??(?.*)$/ +const PATH_REGEX = /^\/(?ip[fn]s)\/(?[^/?]+)\/?(?[^?]*)\??(?.*)$/ +const PATH_GATEWAY_REGEX = /^https?:\/\/(.*[^/])\/(?ip[fn]s)\/(?[^/?]+)\/?(?[^?]*)\??(?.*)$/ +const SUBDOMAIN_GATEWAY_REGEX = /^https?:\/\/(?[^/.?]+)\.(?ip[fn]s)\.([^/?]+)\/?(?[^?]*)\??(?.*)$/ + +function matchURLString (urlString: string): Record { + for (const pattern of [URL_REGEX, PATH_REGEX, PATH_GATEWAY_REGEX, SUBDOMAIN_GATEWAY_REGEX]) { + const match = urlString.match(pattern) + + if (match?.groups != null) { + return match.groups + } + } + + throw new TypeError(`Invalid URL: ${urlString}, please use ipfs://, ipns://, or gateway URLs only`) +} /** * A function that parses ipfs:// and ipns:// URLs, returning an object with easily recognizable properties. @@ -41,13 +56,7 @@ const URL_REGEX = /^(?ip[fn]s):\/\/(?[^/$?]+)\/? */ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise { const log = logger.forComponent('helia:verified-fetch:parse-url-string') - const match = urlString.match(URL_REGEX) - - if (match == null || match.groups == null) { - throw new TypeError(`Invalid URL: ${urlString}, please use ipfs:// or ipns:// URLs only.`) - } - - const { protocol, cidOrPeerIdOrDnsLink, path: urlPath, queryString } = match.groups + const { protocol, cidOrPeerIdOrDnsLink, path: urlPath, queryString } = matchURLString(urlString) let cid: CID | undefined let resolvedPath: string | undefined diff --git a/packages/verified-fetch/test/utils/parse-url-string.spec.ts b/packages/verified-fetch/test/utils/parse-url-string.spec.ts index a77ff43..454fc1c 100644 --- a/packages/verified-fetch/test/utils/parse-url-string.spec.ts +++ b/packages/verified-fetch/test/utils/parse-url-string.spec.ts @@ -9,10 +9,31 @@ import type { IPNS } from '@helia/ipns' import type { ComponentLogger, PeerId } from '@libp2p/interface' import type { StubbedInstance } from 'sinon-ts' +const HTTP_PROTOCOLS = [ + 'http', + 'https' +] + describe('parseUrlString', () => { let logger: ComponentLogger let ipns: StubbedInstance + /** + * Assert that the passed url is matched to the passed protocol, cid, etc + */ + async function assertMatchUrl (urlString: string, match: { protocol: string, cid: string, path: string, query: any }): Promise { + const result = await parseUrlString({ + urlString, + ipns, + logger + }) + + expect(result.protocol).to.equal(match.protocol) + expect(result.cid.toString()).to.equal(match.cid) + expect(result.path).to.equal(match.path) + expect(result.query).to.deep.equal(match.query) + } + beforeEach(() => { logger = defaultLogger() ipns = stubInterface() @@ -27,7 +48,7 @@ describe('parseUrlString', () => { logger }) ).to.eventually.be.rejected - .with.property('message', 'Invalid URL: invalid, please use ipfs:// or ipns:// URLs only.') + .with.property('message', 'Invalid URL: invalid, please use ipfs://, ipns://, or gateway URLs only') }) it('throws for invalid protocols', async () => { @@ -38,7 +59,7 @@ describe('parseUrlString', () => { logger }) ).to.eventually.be.rejected - .with.property('message', 'Invalid URL: invalid, please use ipfs:// or ipns:// URLs only.') + .with.property('message', 'Invalid URL: invalid, please use ipfs://, ipns://, or gateway URLs only') }) it('throws an error if resulting CID is invalid', async () => { @@ -69,49 +90,64 @@ describe('parseUrlString', () => { }) it('can parse a URL with CID only', async () => { - const result = await parseUrlString({ - urlString: 'ipfs://QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', - ipns, - logger - }) - expect(result.protocol).to.equal('ipfs') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('') + await assertMatchUrl( + 'ipfs://QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', { + protocol: 'ipfs', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: '', + query: {} + } + ) }) it('can parse URL with CID+path', async () => { - const result = await parseUrlString({ - urlString: 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', - ipns, - logger - }) - expect(result.protocol).to.equal('ipfs') - expect(result.cid.toString()).to.equal('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') - expect(result.path).to.equal('1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt') + await assertMatchUrl( + 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: {} + } + ) }) it('can parse URL with CID+queryString', async () => { - const result = await parseUrlString({ - urlString: 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm?format=car', - ipns, - logger - }) - expect(result.protocol).to.equal('ipfs') - expect(result.cid.toString()).to.equal('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') - expect(result.path).to.equal('') - expect(result.query).to.deep.equal({ format: 'car' }) + await assertMatchUrl( + 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm?format=car', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '', + query: { + format: 'car' + } + } + ) + }) + + it('can parse URL with CID, trailing slash and queryString', async () => { + await assertMatchUrl( + 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/?format=car', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '', + query: { + format: 'car' + } + } + ) }) it('can parse URL with CID+path+queryString', async () => { - const result = await parseUrlString({ - urlString: 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar', - ipns, - logger - }) - expect(result.protocol).to.equal('ipfs') - expect(result.cid.toString()).to.equal('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') - expect(result.path).to.equal('1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt') - expect(result.query).to.deep.equal({ format: 'tar' }) + await assertMatchUrl( + 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: { + format: 'tar' + } + } + ) }) }) @@ -133,14 +169,14 @@ describe('parseUrlString', () => { path: '' }) - const result = await parseUrlString({ - urlString: 'ipns://mydomain.com', - ipns, - logger - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('') + await assertMatchUrl( + 'ipns://mydomain.com', { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: '', + query: {} + } + ) }) it('can parse a URL with DNSLinkDomain+path', async () => { @@ -149,14 +185,14 @@ describe('parseUrlString', () => { path: '' }) - const result = await parseUrlString({ - urlString: 'ipns://mydomain.com/some/path/to/file.txt', - ipns, - logger - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('some/path/to/file.txt') + await assertMatchUrl( + 'ipns://mydomain.com/some/path/to/file.txt', { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: 'some/path/to/file.txt', + query: {} + } + ) }) it('can parse a URL with DNSLinkDomain+queryString', async () => { @@ -165,15 +201,16 @@ describe('parseUrlString', () => { path: '' }) - const result = await parseUrlString({ - urlString: 'ipns://mydomain.com?format=json', - ipns, - logger - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('') - expect(result.query).to.deep.equal({ format: 'json' }) + await assertMatchUrl( + 'ipns://mydomain.com?format=json', { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: '', + query: { + format: 'json' + } + } + ) }) it('can parse a URL with DNSLinkDomain+path+queryString', async () => { @@ -182,15 +219,184 @@ describe('parseUrlString', () => { path: '' }) - const result = await parseUrlString({ - urlString: 'ipns://mydomain.com/some/path/to/file.txt?format=json', - ipns, - logger: defaultLogger() + await assertMatchUrl( + 'ipns://mydomain.com/some/path/to/file.txt?format=json', { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: 'some/path/to/file.txt', + query: { + format: 'json' + } + } + ) + }) + + it('can parse a URL with DNSLinkDomain+directoryPath+queryString', async () => { + ipns.resolveDns.withArgs('mydomain.com').resolves({ + cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), + path: '' }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('some/path/to/file.txt') - expect(result.query).to.deep.equal({ format: 'json' }) + + await assertMatchUrl( + 'ipns://mydomain.com/some/path/to/dir/?format=json', { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: 'some/path/to/dir/', + query: { + format: 'json' + } + } + ) + }) + }) + + describe('/ipfs/ URLs', () => { + it('should parse an IPFS Path with a CID only', async () => { + await assertMatchUrl( + '/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '', + query: {} + } + ) + }) + + it('can parse an IPFS Path with CID+path', async () => { + await assertMatchUrl( + '/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: {} + } + ) + }) + + it('can parse an IPFS Path with CID+queryString', async () => { + await assertMatchUrl( + '/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm?format=car', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '', + query: { + format: 'car' + } + } + ) + }) + + it('can parse an IPFS Path with CID+path+queryString', async () => { + await assertMatchUrl( + '/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: { + format: 'tar' + } + } + ) + }) + }) + + describe('http://example.com/ipfs/ URLs', () => { + it('should parse an IPFS Gateway URL with a CID only', async () => { + await assertMatchUrl( + 'http://example.com/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '', + query: {} + } + ) + }) + + it('can parse an IPFS Gateway URL with CID+path', async () => { + await assertMatchUrl( + 'http://example.com/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: {} + } + ) + }) + + it('can parse an IPFS Gateway URL with CID+queryString', async () => { + await assertMatchUrl( + 'http://example.com/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm?format=car', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '', + query: { + format: 'car' + } + } + ) + }) + + it('can parse an IPFS Gateway URL with CID+path+queryString', async () => { + await assertMatchUrl( + 'http://example.com/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: { + format: 'tar' + } + } + ) + }) + }) + + describe('http://.ipfs.example.com URLs', () => { + it('should parse a IPFS Subdomain Gateway URL with a CID only', async () => { + await assertMatchUrl( + 'http://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm.ipfs.example.com', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '', + query: {} + } + ) + }) + + it('can parse a IPFS Subdomain Gateway URL with CID+path', async () => { + await assertMatchUrl( + 'http://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm.ipfs.example.com/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: {} + } + ) + }) + + it('can parse a IPFS Subdomain Gateway URL with CID+queryString', async () => { + await assertMatchUrl( + 'http://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm.ipfs.example.com?format=car', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '', + query: { + format: 'car' + } + } + ) + }) + + it('can parse a IPFS Subdomain Gateway URL with CID+path+queryString', async () => { + await assertMatchUrl( + 'http://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm.ipfs.example.com/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar', { + protocol: 'ipfs', + cid: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm', + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: { + format: 'tar' + } + } + ) }) }) @@ -228,14 +434,15 @@ describe('parseUrlString', () => { cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), path: '' }) - const result = await parseUrlString({ - urlString: `ipns://${testPeerId}`, - ipns, - logger - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('') + + await assertMatchUrl( + `ipns://${testPeerId}`, { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: '', + query: {} + } + ) }) it('can parse a URL with PeerId+path', async () => { @@ -243,14 +450,15 @@ describe('parseUrlString', () => { cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), path: '' }) - const result = await parseUrlString({ - urlString: `ipns://${testPeerId}/some/path/to/file.txt`, - ipns, - logger - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('some/path/to/file.txt') + + await assertMatchUrl( + `ipns://${testPeerId}/some/path/to/file.txt`, { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: 'some/path/to/file.txt', + query: {} + } + ) }) it('can parse a URL with PeerId+path with a trailing slash', async () => { @@ -258,14 +466,15 @@ describe('parseUrlString', () => { cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), path: '' }) - const result = await parseUrlString({ - urlString: `ipns://${testPeerId}/some/path/to/dir/`, - ipns, - logger - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('some/path/to/dir/') + + await assertMatchUrl( + `ipns://${testPeerId}/some/path/to/dir/`, { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: 'some/path/to/dir/', + query: {} + } + ) }) it('can parse a URL with PeerId+queryString', async () => { @@ -273,15 +482,17 @@ describe('parseUrlString', () => { cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), path: '' }) - const result = await parseUrlString({ - urlString: `ipns://${testPeerId}?fomat=dag-cbor`, - ipns, - logger - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('') - expect(result.query).to.deep.equal({ fomat: 'dag-cbor' }) + + await assertMatchUrl( + `ipns://${testPeerId}?format=dag-cbor`, { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: '', + query: { + format: 'dag-cbor' + } + } + ) }) it('can parse a URL with PeerId+path+queryString', async () => { @@ -289,15 +500,17 @@ describe('parseUrlString', () => { cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), path: '' }) - const result = await parseUrlString({ - urlString: `ipns://${testPeerId}/some/path/to/file.txt?fomat=dag-cbor`, - ipns, - logger - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('some/path/to/file.txt') - expect(result.query).to.deep.equal({ fomat: 'dag-cbor' }) + + await assertMatchUrl( + `ipns://${testPeerId}/some/path/to/file.txt?format=dag-cbor`, { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: 'some/path/to/file.txt', + query: { + format: 'dag-cbor' + } + } + ) }) it('should parse an ipns:// url with a path that resolves to a CID with a path', async () => { @@ -311,16 +524,14 @@ describe('parseUrlString', () => { path: recordPath }) - await expect(parseUrlString({ - urlString: `ipns://${peerId}/${requestPath}`, - ipns, - logger - })).to.eventually.deep.equal({ - protocol: 'ipns', - path: `${recordPath}/${requestPath}`, - cid, - query: {} - }) + await assertMatchUrl( + `ipns://${peerId}/${requestPath}`, { + protocol: 'ipns', + cid: 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + path: `${recordPath}/${requestPath}`, + query: {} + } + ) }) it('should parse an ipns:// url with a path that resolves to a CID with a path with a trailing slash', async () => { @@ -334,16 +545,14 @@ describe('parseUrlString', () => { path: recordPath }) - await expect(parseUrlString({ - urlString: `ipns://${peerId}/${requestPath}`, - ipns, - logger - })).to.eventually.deep.equal({ - protocol: 'ipns', - path: 'foo/bar/baz.txt', - cid, - query: {} - }) + await assertMatchUrl( + `ipns://${peerId}/${requestPath}`, { + protocol: 'ipns', + path: 'foo/bar/baz.txt', + cid: cid.toString(), + query: {} + } + ) }) it('should parse an ipns:// url with a path that resolves to a CID with a path with a trailing slash', async () => { @@ -357,15 +566,275 @@ describe('parseUrlString', () => { path: recordPath }) - await expect(parseUrlString({ - urlString: `ipns://${peerId}/${requestPath}`, - ipns, - logger - })).to.eventually.deep.equal({ - protocol: 'ipns', - path: 'foo/bar/baz/qux.txt', + await assertMatchUrl( + `ipns://${peerId}/${requestPath}`, { + protocol: 'ipns', + path: 'foo/bar/baz/qux.txt', + cid: cid.toString(), + query: {} + } + ) + }) + }) + + describe('/ipns/ URLs', () => { + let peerId: PeerId + let cid: CID + + beforeEach(async () => { + peerId = await createEd25519PeerId() + cid = CID.parse('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') + ipns.resolve.withArgs(matchPeerId(peerId)).resolves({ cid, - query: {} + path: '' + }) + }) + + it('should parse an IPNS Path with a PeerId only', async () => { + await assertMatchUrl( + `/ipns/${peerId}/`, { + protocol: 'ipns', + cid: cid.toString(), + path: '', + query: {} + } + ) + }) + + it('can parse an IPNS Path with PeerId+path', async () => { + await assertMatchUrl( + `/ipns/${peerId}/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt`, { + protocol: 'ipns', + cid: cid.toString(), + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: {} + } + ) + }) + + it('can parse an IPNS Path with PeerId+directoryPath', async () => { + await assertMatchUrl( + `/ipns/${peerId}/path/to/dir/`, { + protocol: 'ipns', + cid: cid.toString(), + path: 'path/to/dir/', + query: {} + } + ) + }) + + it('can parse an IPNS Path with PeerId+queryString', async () => { + await assertMatchUrl( + `/ipns/${peerId}?format=car`, { + protocol: 'ipns', + cid: cid.toString(), + path: '', + query: { + format: 'car' + } + } + ) + }) + + it('can parse an IPNS Path with PeerId+path+queryString', async () => { + await assertMatchUrl( + `/ipns/${peerId}/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar`, { + protocol: 'ipns', + cid: cid.toString(), + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: { + format: 'tar' + } + } + ) + }) + + it('can parse an IPNS Path with PeerId+directoryPath+queryString', async () => { + await assertMatchUrl( + `/ipns/${peerId}/path/to/dir/?format=tar`, { + protocol: 'ipns', + cid: cid.toString(), + path: 'path/to/dir/', + query: { + format: 'tar' + } + } + ) + }) + }) + + HTTP_PROTOCOLS.forEach(proto => { + describe(`${proto}://example.com/ipfs/ URLs`, () => { + let peerId: PeerId + let cid: CID + + beforeEach(async () => { + peerId = await createEd25519PeerId() + cid = CID.parse('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') + ipns.resolve.withArgs(matchPeerId(peerId)).resolves({ + cid, + path: '' + }) + }) + + it('should parse an IPFS Gateway URL with a CID only', async () => { + await assertMatchUrl( + `${proto}://example.com/ipns/${peerId}`, { + protocol: 'ipns', + cid: cid.toString(), + path: '', + query: {} + } + ) + }) + + it('can parse an IPNS Gateway URL with CID+path', async () => { + await assertMatchUrl( + `${proto}://example.com/ipns/${peerId}/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt`, { + protocol: 'ipns', + cid: cid.toString(), + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: {} + } + ) + }) + + it('can parse an IPNS Gateway URL with CID+directoryPath', async () => { + await assertMatchUrl( + `${proto}://example.com/ipns/${peerId}/path/to/dir/`, { + protocol: 'ipns', + cid: cid.toString(), + path: 'path/to/dir/', + query: {} + } + ) + }) + + it('can parse an IPNS Gateway URL with CID+queryString', async () => { + await assertMatchUrl( + `${proto}://example.com/ipns/${peerId}?format=car`, { + protocol: 'ipns', + cid: cid.toString(), + path: '', + query: { + format: 'car' + } + } + ) + }) + + it('can parse an IPNS Gateway URL with CID+path+queryString', async () => { + await assertMatchUrl( + `${proto}://example.com/ipns/${peerId}/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar`, { + protocol: 'ipns', + cid: cid.toString(), + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: { + format: 'tar' + } + } + ) + }) + + it('can parse an IPNS Gateway URL with CID+directoryPath+queryString', async () => { + await assertMatchUrl( + `${proto}://example.com/ipns/${peerId}/path/to/dir/?format=tar`, { + protocol: 'ipns', + cid: cid.toString(), + path: 'path/to/dir/', + query: { + format: 'tar' + } + } + ) + }) + }) + }) + + HTTP_PROTOCOLS.forEach(proto => { + describe(`${proto}://.ipns.example.com URLs`, () => { + let peerId: PeerId + let cid: CID + + beforeEach(async () => { + peerId = await createEd25519PeerId() + cid = CID.parse('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') + ipns.resolve.withArgs(matchPeerId(peerId)).resolves({ + cid, + path: '' + }) + }) + + it('should parse a IPNS Subdomain Gateway URL with a CID only', async () => { + await assertMatchUrl( + `${proto}://${peerId}.ipns.example.com`, { + protocol: 'ipns', + cid: cid.toString(), + path: '', + query: {} + } + ) + }) + + it('can parse a IPNS Subdomain Gateway URL with CID+path', async () => { + await assertMatchUrl( + `${proto}://${peerId}.ipns.example.com/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt`, { + protocol: 'ipns', + cid: cid.toString(), + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: {} + } + ) + }) + + it('can parse a IPNS Subdomain Gateway URL with CID+directoryPath', async () => { + await assertMatchUrl( + `${proto}://${peerId}.ipns.example.com/path/to/dir/`, { + protocol: 'ipns', + cid: cid.toString(), + path: 'path/to/dir/', + query: {} + } + ) + }) + + it('can parse a IPNS Subdomain Gateway URL with CID+queryString', async () => { + await assertMatchUrl( + `${proto}://${peerId}.ipns.example.com?format=car`, { + protocol: 'ipns', + cid: cid.toString(), + path: '', + query: { + format: 'car' + } + } + ) + }) + + it('can parse a IPNS Subdomain Gateway URL with CID+path+queryString', async () => { + await assertMatchUrl( + `${proto}://${peerId}.ipns.example.com/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar`, { + protocol: 'ipns', + cid: cid.toString(), + path: '1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + query: { + format: 'tar' + } + } + ) + }) + + it('can parse a IPNS Subdomain Gateway URL with CID+directoryPath+queryString', async () => { + await assertMatchUrl( + `${proto}://${peerId}.ipns.example.com/path/to/dir/?format=tar`, { + protocol: 'ipns', + cid: cid.toString(), + path: 'path/to/dir/', + query: { + format: 'tar' + } + } + ) }) }) })