diff --git a/src/components/Form.tsx b/src/components/Form.tsx index 07fe250e..f8c2010b 100644 --- a/src/components/Form.tsx +++ b/src/components/Form.tsx @@ -2,7 +2,7 @@ import React from 'preact/compat' export default ({ handleSubmit, requestPath, setRequestPath }): React.JSX.Element => (
- + +

Invalid address, correct it and try again. For reference, accepted formats are:

+ + + + + + + + + + + + + + + +
UNIX-like Content Path
/ipfs/cid/..
HTTP Gateway URL
https://ipfs.io/ipfs/cid..
Native IPFS URL
ipfs://cid/..
+

Learn more at Addressing IPFS on the Web

+ + ) +} -function ValidationMessage ({ cid, requestPath, pathNamespacePrefix, children }): React.JSX.Element { +function ValidationMessage ({ cidOrPeerIdOrDnslink, requestPath, protocol, children }): React.JSX.Element { let errorElement: React.JSX.Element | null = null if (requestPath == null || requestPath === '') { - errorElement = Nothing to render yet. Enter an IPFS Path // bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq - } else if (pathNamespacePrefix !== 'ipfs' && pathNamespacePrefix !== 'ipns') { - errorElement = Not a valid IPFS or IPNS path. Use the format
/ip(f|n)s/cid/path
, where /path is optional
- } else if (cid == null || cid === '') { - errorElement = Nothing to render yet. Add a CID to your path // bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq - } else if (pathNamespacePrefix === 'ipfs') { + errorElement = Enter a valid IPFS/IPNS path. + } else if (protocol !== 'ipfs' && protocol !== 'ipns') { + errorElement = + } else if (cidOrPeerIdOrDnslink == null || cidOrPeerIdOrDnslink === '') { + const contentType = protocol === 'ipfs' ? 'CID' : 'PeerId or DnsLink' + errorElement = Content identifier missing. Add a {contentType} to your path + } else if (protocol === 'ipfs') { try { - CID.parse(cid) + CID.parse(cidOrPeerIdOrDnslink) } catch { errorElement = Invalid CID } @@ -49,32 +55,34 @@ function ValidationMessage ({ cid, requestPath, pathNamespacePrefix, children }) } +const parseInput = (uri: string): Partial => { + const uriMatch = uri.match(pathRegex) ?? uri.match(subdomainRegex) ?? uri.match(nativeProtocolRegex) + if (uriMatch?.groups != null) { + const { protocol, cidOrPeerIdOrDnslink, path } = uriMatch.groups as unknown as IpfsUriParts + return { protocol, cidOrPeerIdOrDnslink, path: path?.trim() ?? undefined } + } + + // it may be just a CID + try { + CID.parse(uri) + return { protocol: 'ipfs', cidOrPeerIdOrDnslink: uri } + } catch (_) { + // ignore. + } + + return {} +} + export default function InputValidator ({ requestPath }: { requestPath: string }): React.JSX.Element { - /** - * requestPath may be any of the following formats: - * - * * `/ipfs/${cid}[/${path}]` - * * `/ipns/${dnsLinkDomain}[/${path}]` - * * `/ipns/${peerId}[/${path}]` - * * `http[s]://${cid}.ipfs.example.com[/${path}]` - * * `http[s]://${dnsLinkDomain}.ipns.example.com[/${path}]` - * * `http[s]://${peerId}.ipns.example.com[/${path}]` - * TODO: https://github.com/ipfs-shipyard/service-worker-gateway/issues/66 - */ - const requestPathParts = requestPath.split('/') - const pathNamespacePrefix = requestPathParts[1] - const cid = requestPathParts[2] - const cidPath = requestPathParts[3] ? `/${requestPathParts.slice(3).join('/')}` : '' - const swPath = `/${pathNamespacePrefix}/${cid ?? ''}${cidPath ?? ''}` + const { protocol, cidOrPeerIdOrDnslink, path } = parseInput(requestPath) + const swPath = `/${protocol}/${cidOrPeerIdOrDnslink}${path ?? ''}` return (
- - + -
) diff --git a/src/lib/path-or-subdomain.ts b/src/lib/path-or-subdomain.ts index c6737d96..2e3b5530 100644 --- a/src/lib/path-or-subdomain.ts +++ b/src/lib/path-or-subdomain.ts @@ -2,10 +2,7 @@ import { base32 } from 'multiformats/bases/base32' import { base36 } from 'multiformats/bases/base36' import { CID } from 'multiformats/cid' import { dnsLinkLabelEncoder } from './dns-link-labels.js' - -// TODO: dry, this is same regex code as in getSubdomainParts -const subdomainRegex = /^(?[^/]+)\.(?ip[fn]s)\.[^/]+$/ -const pathRegex = /^\/(?ip[fn]s)\/(?.*)$/ +import { pathRegex, subdomainRegex } from './regex.js' export const isPathOrSubdomainRequest = (location: Pick): boolean => { return isPathGatewayRequest(location) || isSubdomainGatewayRequest(location) diff --git a/src/lib/regex.ts b/src/lib/regex.ts new file mode 100644 index 00000000..78c8bd71 --- /dev/null +++ b/src/lib/regex.ts @@ -0,0 +1,30 @@ +export interface IpfsUriParts { + protocol: 'ipfs' | 'ipns' + cidOrPeerIdOrDnslink: string + path?: string + parentDomain?: string +} + +/** + * `http[s]://${cid}.ipfs.example.com[/${path}]` + * `http[s]://${dnsLinkDomain}.ipns.example.com[/${path}]` + * `http[s]://${peerId}.ipns.example.com[/${path}]` + * + * @see https://regex101.com/r/EcY028/4 + */ +export const subdomainRegex = /^(?:https?:\/\/|\/\/)?(?[^/]+)\.(?ip[fn]s)\.(?[^/?#]*)(?.*)$/ + +/** + * `http[s]://example.com/ipfs/${cid}[/${path}]` + * `/ipfs/${cid}[/${path}]` + * `/ipns/${dnsLinkDomain}[/${path}]` + * `/ipns/${peerId}[/${path}]` + * + * @see https://regex101.com/r/zdDp0i/1 + */ +export const pathRegex = /^.*\/(?ip[fn]s)\/(?[^/?#]*)(?.*)$/ + +/** + * `ip[fn]s://${cidOrPeerIdOrDnslink}${path}` + */ +export const nativeProtocolRegex = /^(?ip[fn]s):\/\/(?[^/?#]*)(?.*)$/ diff --git a/src/pages/redirects-interstitial.tsx b/src/pages/redirects-interstitial.tsx index 4a54f955..bb86ed4d 100644 --- a/src/pages/redirects-interstitial.tsx +++ b/src/pages/redirects-interstitial.tsx @@ -1,4 +1,4 @@ -import React from 'preact' +import React from 'preact/compat' /** * This page is only used to capture the ?helia-sw=/ip[fn]s/blah query parameter that diff --git a/test/README.md b/test/README.md index 8dd09bfb..86bc85d1 100644 --- a/test/README.md +++ b/test/README.md @@ -1,5 +1,5 @@ # Tests -These tests are ran using `aegir test`. +Tests with the name `*.spec.ts` are ran using `npm run test:iso`. -Since we don't use aegir for building our dist folder, we need all tests in this directory to be javascript files. This is a temporary solution until we can use aegir for building our dist folder. +There is one file without a `.spec.ts` suffix that verifies our dist folder ran with `npm run test:node`, and no further test files without `.spec.ts` suffix should be added. diff --git a/test/regex.spec.ts b/test/regex.spec.ts new file mode 100644 index 00000000..4cddc5d9 --- /dev/null +++ b/test/regex.spec.ts @@ -0,0 +1,156 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { nativeProtocolRegex, pathRegex, subdomainRegex } from '../src/lib/regex.js' + +const validPathUrls = [ + // IPFS paths without domain + ['/ipfs/bafyFoo', 'ipfs', 'bafyFoo', ''], + ['/ipfs/bafyFoo/', 'ipfs', 'bafyFoo', '/'], + ['/ipfs/bafyFoo/path/to/file', 'ipfs', 'bafyFoo', '/path/to/file'], + + // IPFS paths with domain + ['http://example.com/ipfs/bafyFoo', 'ipfs', 'bafyFoo', ''], + ['http://example.com/ipfs/bafyFoo/', 'ipfs', 'bafyFoo', '/'], + ['http://example.com/ipfs/bafyFoo/path/to/file', 'ipfs', 'bafyFoo', '/path/to/file'], + ['http://example.com/ipfs/bafyFoo/path/to/file/', 'ipfs', 'bafyFoo', '/path/to/file/'], + + // IPNS paths without domain + ['/ipns/specs.ipfs.tech', 'ipns', 'specs.ipfs.tech', ''], + ['/ipns/specs.ipfs.tech/', 'ipns', 'specs.ipfs.tech', '/'], + ['/ipns/specs.ipfs.tech/path/to/file', 'ipns', 'specs.ipfs.tech', '/path/to/file'], + + // IPNS paths with domain + ['http://example.com/ipns/specs.ipfs.tech', 'ipns', 'specs.ipfs.tech', ''], + ['http://example.com/ipns/specs.ipfs.tech/', 'ipns', 'specs.ipfs.tech', '/'], + ['http://example.com/ipns/specs.ipfs.tech/path/to/file', 'ipns', 'specs.ipfs.tech', '/path/to/file'], + ['http://example.com/ipns/specs.ipfs.tech/path/to/file/', 'ipns', 'specs.ipfs.tech', '/path/to/file/'], + + // schemeless + ['//example.com/ipns/specs.ipfs.tech', 'ipns', 'specs.ipfs.tech', ''], + ['//example.com/ipns/specs.ipfs.tech/', 'ipns', 'specs.ipfs.tech', '/'], + ['//example.com/ipns/specs.ipfs.tech/path/to/file', 'ipns', 'specs.ipfs.tech', '/path/to/file'], + ['//example.com/ipns/specs.ipfs.tech/path/to/file/', 'ipns', 'specs.ipfs.tech', '/path/to/file/'], + + // with different path starts (hash, query, etc) + ['/ipfs/bafyFoo#hash', 'ipfs', 'bafyFoo', '#hash'], + ['/ipfs/bafyFoo?query', 'ipfs', 'bafyFoo', '?query'], + ['/ipfs/bafyFoo?query#hash', 'ipfs', 'bafyFoo', '?query#hash'], + ['/ipfs/bafyFoo#hash?query', 'ipfs', 'bafyFoo', '#hash?query'] +] + +const validSubdomainUrls = [ + // IPFS subdomains + ['http://bafyFoo.ipfs.example.com', 'ipfs', 'bafyFoo', ''], + ['http://bafyFoo.ipfs.example.com/', 'ipfs', 'bafyFoo', '/'], + ['http://bafyFoo.ipfs.example.com/path/to/file', 'ipfs', 'bafyFoo', '/path/to/file'], + + // IPNS subdomains + ['http://bafyFoo.ipns.example.com', 'ipns', 'bafyFoo', ''], + ['http://bafyFoo.ipns.example.com/', 'ipns', 'bafyFoo', '/'], + ['http://bafyFoo.ipns.example.com/path/to/file', 'ipns', 'bafyFoo', '/path/to/file'], + + // IPNS subdomains with dnslink + ['http://specs-ipfs-tech.ipns.example.com', 'ipns', 'specs-ipfs-tech', ''], + ['http://specs-ipfs-tech.ipns.example.com/', 'ipns', 'specs-ipfs-tech', '/'], + ['http://specs-ipfs-tech.ipns.example.com/path/to/file', 'ipns', 'specs-ipfs-tech', '/path/to/file'], + + // schemeless + ['//bafyFoo.ipfs.example.com', 'ipfs', 'bafyFoo', ''], + ['//bafyFoo.ipfs.example.com/', 'ipfs', 'bafyFoo', '/'], + ['//bafyFoo.ipfs.example.com/path/to/file', 'ipfs', 'bafyFoo', '/path/to/file'], + + ['//bafyFoo.ipns.example.com', 'ipns', 'bafyFoo', ''], + ['//bafyFoo.ipns.example.com/', 'ipns', 'bafyFoo', '/'], + ['//bafyFoo.ipns.example.com/path/to/file', 'ipns', 'bafyFoo', '/path/to/file'], + + ['//specs-ipfs-tech.ipns.example.com', 'ipns', 'specs-ipfs-tech', ''], + ['//specs-ipfs-tech.ipns.example.com/', 'ipns', 'specs-ipfs-tech', '/'], + ['//specs-ipfs-tech.ipns.example.com/path/to/file', 'ipns', 'specs-ipfs-tech', '/path/to/file'], + + // hostname only + ['bafyFoo.ipfs.example.com', 'ipfs', 'bafyFoo', ''], + ['bafyFoo.ipfs.example.com/', 'ipfs', 'bafyFoo', '/'], + ['bafyFoo.ipfs.example.com/path/to/file', 'ipfs', 'bafyFoo', '/path/to/file'], + + ['bafyFoo.ipns.example.com', 'ipns', 'bafyFoo', ''], + ['bafyFoo.ipns.example.com/', 'ipns', 'bafyFoo', '/'], + ['bafyFoo.ipns.example.com/path/to/file', 'ipns', 'bafyFoo', '/path/to/file'], + + ['specs-ipfs-tech.ipns.example.com', 'ipns', 'specs-ipfs-tech', ''], + ['specs-ipfs-tech.ipns.example.com/', 'ipns', 'specs-ipfs-tech', '/'], + ['specs-ipfs-tech.ipns.example.com/path/to/file', 'ipns', 'specs-ipfs-tech', '/path/to/file'], + + // with different path starts (hash, query, etc) + ['bafyFoo.ipfs.example.com#hash', 'ipfs', 'bafyFoo', '#hash'], + ['bafyFoo.ipfs.example.com?query', 'ipfs', 'bafyFoo', '?query'], + ['bafyFoo.ipfs.example.com?query#hash', 'ipfs', 'bafyFoo', '?query#hash'], + ['bafyFoo.ipfs.example.com#hash?query', 'ipfs', 'bafyFoo', '#hash?query'] +] + +const validNativeProtocolUrls = [ + ['ipfs://bafyFoo', 'ipfs', 'bafyFoo', ''], + ['ipfs://bafyFoo/', 'ipfs', 'bafyFoo', '/'], + ['ipfs://bafyFoo/path/to/file', 'ipfs', 'bafyFoo', '/path/to/file'], + + ['ipns://specs.ipfs.tech', 'ipns', 'specs.ipfs.tech', ''], + ['ipns://specs.ipfs.tech/', 'ipns', 'specs.ipfs.tech', '/'], + ['ipns://specs.ipfs.tech/path/to/file', 'ipns', 'specs.ipfs.tech', '/path/to/file'] +] + +const invalidUrls = [ + 'http://localhost/notipfs/bafyFoo/path/to/file' +] + +describe('regex', () => { + describe('paths', () => { + validPathUrls.forEach(([url, protocol, cidOrPeerIdOrDnslink, path]) => { + it(`should correctly match "${url}"`, () => { + const match = url.match(pathRegex) + + expect(match).not.to.be.null() + expect(match?.groups).to.be.ok() + expect(match?.groups?.protocol).to.equal(protocol) + expect(match?.groups?.cidOrPeerIdOrDnslink).to.equal(cidOrPeerIdOrDnslink) + expect(match?.groups?.path).to.equal(path) + }) + }) + }) + + describe('subdomains', () => { + validSubdomainUrls.forEach(([url, protocol, cidOrPeerIdOrDnslink, path]) => { + it(`should correctly match "${url}"`, () => { + const match = url.match(subdomainRegex) + + expect(match).not.to.be.null() + expect(match?.groups).to.be.ok() + expect(match?.groups?.protocol).to.equal(protocol) + expect(match?.groups?.cidOrPeerIdOrDnslink).to.equal(cidOrPeerIdOrDnslink) + expect(match?.groups?.path).to.equal(path) + }) + }) + }) + + describe('native protocols', () => { + validNativeProtocolUrls.forEach(([url, protocol, cidOrPeerIdOrDnslink, path]) => { + it(`should correctly match "${url}"`, () => { + const match = url.match(nativeProtocolRegex) + + expect(match).not.to.be.null() + expect(match?.groups).to.be.ok() + expect(match?.groups?.protocol).to.equal(protocol) + expect(match?.groups?.cidOrPeerIdOrDnslink).to.equal(cidOrPeerIdOrDnslink) + expect(match?.groups?.path).to.equal(path) + }) + }) + }) + + describe('invalid urls', () => { + invalidUrls.forEach(url => { + it(`should return null for "${url}"`, () => { + const match = url.match(pathRegex) + + expect(match).to.be.null() + }) + }) + }) +})