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: helper-ui input-validator update #202

Merged
92 changes: 45 additions & 47 deletions src/components/input-validator.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { CID } from 'multiformats/cid'
import React from 'react'
import { pathRegex, subdomainRegex, type IpfsUriParts } from '../lib/regex.js'

/**
* Test files:
* bafkreienxxjqg3jomg5b75k7547dgf7qlbd3qpxy2kbg537ck3rol4mcve - text - https://bafkreienxxjqg3jomg5b75k7547dgf7qlbd3qpxy2kbg537ck3rol4mcve.ipfs.w3s.link/?filename=test.txt
* bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra - image/jpeg - http://127.0.0.1:8080/ipfs/bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra?filename=bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra
* bafkreif4ufrfpfcmqn5ltjvmeisgv4k7ykqz2mjygpngtwt4bijxosidqa - image/svg+xml - https://bafkreif4ufrfpfcmqn5ltjvmeisgv4k7ykqz2mjygpngtwt4bijxosidqa.ipfs.dweb.link/?filename=Web3.Storage-logo.svg
* bafybeiekildl23opzqcsufewlbadhbabs6pyqg35tzpfavgtjyhchyikxa - video/quicktime - https://bafybeiekildl23opzqcsufewlbadhbabs6pyqg35tzpfavgtjyhchyikxa.ipfs.dweb.link
* bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq - video/webm (147.78 KiB) - https://bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq.ipfs.dweb.link
* bafybeierkpfmf4vhtdiujgahfptiyriykoetxf3rmd3vcxsdemincpjoyu - video/mp4 (2.80 MiB) - https://bafybeierkpfmf4vhtdiujgahfptiyriykoetxf3rmd3vcxsdemincpjoyu.ipfs.dweb.link
* QmbGtJg23skhvFmu9mJiePVByhfzu5rwo74MEkVDYAmF5T - video (160MiB)
* /ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8 -
* /ipns/libp2p.io/
*/

/**
*
* Test CIDs
* QmbGtJg23skhvFmu9mJiePVByhfzu5rwo74MEkVDYAmF5T
*
*/
function FormatHelp (): JSX.Element {
return (
<p>
<span>Not a valid IPFS or IPNS path. Use one of the following formats:</span>
<ul>
<li><pre className="di">/ipfs/cid/path</pre></li>
<li><pre className="di">/ipns/peerId/path</pre></li>
<li><pre className="di">/ipns/dnsLink/path</pre></li>
<li><pre className="di">https?://example.com/ipfs/cid/path</pre></li>
<li><pre className="di">https?://example.com/ipns/peerId/path</pre></li>
<li><pre className="di">https?://example.com/ipns/dnsLink/path</pre></li>
<li><pre className="di">https?://cid.ipfs.example.com/path</pre></li>
<li><pre className="di">https?://peerId.ipns.example.com/path</pre></li>
<li><pre className="di">https?://encodedDnsLink.ipns.example.com/path</pre></li>
<li><pre className="di">cid.ipfs.example.com/path</pre></li>
<li><pre className="di">peerId.ipns.example.com/path</pre></li>
<li><pre className="di">encodedDnsLink.ipns.example.com/path</pre></li>
</ul>
<span>Note that <pre className="di">/path</pre> is optional.</span>
</p>
)
}

function ValidationMessage ({ cid, requestPath, pathNamespacePrefix, children }): JSX.Element {
function ValidationMessage ({ cidOrPeerIdOrDnslink, requestPath, protocol, children }): JSX.Element {
let errorElement: JSX.Element | null = null
if (requestPath == null || requestPath === '') {
errorElement = <span>Nothing to render yet. Enter an IPFS Path</span> // bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq
} else if (pathNamespacePrefix !== 'ipfs' && pathNamespacePrefix !== 'ipns') {
errorElement = <span>Not a valid IPFS or IPNS path. Use the format <pre className="di">/ip(f|n)s/cid/path</pre>, where /path is optional</span>
} else if (cid == null || cid === '') {
errorElement = <span>Nothing to render yet. Add a CID to your path</span> // bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq
} else if (pathNamespacePrefix === 'ipfs') {
errorElement = <span>Enter a valid IPFS/IPNS path.</span>
} else if (protocol !== 'ipfs' && protocol !== 'ipns') {
errorElement = <FormatHelp />
} else if (cidOrPeerIdOrDnslink == null || cidOrPeerIdOrDnslink === '') {
const contentType = protocol === 'ipfs' ? 'CID' : 'PeerId or DnsLink'
errorElement = <span>Content identifier missing. Add a {contentType} to your path</span>
} else if (protocol === 'ipfs') {
try {
CID.parse(cid)
CID.parse(cidOrPeerIdOrDnslink)
} catch {
errorElement = <span>Invalid CID</span>
}
Expand All @@ -49,32 +53,26 @@ function ValidationMessage ({ cid, requestPath, pathNamespacePrefix, children })
</>
}

const parseInput = (uri: string): Partial<IpfsUriParts> => {
const uriMatch = uri.match(pathRegex) ?? uri.match(subdomainRegex)
if (uriMatch?.groups != null) {
const { protocol, cidOrPeerIdOrDnslink, path } = uriMatch.groups as unknown as IpfsUriParts
return { protocol, cidOrPeerIdOrDnslink, path: path?.trim() ?? undefined }
}

return {}
}

export default function InputValidator ({ requestPath }: { requestPath: string }): 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 (
<div>
<ValidationMessage pathNamespacePrefix={pathNamespacePrefix} cid={cid} requestPath={requestPath}>

<ValidationMessage protocol={protocol} cidOrPeerIdOrDnslink={cidOrPeerIdOrDnslink} requestPath={requestPath}>
<a className="db" href={swPath} target="_blank">
<button id="load-directly" className='button-reset pv3 tc bn bg-animate bg-black-80 hover-bg-aqua white pointer w-100'>Load content</button>
</a>

</ValidationMessage>
</div>
)
Expand Down
5 changes: 1 addition & 4 deletions src/lib/path-or-subdomain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /^(?<id>[^/]+)\.(?<protocol>ip[fn]s)\.[^/]+$/
const pathRegex = /^\/(?<protocol>ip[fn]s)\/(?<path>.*)$/
import { pathRegex, subdomainRegex } from './regex.js'

export const isPathOrSubdomainRequest = (location: Pick<Location, 'host' | 'pathname'>): boolean => {
return isPathGatewayRequest(location) || isSubdomainGatewayRequest(location)
Expand Down
20 changes: 20 additions & 0 deletions src/lib/regex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export interface IpfsUriParts {
protocol: 'ipfs' | 'ipns'
cidOrPeerIdOrDnslink: string
path?: string
}

/**
* `http[s]://${cid}.ipfs.example.com[/${path}]`
* `http[s]://${dnsLinkDomain}.ipns.example.com[/${path}]`
* `http[s]://${peerId}.ipns.example.com[/${path}]`
*/
export const subdomainRegex = /^(?:https?:\/\/)?(?<cidOrPeerIdOrDnslink>[^/]+)\.(?<protocol>ip[fn]s)\.(?<parentDomain>[^/]*)(?<path>\/?.*)$$/

/**
* `http[s]://example.com/ipfs/${cid}[/${path}]`
* `/ipfs/${cid}[/${path}]`
* `/ipns/${dnsLinkDomain}[/${path}]`
* `/ipns/${peerId}[/${path}]`
*/
export const pathRegex = /^.*\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnslink>[^/]*)(?<path>.*)$/
4 changes: 2 additions & 2 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -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.
81 changes: 81 additions & 0 deletions test/regex.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { 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/']
]

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']
]

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)
})
})
})

it('should return null for non-matching urls', () => {
const url = 'http://localhost/notipfs/bafyFoo/path/to/file'
const match = url.match(pathRegex)

expect(match).to.be.null()
})
})
Loading