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/add iana ipfs #95

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,19 @@ isIPFS.path('/ipfs/js-ipfs/blob/master/README.md') // false
isIPFS.urlOrPath('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
isIPFS.urlOrPath('https://ipfs.io/ipns/github.com') // true
isIPFS.urlOrPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
isIPFS.urlOrPath('ipfs://QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
isIPFS.urlOrPath('/ipns/github.com') // true
isIPFS.urlOrPath('ipns://github.com') // true
isIPFS.urlOrPath('https://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // true
isIPFS.urlOrPath('https://google.com') // false

isIPFS.ipfsUrl('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
isIPFS.ipfsUrl('https://ipfs.io/ipfs/invalid-hash') // false
isIPFS.ipfsUrl('ipfs://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
isIPFS.ipfsUrl('ipfs://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
isIPFS.ipfsUrl('ipfs://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false

this should return as invalid since it should have a CID as the "domain" when "ipfs" is the protocol


isIPFS.ipnsUrl('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false
isIPFS.ipnsUrl('https://ipfs.io/ipns/github.com') // true
isIPFS.ipnsUrl('ipns://ipfs.io/ipns/github.com') // true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return as true because ipfs.io might reference a valid dnslink, but the /ipns/<domain> might confuse users. we should remove this, or just use ipns://<validDnsLink>

also we should probably show invalid here:

  • ipns://CID is invalid
  • ipns:// is invalid
  • ipns:// is invalid


isIPFS.ipfsPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
isIPFS.ipfsPath('/ipfs/invalid-hash') // false
Expand Down
182 changes: 182 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
'use strict'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file shouldn't be committed here. might have been extra from a manual tsc run?


const multihash = require('multihashes')
const multibase = require('multibase')
const Multiaddr = require('multiaddr')
const mafmt = require('mafmt')
const CID = require('cids')
const { URL } = require('iso-url')
const uint8ArrayToString = require('uint8arrays/to-string')

const pathGatewayPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/?#]+)/
const pathPattern = /^\/(ip[fn]s)\/([^/?#]+)/
const defaultProtocolMatch = 1
const defaultHashMath = 2

// CID, libp2p-key or DNSLink
const subdomainGatewayPattern = /^https?:\/\/([^/]+)\.(ip[fn]s)\.[^/?]+/
const subdomainIdMatch = 1
const subdomainProtocolMatch = 2

// Fully qualified domain name (FQDN) that has an explicit .tld suffix
const fqdnWithTld = /^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$/

function isMultihash (hash) {
const formatted = convertToString(hash)
try {
multihash.decode(multibase.decode('z' + formatted))
return true
} catch (e) {
return false
}
}

function isMultibase (hash) {
try {
return multibase.isEncoded(hash)
} catch (e) {
return false
}
}

function isCID (hash) {
try {
new CID(hash) // eslint-disable-line no-new
return true
} catch (e) {
return false
}
}

function isMultiaddr (input) {
if (!input) return false
if (Multiaddr.isMultiaddr(input)) return true
try {
new Multiaddr(input) // eslint-disable-line no-new
return true
} catch (e) {
return false
}
}

function isPeerMultiaddr (input) {
return isMultiaddr(input) && mafmt.IPFS.matches(input)
}

function isIpfs (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch = defaultHashMath) {
const formatted = convertToString(input)
if (!formatted) {
return false
}

const match = formatted.match(pattern)
if (!match) {
return false
}

if (match[protocolMatch] !== 'ipfs') {
return false
}

let hash = match[hashMatch]

if (hash && pattern === subdomainGatewayPattern) {
// when doing checks for subdomain context
// ensure hash is case-insensitive
// (browsers force-lowercase authority compotent anyway)
hash = hash.toLowerCase()
}

return isCID(hash)
}

function isIpns (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch) {
const formatted = convertToString(input)
if (!formatted) {
return false
}
const match = formatted.match(pattern)
if (!match) {
return false
}

if (match[protocolMatch] !== 'ipns') {
return false
}

let ipnsId = match[hashMatch]

if (ipnsId && pattern === subdomainGatewayPattern) {
// when doing checks for subdomain context
// ensure ipnsId is case-insensitive
// (browsers force-lowercase authority compotent anyway)
ipnsId = ipnsId.toLowerCase()
// Check if it is cidv1
if (isCID(ipnsId)) return true
// Check if it looks like FQDN
try {
if (!ipnsId.includes('.') && ipnsId.includes('-')) {
// name without tld, assuming its inlined into a single DNS label
// (https://github.com/ipfs/in-web-browsers/issues/169)
// en-wikipedia--on--ipfs-org → en.wikipedia-on-ipfs.org
ipnsId = ipnsId.replace(/--/g, '@').replace(/-/g, '.').replace(/@/g, '-')
}
// URL implementation in web browsers forces lowercase of the hostname
const { hostname } = new URL(`http://${ipnsId}`) // eslint-disable-line no-new
// Check if potential FQDN has an explicit TLD
return fqdnWithTld.test(hostname)
} catch (e) {
return false
}
}

return true
}

function isString (input) {
return typeof input === 'string'
}

function convertToString (input) {
if (input instanceof Uint8Array) {
return uint8ArrayToString(input, 'base58btc')
}

if (isString(input)) {
return input
}

return false
}

const ipfsSubdomain = (url) => isIpfs(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch)
const ipnsSubdomain = (url) => isIpns(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch)
const subdomain = (url) => ipfsSubdomain(url) || ipnsSubdomain(url)

const ipfsUrl = (url) => isIpfs(url, pathGatewayPattern) || ipfsSubdomain(url)
const ipnsUrl = (url) => isIpns(url, pathGatewayPattern) || ipnsSubdomain(url)
const url = (url) => ipfsUrl(url) || ipnsUrl(url) || subdomain(url)

const path = (path) => isIpfs(path, pathPattern) || isIpns(path, pathPattern)

module.exports = {
multihash: isMultihash,
multiaddr: isMultiaddr,
peerMultiaddr: isPeerMultiaddr,
cid: isCID,
base32cid: (cid) => (isMultibase(cid) === 'base32' && isCID(cid)),
ipfsSubdomain,
ipnsSubdomain,
subdomain,
subdomainGatewayPattern,
ipfsUrl,
ipnsUrl,
url,
pathGatewayPattern: pathGatewayPattern,
ipfsPath: (path) => isIpfs(path, pathPattern),
ipnsPath: (path) => isIpns(path, pathPattern),
path,
pathPattern,
urlOrPath: (x) => url(x) || path(x),
cidPath: path => isString(path) && !isCID(path) && isIpfs(`/ipfs/${path}`, pathPattern)
}
11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,19 @@
* isIPFS.urlOrPath('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
* isIPFS.urlOrPath('https://ipfs.io/ipns/github.com') // true
* isIPFS.urlOrPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
* isIPFS.urlOrPath('ipfs://QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
* isIPFS.urlOrPath('/ipns/github.com') // true
* isIPFS.urlOrPath('ipns://github.com') // true
* isIPFS.urlOrPath('https://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // true
* isIPFS.urlOrPath('https://google.com') // false
*
* isIPFS.ipfsUrl('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
* isIPFS.ipfsUrl('https://ipfs.io/ipfs/invalid-hash') // false
* isIPFS.ipfsUrl('ipfs://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
*
* isIPFS.ipnsUrl('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false
* isIPFS.ipnsUrl('https://ipfs.io/ipns/github.com') // true
* isIPFS.ipnsUrl('ipns://ipfs.io/ipns/github.com') // true
*
* isIPFS.ipfsPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
* isIPFS.ipfsPath('/ipfs/invalid-hash') // false
Expand Down Expand Up @@ -116,6 +120,9 @@ const subdomainProtocolMatch = 2
// Fully qualified domain name (FQDN) that has an explicit .tld suffix
const fqdnWithTld = /^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$/

// URI IANA-scheme
const uriSchemePattern = /^(ip[fn]s):\/\/([^/?#]+)/

function isMultihash (hash: Uint8Array | string): boolean {
const formatted = convertToString(hash)

Expand Down Expand Up @@ -320,13 +327,13 @@ export const subdomain = (url: string | Uint8Array): boolean => ipfsSubdomain(ur
* Returns `true` if the provided string is a valid IPFS url or `false`
* otherwise.
*/
export const ipfsUrl = (url: string | Uint8Array): boolean => isIpfs(url, pathGatewayPattern) || ipfsSubdomain(url)
export const ipfsUrl = (url: string | Uint8Array): boolean => isIpfs(url, pathGatewayPattern) || ipfsSubdomain(url) || isIpfs(url, uriSchemePattern)

/**
* Returns `true` if the provided string is a valid IPNS url or `false`
* otherwise.
*/
export const ipnsUrl = (url: string | Uint8Array): boolean => isIpns(url, pathGatewayPattern) || ipnsSubdomain(url)
export const ipnsUrl = (url: string | Uint8Array): boolean => isIpns(url, pathGatewayPattern) || ipnsSubdomain(url) || isIpns(url, uriSchemePattern)

/**
* Returns `true` if the provided string is a valid IPFS or IPNS url or `false`
Expand Down
15 changes: 13 additions & 2 deletions test/test-path.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ describe('ipfs path', () => {
done()
})

it('isIPFS.urlOrPath should match an IANA-schema compliant ipfs url', (done) => {
const actual = isIPFS.urlOrPath('ipfs://QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')
expect(actual).to.equal(true)
done()
})

it('isIPFS.urlOrPath should match ipns url', (done) => {
const actual = isIPFS.urlOrPath('http://ipfs.io/ipns/foo.bar.com')
expect(actual).to.equal(true)
Expand Down Expand Up @@ -153,7 +159,12 @@ describe('ipfs path', () => {
})

it('isIPFS.cidPath should not match an IPFS path', () => {
const actual = isIPFS.cidPath('/ipfs/QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')
expect(actual).to.equal(false)
expect(isIPFS.cidPath('/ipfs/QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')).to.be.false()
})
})

describe('ipns path', () => {
it('isIPFS.urlOrPath should match an IANA-schema compliant ipns url', () => {
expect(isIPFS.urlOrPath('ipns://QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')).to.be.true()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect(isIPFS.urlOrPath('ipns://QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')).to.be.true()
expect(isIPFS.urlOrPath('ipns://QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')).to.be.false()

ipns:// is not valid. Only keys and domains are valid.

})
})
32 changes: 32 additions & 0 deletions test/test-url.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ describe('ipfs url', () => {
done()
})

it('isIPFS.ipfsUrl should match an IANA-schema compliant ipfs uri', (done) => {
const actual = isIPFS.ipfsUrl('ipfs://QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')
expect(actual).to.equal(true)
done()
})

it('isIPFS.ipfsUrl should match a complex ipfs url', (done) => {
const actual = isIPFS.ipfsUrl('http://ipfs.alexandria.media/ipfs/QmeWz9YZEeNFXQhHg4PnR5ZiNr5isttgi5n1tc1eD5EfGU/content/index.html?arg=val#hash')
expect(actual).to.equal(true)
Expand Down Expand Up @@ -71,12 +77,24 @@ describe('ipfs url', () => {
done()
})

it('isIPFS.ipnsUrl should not match an IANA-schema compliant ipfs uri', (done) => {
const actual = isIPFS.ipnsUrl('ipfs://QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')
expect(actual).to.equal(false)
done()
})

it('isIPFS.url should match an ipfs url', (done) => {
const actual = isIPFS.url('http://ipfs.io/ipfs/QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')
expect(actual).to.equal(true)
done()
})

it('isIPFS.url should match an IANA-schema compliant ipfs uri', (done) => {
const actual = isIPFS.url('ipfs://QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')
expect(actual).to.equal(true)
done()
})

it('isIPFS.url should match an ipns url', (done) => {
const actual = isIPFS.url('http://ipfs.io/ipns/github.com/')
expect(actual).to.equal(true)
Expand All @@ -101,3 +119,17 @@ describe('ipfs url', () => {
done()
})
})

describe('ipns url', () => {
it('isIPFS.ipnsUrl should match an IANA-schema compliant ipns uri', () => {
expect(isIPFS.ipnsUrl('ipns://QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')).to.be.true()
})

it('isIPFS.ipnsUrl should not match an IANA-schema compliant ipfs uri', () => {
expect(isIPFS.ipnsUrl('ipfs://QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')).to.be.false()
})

it('isIPFS.url should match an IANA-schema compliant ipns uri', () => {
expect(isIPFS.url('ipns://QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm')).to.be.true()
})
})
Loading