From cc40cd486202db6a2ea1a6714c916af9cdcefe4f Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Fri, 20 Mar 2020 22:15:22 +0100 Subject: [PATCH 01/10] feat: support DNSLink subdomains This change adds support for DNSLink subdomains on localhost gateway (https://github.com/ipfs/go-ipfs/pull/6096) Example: en.wikipedia-on-ipfs.org.ipfs.localhost:8080 BREAKING CHANGE: `isIPFS.subdomain` now returns true for .ipns.localhost BREAKING CHANGE: `isIPFS.subdomainPattern` changed License: MIT Signed-off-by: Marcin Rataj --- .gitignore | 1 + README.md | 17 +++++--- package.json | 6 +-- src/index.js | 78 ++++++++++++++++++++++++++++--------- test/test-subdomain.spec.js | 39 +++++++++++++++++++ 5 files changed, 115 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 858990e..1fbd791 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ node_modules dist lib +docs diff --git a/README.md b/README.md index 43d82d8..fe91aea 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ $ npm install --save is-ipfs The code published to npm that gets loaded on require is in fact an ES5 transpiled version with the right shims added. This means that you can require it and use with your favorite bundler without having to adjust asset management process. ```js -var isIPFS = require('is-ipfs') +const isIPFS = require('is-ipfs') ``` @@ -98,6 +98,9 @@ isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j5 isIPFS.ipnsSubdomain('http://QmcNioXSC1bfJj1dcFErhUfyjFzoX2HodkRccsFFVJJvg8.ipns.dweb.link') // false isIPFS.ipnsSubdomain('http://foo-bar.ipns.dweb.link') // false (not a PeerID) +isIPFS.dnslinkSubdomain('http://en.wikipedia-on-ipfs.org.ipns.localhost:8080') // true +isIPFS.dnslinkSubdomain('http//bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') // false + isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234') // true isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234/http') // true isIPFS.multiaddr('/ip6/::1/udp/1234') // true @@ -116,7 +119,7 @@ A suite of util methods that provides efficient validation. Detection of IPFS Paths and identifiers in URLs is a two-stage process: 1. `urlPattern`/`pathPattern`/`subdomainPattern` regex is applied to quickly identify potential candidates -2. proper CID validation is applied to remove false-positives +2. proper CID/FQDN validation is applied to remove false-positives ## Content Identifiers @@ -178,15 +181,19 @@ Validated subdomain convention: `cidv1b32.ip(f|n)s.domain.tld` ### `isIPFS.subdomain(url)` -Returns `true` if the provided string includes a valid IPFS or IPNS subdomain or `false` otherwise. +Returns `true` if the provided `url` string includes a valid IPFS, IPNS or DNSLink subdomain or `false` otherwise. ### `isIPFS.ipfsSubdomain(url)` -Returns `true` if the provided string includes a valid IPFS subdomain or `false` otherwise. +Returns `true` if the provided `url` string includes a valid IPFS subdomain (case-insensitive CIDv1) or `false` otherwise. ### `isIPFS.ipnsSubdomain(url)` -Returns `true` if the provided string includes a valid IPNS subdomain or `false` otherwise. +Returns `true` if the provided `url` string includes a valid IPNS subdomain (CIDv1 with `libp2p-key` multicodec) or `false` otherwise. + +### `isIPFS.dnslinkSubdomain(url)` + +Returns `true` if the provided `url` string includes a valid DNSLink subdomain (such as `http://en.wikipedia-on-ipfs.org.ipns.localhost:8080`) or `false` otherwise. ## Multiaddrs diff --git a/package.json b/package.json index bbc5edb..8a514fa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "is-ipfs", "version": "0.6.3", - "description": "A set of utilities to help identify IPFS resources", + "description": "A set of utilities to help identify IPFS resources in URLs and paths", "leadMaintainer": "Marcin Rataj ", "main": "src/index.js", "browser": { @@ -34,11 +34,11 @@ "cids": "~0.7.0", "mafmt": "^7.0.0", "multiaddr": "^7.2.1", - "multibase": "~0.6.0", + "multibase": "~0.7.0", "multihashes": "~0.4.13" }, "devDependencies": { - "aegir": "^20.5.0", + "aegir": "^21.4.3", "chai": "^4.2.0", "pre-commit": "^1.2.2" }, diff --git a/src/index.js b/src/index.js index 91955ec..9e4b9d9 100644 --- a/src/index.js +++ b/src/index.js @@ -7,14 +7,22 @@ const Multiaddr = require('multiaddr') const mafmt = require('mafmt') const CID = require('cids') -const urlPattern = /^https?:\/\/[^/]+\/(ip(f|n)s)\/((\w+).*)/ -const pathPattern = /^\/(ip(f|n)s)\/((\w+).*)/ +const urlPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/]+)/ +const pathPattern = /^\/(ip[fn]s)\/([^/]+)/ const defaultProtocolMatch = 1 -const defaultHashMath = 4 - -const fqdnPattern = /^https?:\/\/([^/]+)\.(ip(?:f|n)s)\.[^/]+/ -const fqdnHashMatch = 1 -const fqdnProtocolMatch = 2 +const defaultHashMath = 2 + +// CID, libp2p-key or DNSLink +const subdomainPattern = /^https?:\/\/([^/]+)\.(ip[fs]s)\.[^/]+/ +const subdomainIdMatch = 1 +const subdomainProtocolMatch = 2 +// /ipfs/$cid represented as subdomain +const ipfsSubdomainPattern = /^https?:\/\/([^/]+)\.(ipfs)\.[^/]+/ +// /ipns/$libp2p-key represented as subdomain +const libp2pKeySubdomainPattern = /^https?:\/\/([^/]+)\.(ipns)\.[^/]+/ +// /ipns/$fqdn represented as subdomain +// (requires at least two DNS labels separated by ".") +const dnslinkSubdomainPattern = /^https?:\/\/([^.]+\.[^/]+)\.(ipns)\.[^/]+/ function isMultihash (hash) { const formatted = convertToString(hash) @@ -76,7 +84,7 @@ function isIpfs (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch let hash = match[hashMatch] - if (hash && pattern === fqdnPattern) { + if (hash && pattern === ipfsSubdomainPattern) { // when doing checks for subdomain context // ensure hash is case-insensitive // (browsers force-lowercase authority compotent anyway) @@ -100,18 +108,47 @@ function isIpns (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch return false } - if (hashMatch && pattern === fqdnPattern) { - let hash = match[hashMatch] + let ipnsId = match[hashMatch] + + if (ipnsId && pattern === libp2pKeySubdomainPattern) { // when doing checks for subdomain context // ensure hash is case-insensitive // (browsers force-lowercase authority compotent anyway) - hash = hash.toLowerCase() - return isCID(hash) + ipnsId = ipnsId.toLowerCase() + return isCID(ipnsId) } return true } +function isDNSLink (input, pattern, protocolMatch = defaultProtocolMatch, idMatch) { + const formatted = convertToString(input) + if (!formatted) { + return false + } + + const match = formatted.match(pattern) + if (!match) { + return false + } + + if (match[protocolMatch] !== 'ipns') { + return false + } + + const fqdn = match[idMatch] + + if (fqdn && pattern === dnslinkSubdomainPattern) { + try { + const { hostname } = new URL(`http://${fqdn}`) // eslint-disable-line no-new + return fqdn === hostname + } catch (e) { + return false + } + } + return false +} + function isString (input) { return typeof input === 'string' } @@ -128,8 +165,9 @@ function convertToString (input) { return false } -const ipfsSubdomain = (url) => isIpfs(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch) -const ipnsSubdomain = (url) => isIpns(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch) +const ipfsSubdomain = (url) => isIpfs(url, ipfsSubdomainPattern, subdomainProtocolMatch, subdomainIdMatch) +const ipnsSubdomain = (url) => isIpns(url, libp2pKeySubdomainPattern, subdomainProtocolMatch, subdomainIdMatch) +const dnslinkSubdomain = (url) => isDNSLink(url, dnslinkSubdomainPattern, subdomainProtocolMatch, subdomainIdMatch) module.exports = { multihash: isMultihash, @@ -137,10 +175,14 @@ module.exports = { peerMultiaddr: isPeerMultiaddr, cid: isCID, base32cid: (cid) => (isMultibase(cid) === 'base32' && isCID(cid)), - ipfsSubdomain: ipfsSubdomain, - ipnsSubdomain: ipnsSubdomain, - subdomain: (url) => (ipfsSubdomain(url) || ipnsSubdomain(url)), - subdomainPattern: fqdnPattern, + ipfsSubdomain, + ipnsSubdomain, + dnslinkSubdomain, + subdomain: (url) => (ipfsSubdomain(url) || ipnsSubdomain(url) || dnslinkSubdomain(url)), + subdomainPattern, + ipfsSubdomainPattern, + libp2pKeySubdomainPattern, + dnslinkSubdomainPattern, ipfsUrl: (url) => isIpfs(url, urlPattern), ipnsUrl: (url) => isIpns(url, urlPattern), url: (url) => (isIpfs(url, urlPattern) || isIpns(url, urlPattern)), diff --git a/test/test-subdomain.spec.js b/test/test-subdomain.spec.js index 23e0ffe..4439ef6 100644 --- a/test/test-subdomain.spec.js +++ b/test/test-subdomain.spec.js @@ -18,6 +18,12 @@ describe('ipfs subdomain', () => { done() }) + it('isIPFS.ipfsSubdomain should match localhost with port', (done) => { + const actual = isIPFS.ipfsSubdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.localhost:8080') + expect(actual).to.equal(true) + done() + }) + it('isIPFS.ipfsSubdomain should not match non-cid subdomains', (done) => { const actual = isIPFS.ipfsSubdomain('http://not-a-cid.ipfs.dweb.link') expect(actual).to.equal(false) @@ -87,6 +93,32 @@ describe('ipfs subdomain', () => { done() }) + it('isIPFS.dnslinkSubdomain should match .ipns.localhost zone with FQDN', (done) => { + // we do not support opaque strings in subdomains, only peerids + const actual = isIPFS.dnslinkSubdomain('http://docs.ipfs.io.ipns.localhost:8080/some/path') + expect(actual).to.equal(true) + done() + }) + + it('isIPFS.dnslinkSubdomain should match .ipns.sub.sub.domain.tld zone with FQDN', (done) => { + // we do not support opaque strings in subdomains, only peerids + const actual = isIPFS.dnslinkSubdomain('http://docs.ipfs.io.ipns.foo.bar.buzz.dweb.link') + expect(actual).to.equal(true) + done() + }) + + it('isIPFS.dnslinkSubdomain should match *.ipns. zone with FQDN', (done) => { + const actual = isIPFS.dnslinkSubdomain('http://docs.ipfs.io.ipns.locahost:8080') + expect(actual).to.equal(true) + done() + }) + + it('isIPFS.dnslinkSubdomain should not match a .ipns. zone with cidv1b32', (done) => { + const actual = isIPFS.dnslinkSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') + expect(actual).to.equal(false) + done() + }) + it('isIPFS.subdomain should match an ipfs subdomain', (done) => { const actual = isIPFS.subdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') expect(actual).to.equal(true) @@ -99,6 +131,13 @@ describe('ipfs subdomain', () => { done() }) + it('isIPFS.subdomain should match .ipns.localhost zone with FQDN', (done) => { + // we do not support opaque strings in subdomains, only peerids + const actual = isIPFS.subdomain('http://docs.ipfs.io.ipns.dweb.link') + expect(actual).to.equal(true) + done() + }) + it('isIPFS.subdomain should not match if fqdn does not start with cidv1b32', (done) => { const actual = isIPFS.subdomain('http://www.bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') expect(actual).to.equal(false) From 2644db0b4a469284464f7dfe2f59f64ebe7fa852 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Fri, 20 Mar 2020 22:26:01 +0100 Subject: [PATCH 02/10] test: support peer multiaddr with /p2p/ Context: https://github.com/libp2p/libp2p/issues/79 License: MIT Signed-off-by: Marcin Rataj --- README.md | 9 ++++++--- test/test-multiaddr.spec.js | 10 +++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fe91aea..fd37126 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ isIPFS.url('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o' isIPFS.url('https://ipfs.io/ipns/github.com') // true isIPFS.url('https://github.com/ipfs/js-ipfs/blob/master/README.md') // false isIPFS.url('https://google.com') // false +isIPFS.url('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // false (use .subdomain instead) +isIPFS.url('http://en.wikipedia-on-ipfs.org.ipfs.localhost:8080') // false (use .subdomain instead) isIPFS.path('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true isIPFS.path('/ipns/github.com') // true @@ -107,9 +109,10 @@ isIPFS.multiaddr('/ip6/::1/udp/1234') // true isIPFS.multiaddr('ip6/::1/udp/1234') // false isIPFS.multiaddr('/yoloinvalid/::1/udp/1234') // false -isIPFS.peerMultiaddr('/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4') // true -isIPFS.peerMultiaddr('/ip4/127.0.0.1/tcp/1234/ws/ipfs/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj') // true -isIPFS.peerMultiaddr('/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4/p2p-circuit/ipfs/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj') // true +isIPFS.peerMultiaddr('/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4') // true +isIPFS.peerMultiaddr('/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4') // true (legacy notation) +isIPFS.peerMultiaddr('/ip4/127.0.0.1/tcp/1234/ws/p2p/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj') // true +isIPFS.peerMultiaddr('/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4/p2p-circuit/p2p/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj') // true isIPFS.peerMultiaddr('/ip4/127.0.0.1/udp/1234') // false ``` diff --git a/test/test-multiaddr.spec.js b/test/test-multiaddr.spec.js index 1292f80..ab5cc6b 100644 --- a/test/test-multiaddr.spec.js +++ b/test/test-multiaddr.spec.js @@ -67,19 +67,27 @@ describe('ipfs peerMultiaddr', () => { // https://github.com/multiformats/js-mafmt/blob/v6.0.6/test/index.spec.js#L137 const goodCircuit = [ '/p2p-circuit', - '/p2p-circuit/ipfs/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj', + '/p2p-circuit/ipfs/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj', // /ipfs/ is legacy notation replaced with /p2p/ + '/p2p-circuit/p2p/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj', '/p2p-circuit/ip4/127.0.0.1/tcp/20008/ws/ipfs/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj', + '/p2p-circuit/ip4/127.0.0.1/tcp/20008/ws/p2p/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj', '/p2p-circuit/ip4/1.2.3.4/tcp/3456/ws/p2p-webrtc-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4', + '/p2p-circuit/ip4/1.2.3.4/tcp/3456/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4', + '/p2p-circuit/ip4/1.2.3.4/tcp/3456/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4', '/p2p-circuit/ip4/1.2.3.4/tcp/3456/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4', '/p2p-circuit/ip4/127.0.0.1/tcp/4002/ipfs/QmddWMcQX6orJGHpETYMyPgXrCXCtYANMFVDCvhKoDwLqA', '/p2p-circuit/ipfs/QmddWMcQX6orJGHpETYMyPgXrCXCtYANMFVDCvhKoDwLqA', + '/p2p-circuit/p2p/QmddWMcQX6orJGHpETYMyPgXrCXCtYANMFVDCvhKoDwLqA', '/p2p-circuit/ip4/127.0.0.1/tcp/20008/ws/ipfs/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj/' + 'p2p-circuit/ipfs/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj' ] // https://github.com/multiformats/js-mafmt/blob/v6.0.6/test/index.spec.js#L157 const validPeerMultiaddrs = [ '/ip4/127.0.0.1/tcp/20008/ws/ipfs/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj', + '/ip4/127.0.0.1/tcp/20008/ws/p2p/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj', + '/ip4/127.0.0.1/tcp/20008/ws/p2p/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5', // ed25519+identity multihash '/ip4/1.2.3.4/tcp/3456/ws/p2p-webrtc-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4', + '/ip4/1.2.3.4/tcp/3456/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4', '/ip4/1.2.3.4/tcp/3456/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4', '/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4/p2p-circuit', '/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4/p2p-circuit/ipfs/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj' From 72c0344fd91202505b3bb0b0e0e6b00081631828 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Sat, 21 Mar 2020 01:07:27 +0100 Subject: [PATCH 03/10] fix: explicitly ignore URL param and hash .url and .path now return true when validating: https://ipfs.io/ipfs/?filename=name.png#foo License: MIT Signed-off-by: Marcin Rataj --- README.md | 2 ++ package.json | 2 +- src/index.js | 4 ++-- test/test-path.spec.js | 4 ++-- test/test-url.spec.js | 4 ++-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fd37126..14f667d 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ isIPFS.base32cid('bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va') isIPFS.base32cid('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false isIPFS.url('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true +isIPFS.url('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?filename=guardian.jpg') // true isIPFS.url('https://ipfs.io/ipns/github.com') // true isIPFS.url('https://github.com/ipfs/js-ipfs/blob/master/README.md') // false isIPFS.url('https://google.com') // false @@ -60,6 +61,7 @@ isIPFS.url('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.i isIPFS.url('http://en.wikipedia-on-ipfs.org.ipfs.localhost:8080') // false (use .subdomain instead) isIPFS.path('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true +isIPFS.path('/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?filename=guardian.jpg') // true isIPFS.path('/ipns/github.com') // true isIPFS.path('/ipfs/js-ipfs/blob/master/README.md') // false diff --git a/package.json b/package.json index 8a514fa..084e2a8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "is-ipfs", "version": "0.6.3", - "description": "A set of utilities to help identify IPFS resources in URLs and paths", + "description": "A set of utilities to help identify IPFS resources on the web", "leadMaintainer": "Marcin Rataj ", "main": "src/index.js", "browser": { diff --git a/src/index.js b/src/index.js index 9e4b9d9..c9a6964 100644 --- a/src/index.js +++ b/src/index.js @@ -7,8 +7,8 @@ const Multiaddr = require('multiaddr') const mafmt = require('mafmt') const CID = require('cids') -const urlPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/]+)/ -const pathPattern = /^\/(ip[fn]s)\/([^/]+)/ +const urlPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/?#]+)/ +const pathPattern = /^\/(ip[fn]s)\/([^/?#]+)/ const defaultProtocolMatch = 1 const defaultHashMath = 2 diff --git a/test/test-path.spec.js b/test/test-path.spec.js index e568a08..39ede98 100644 --- a/test/test-path.spec.js +++ b/test/test-path.spec.js @@ -7,13 +7,13 @@ const expect = require('chai').expect describe('ipfs path', () => { it('isIPFS.ipfsPath should match an ipfs path', (done) => { - const actual = isIPFS.ipfsPath('/ipfs/QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm') + const actual = isIPFS.ipfsPath('/ipfs/QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm?arg=val#hash') expect(actual).to.equal(true) done() }) it('isIPFS.ipfsPath should match a complex ipfs path', (done) => { - const actual = isIPFS.ipfsPath('/ipfs/QmeWz9YZEeNFXQhHg4PnR5ZiNr5isttgi5n1tc1eD5EfGU/content/index.html') + const actual = isIPFS.ipfsPath('/ipfs/QmeWz9YZEeNFXQhHg4PnR5ZiNr5isttgi5n1tc1eD5EfGU/content/index.html?arg=val#hash') expect(actual).to.equal(true) done() }) diff --git a/test/test-url.spec.js b/test/test-url.spec.js index dcfaf1c..3df3326 100644 --- a/test/test-url.spec.js +++ b/test/test-url.spec.js @@ -7,13 +7,13 @@ const isIPFS = require('../src/index') describe('ipfs url', () => { it('isIPFS.ipfsUrl should match an ipfs url', (done) => { - const actual = isIPFS.ipfsUrl('http://ipfs.io/ipfs/QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm') + const actual = isIPFS.ipfsUrl('http://ipfs.io/ipfs/QmYHNYAaYK5hm3ZhZFx5W9H6xydKDGimjdgJMrMSdnctEm?arg=val#hash') 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') + const actual = isIPFS.ipfsUrl('http://ipfs.alexandria.media/ipfs/QmeWz9YZEeNFXQhHg4PnR5ZiNr5isttgi5n1tc1eD5EfGU/content/index.html?arg=val#hash') expect(actual).to.equal(true) done() }) From f1823cc0b12e97ac1f202ba7c06663b5b9b5d265 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Sun, 22 Mar 2020 23:49:26 +0100 Subject: [PATCH 04/10] refactor: simplify dnslinkSubdomain License: MIT Signed-off-by: Marcin Rataj --- README.md | 20 +++++++++++++++++--- src/index.js | 31 +++++++++++++------------------ test/test-subdomain.spec.js | 10 ++++++++-- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 14f667d..c541d95 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ isIPFS.ipnsSubdomain('http://foo-bar.ipns.dweb.link') // false (not a PeerID) isIPFS.dnslinkSubdomain('http://en.wikipedia-on-ipfs.org.ipns.localhost:8080') // true isIPFS.dnslinkSubdomain('http//bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') // false +isIPFS.dnslinkSubdomain('http//hostname-without-tld.ipns.dweb.link') // false isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234') // true isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234/http') // true @@ -186,7 +187,7 @@ Validated subdomain convention: `cidv1b32.ip(f|n)s.domain.tld` ### `isIPFS.subdomain(url)` -Returns `true` if the provided `url` string includes a valid IPFS, IPNS or DNSLink subdomain or `false` otherwise. +Returns `true` if the provided `url` string includes a valid IPFS, looks like IPNS or DNSLink subdomain or `false` otherwise. ### `isIPFS.ipfsSubdomain(url)` @@ -194,11 +195,24 @@ Returns `true` if the provided `url` string includes a valid IPFS subdomain (cas ### `isIPFS.ipnsSubdomain(url)` -Returns `true` if the provided `url` string includes a valid IPNS subdomain (CIDv1 with `libp2p-key` multicodec) or `false` otherwise. +Returns `true` if the provided `url` string looks like a valid IPNS subdomain +(subdomain context requires CIDv1 with `libp2p-key` multicodec) or `false` +otherwise. + +**Note:** `ipnsSubdomain` method works in offline mode: it does not perform +actual IPNS record lookup over DHT or other content routing method. It may +return false-positives. To ensure IPNS record exists, make a call to +`/api/v0/name/resolve?arg=` ### `isIPFS.dnslinkSubdomain(url)` -Returns `true` if the provided `url` string includes a valid DNSLink subdomain (such as `http://en.wikipedia-on-ipfs.org.ipns.localhost:8080`) or `false` otherwise. +Returns `true` if the provided `url` string looks like a valid DNSLink +subdomain (such as `http://en.wikipedia-on-ipfs.org.ipns.localhost:8080`) or +`false` otherwise. + +**Note:** `dnslinkSubdomain` method works in offline mode: it does not perform +actual DNSLink lookup. It may return false-positives. To ensure DNSLink exists, +make a call to `/api/v0/dns?arg=` ## Multiaddrs diff --git a/src/index.js b/src/index.js index c9a6964..fd38c18 100644 --- a/src/index.js +++ b/src/index.js @@ -13,16 +13,12 @@ const defaultProtocolMatch = 1 const defaultHashMath = 2 // CID, libp2p-key or DNSLink -const subdomainPattern = /^https?:\/\/([^/]+)\.(ip[fs]s)\.[^/]+/ +const subdomainPattern = /^https?:\/\/([^/]+)\.(ip[fn]s)\.[^/?]+/ const subdomainIdMatch = 1 const subdomainProtocolMatch = 2 -// /ipfs/$cid represented as subdomain -const ipfsSubdomainPattern = /^https?:\/\/([^/]+)\.(ipfs)\.[^/]+/ -// /ipns/$libp2p-key represented as subdomain -const libp2pKeySubdomainPattern = /^https?:\/\/([^/]+)\.(ipns)\.[^/]+/ -// /ipns/$fqdn represented as subdomain -// (requires at least two DNS labels separated by ".") -const dnslinkSubdomainPattern = /^https?:\/\/([^.]+\.[^/]+)\.(ipns)\.[^/]+/ + +// 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) @@ -84,7 +80,7 @@ function isIpfs (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch let hash = match[hashMatch] - if (hash && pattern === ipfsSubdomainPattern) { + if (hash && pattern === subdomainPattern) { // when doing checks for subdomain context // ensure hash is case-insensitive // (browsers force-lowercase authority compotent anyway) @@ -110,7 +106,7 @@ function isIpns (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch let ipnsId = match[hashMatch] - if (ipnsId && pattern === libp2pKeySubdomainPattern) { + if (ipnsId && pattern === subdomainPattern) { // when doing checks for subdomain context // ensure hash is case-insensitive // (browsers force-lowercase authority compotent anyway) @@ -138,10 +134,12 @@ function isDNSLink (input, pattern, protocolMatch = defaultProtocolMatch, idMatc const fqdn = match[idMatch] - if (fqdn && pattern === dnslinkSubdomainPattern) { + if (fqdn && pattern === subdomainPattern) { try { + // URL implementation in web browsers forces lowercase of the hostname const { hostname } = new URL(`http://${fqdn}`) // eslint-disable-line no-new - return fqdn === hostname + // Confirm fqdn has an explicit TLD + return fqdnWithTld.test(hostname) } catch (e) { return false } @@ -165,9 +163,9 @@ function convertToString (input) { return false } -const ipfsSubdomain = (url) => isIpfs(url, ipfsSubdomainPattern, subdomainProtocolMatch, subdomainIdMatch) -const ipnsSubdomain = (url) => isIpns(url, libp2pKeySubdomainPattern, subdomainProtocolMatch, subdomainIdMatch) -const dnslinkSubdomain = (url) => isDNSLink(url, dnslinkSubdomainPattern, subdomainProtocolMatch, subdomainIdMatch) +const ipfsSubdomain = (url) => isIpfs(url, subdomainPattern, subdomainProtocolMatch, subdomainIdMatch) +const ipnsSubdomain = (url) => isIpns(url, subdomainPattern, subdomainProtocolMatch, subdomainIdMatch) +const dnslinkSubdomain = (url) => isDNSLink(url, subdomainPattern, subdomainProtocolMatch, subdomainIdMatch) module.exports = { multihash: isMultihash, @@ -180,9 +178,6 @@ module.exports = { dnslinkSubdomain, subdomain: (url) => (ipfsSubdomain(url) || ipnsSubdomain(url) || dnslinkSubdomain(url)), subdomainPattern, - ipfsSubdomainPattern, - libp2pKeySubdomainPattern, - dnslinkSubdomainPattern, ipfsUrl: (url) => isIpfs(url, urlPattern), ipnsUrl: (url) => isIpns(url, urlPattern), url: (url) => (isIpfs(url, urlPattern) || isIpns(url, urlPattern)), diff --git a/test/test-subdomain.spec.js b/test/test-subdomain.spec.js index 4439ef6..3865718 100644 --- a/test/test-subdomain.spec.js +++ b/test/test-subdomain.spec.js @@ -119,6 +119,12 @@ describe('ipfs subdomain', () => { done() }) + it('isIPFS.dnslinkSubdomain should not match if *.ipns is not a fqdn with tld', (done) => { + const actual = isIPFS.dnslinkSubdomain('http://no-fqdn-with-tld.ipns.dweb.link') + expect(actual).to.equal(false) + done() + }) + it('isIPFS.subdomain should match an ipfs subdomain', (done) => { const actual = isIPFS.subdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') expect(actual).to.equal(true) @@ -150,8 +156,8 @@ describe('ipfs subdomain', () => { done() }) - it('isIPFS.subdomain should not match if ipns peerid is invalid', (done) => { - const actual = isIPFS.subdomain('http://not-a-cid.ipns.dweb.link') + it('isIPFS.subdomain should not match if *.ipns is not libp2pkey nor fqdn', (done) => { + const actual = isIPFS.subdomain('http://not-a-cid-or-dnslink.ipns.dweb.link') expect(actual).to.equal(false) done() }) From c520efce6310a63c20e22bb421886ee03c59ffd3 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 25 Mar 2020 00:22:02 +0100 Subject: [PATCH 05/10] fix: url() check should include subdomain() When .url was created we only had path gateways. When .subdomain was added, we did not update .url to test for subdomain gateways, which in the long run will confuse people and feels like a bug. Let's fix this: .url() will now check for both subdomain and path gateways https://github.com/ipfs/is-ipfs/pull/32#discussion_r396161665 BREAKING CHANGE: .url(url) now returns true if .subdomain(url) is true License: MIT Signed-off-by: Marcin Rataj --- README.md | 10 +++++----- src/index.js | 37 ++++++++++++++++++++----------------- test/test-subdomain.spec.js | 34 +++++++++++++++++++++++++--------- 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index c541d95..d894be5 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,10 @@ isIPFS.base32cid('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false isIPFS.url('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true isIPFS.url('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?filename=guardian.jpg') // true isIPFS.url('https://ipfs.io/ipns/github.com') // true +isIPFS.url('https://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // true +isIPFS.url('http://en.wikipedia-on-ipfs.org.ipfs.localhost:8080') // true isIPFS.url('https://github.com/ipfs/js-ipfs/blob/master/README.md') // false isIPFS.url('https://google.com') // false -isIPFS.url('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // false (use .subdomain instead) -isIPFS.url('http://en.wikipedia-on-ipfs.org.ipfs.localhost:8080') // false (use .subdomain instead) isIPFS.path('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true isIPFS.path('/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?filename=guardian.jpg') // true @@ -69,6 +69,7 @@ isIPFS.urlOrPath('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFo isIPFS.urlOrPath('https://ipfs.io/ipns/github.com') // true isIPFS.urlOrPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // 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 @@ -124,9 +125,8 @@ isIPFS.peerMultiaddr('/ip4/127.0.0.1/udp/1234') // false A suite of util methods that provides efficient validation. Detection of IPFS Paths and identifiers in URLs is a two-stage process: -1. `urlPattern`/`pathPattern`/`subdomainPattern` regex is applied to quickly identify potential candidates -2. proper CID/FQDN validation is applied to remove false-positives - +1. `pathPattern`/`pathGatewayPattern`/`subdomainGatewayPattern` regex is applied to quickly identify potential candidates +2. proper CID validation is applied to remove false-positives ## Content Identifiers diff --git a/src/index.js b/src/index.js index fd38c18..b58a868 100644 --- a/src/index.js +++ b/src/index.js @@ -7,13 +7,13 @@ const Multiaddr = require('multiaddr') const mafmt = require('mafmt') const CID = require('cids') -const urlPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/?#]+)/ +const pathGatewayPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/?#]+)/ const pathPattern = /^\/(ip[fn]s)\/([^/?#]+)/ const defaultProtocolMatch = 1 const defaultHashMath = 2 // CID, libp2p-key or DNSLink -const subdomainPattern = /^https?:\/\/([^/]+)\.(ip[fn]s)\.[^/?]+/ +const subdomainGatewayPattern = /^https?:\/\/([^/]+)\.(ip[fn]s)\.[^/?]+/ const subdomainIdMatch = 1 const subdomainProtocolMatch = 2 @@ -80,7 +80,7 @@ function isIpfs (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch let hash = match[hashMatch] - if (hash && pattern === subdomainPattern) { + if (hash && pattern === subdomainGatewayPattern) { // when doing checks for subdomain context // ensure hash is case-insensitive // (browsers force-lowercase authority compotent anyway) @@ -106,7 +106,7 @@ function isIpns (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch let ipnsId = match[hashMatch] - if (ipnsId && pattern === subdomainPattern) { + if (ipnsId && pattern === subdomainGatewayPattern) { // when doing checks for subdomain context // ensure hash is case-insensitive // (browsers force-lowercase authority compotent anyway) @@ -134,7 +134,7 @@ function isDNSLink (input, pattern, protocolMatch = defaultProtocolMatch, idMatc const fqdn = match[idMatch] - if (fqdn && pattern === subdomainPattern) { + if (fqdn && pattern === subdomainGatewayPattern) { try { // URL implementation in web browsers forces lowercase of the hostname const { hostname } = new URL(`http://${fqdn}`) // eslint-disable-line no-new @@ -163,9 +163,12 @@ function convertToString (input) { return false } -const ipfsSubdomain = (url) => isIpfs(url, subdomainPattern, subdomainProtocolMatch, subdomainIdMatch) -const ipnsSubdomain = (url) => isIpns(url, subdomainPattern, subdomainProtocolMatch, subdomainIdMatch) -const dnslinkSubdomain = (url) => isDNSLink(url, subdomainPattern, subdomainProtocolMatch, subdomainIdMatch) +const url = (url) => (isIpfs(url, pathGatewayPattern) || isIpns(url, pathGatewayPattern) || subdomain(url)) +const path = (path) => (isIpfs(path, pathPattern) || isIpns(path, pathPattern)) +const ipfsSubdomain = (url) => isIpfs(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) +const ipnsSubdomain = (url) => isIpns(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) +const dnslinkSubdomain = (url) => isDNSLink(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) +const subdomain = (url) => (ipfsSubdomain(url) || ipnsSubdomain(url) || dnslinkSubdomain(url)) module.exports = { multihash: isMultihash, @@ -176,16 +179,16 @@ module.exports = { ipfsSubdomain, ipnsSubdomain, dnslinkSubdomain, - subdomain: (url) => (ipfsSubdomain(url) || ipnsSubdomain(url) || dnslinkSubdomain(url)), - subdomainPattern, - ipfsUrl: (url) => isIpfs(url, urlPattern), - ipnsUrl: (url) => isIpns(url, urlPattern), - url: (url) => (isIpfs(url, urlPattern) || isIpns(url, urlPattern)), - urlPattern: urlPattern, + subdomain, + subdomainGatewayPattern, + ipfsUrl: (url) => isIpfs(url, pathGatewayPattern), + ipnsUrl: (url) => isIpns(url, pathGatewayPattern), + url, + pathGatewayPattern: pathGatewayPattern, ipfsPath: (path) => isIpfs(path, pathPattern), ipnsPath: (path) => isIpns(path, pathPattern), - path: (path) => (isIpfs(path, pathPattern) || isIpns(path, pathPattern)), - pathPattern: pathPattern, - urlOrPath: (x) => (isIpfs(x, urlPattern) || isIpns(x, urlPattern) || isIpfs(x, pathPattern) || isIpns(x, pathPattern)), + path, + pathPattern, + urlOrPath: (x) => (url(x) || path(x)), cidPath: path => isString(path) && !isCID(path) && isIpfs(`/ipfs/${path}`, pathPattern) } diff --git a/test/test-subdomain.spec.js b/test/test-subdomain.spec.js index 3865718..79eb186 100644 --- a/test/test-subdomain.spec.js +++ b/test/test-subdomain.spec.js @@ -168,23 +168,39 @@ describe('ipfs subdomain', () => { done() }) - /* We keep subdomain logic separate from legacy urlOrPath checks, below is a fail-safe to ensure we keep that behavior */ - - it('isIPFS.urlOrPath should not match ipfs url with cidv1b32 subdomain', (done) => { + it('isIPFS.urlOrPath should match ipfs url with cidv1b32 subdomain', (done) => { const actual = isIPFS.urlOrPath('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') - expect(actual).to.equal(false) + expect(actual).to.equal(true) done() }) - it('isIPFS.urlOrPath should not match ipns url', (done) => { + it('isIPFS.urlOrPath should match subdomain ipns', (done) => { const actual = isIPFS.urlOrPath('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') - expect(actual).to.equal(false) + expect(actual).to.equal(true) done() }) - it('isIPFS.urlOrPath should not match ipns in subdomain', (done) => { - const actual = isIPFS.urlOrPath('http://a-dnslink-website.com.ipns.dweb.link') - expect(actual).to.equal(false) + it('isIPFS.urlOrPath should match potential DNSLink in subdomain', (done) => { + const actual = isIPFS.urlOrPath('http://a-dnslink-website.com.ipns.localhost:8080') + expect(actual).to.equal(true) + done() + }) + + it('isIPFS.url should match ipfs url with cidv1b32 subdomain', (done) => { + const actual = isIPFS.url('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') + expect(actual).to.equal(true) + done() + }) + + it('isIPFS.url should match subdomain ipns', (done) => { + const actual = isIPFS.url('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') + expect(actual).to.equal(true) + done() + }) + + it('isIPFS.url should match potential DNSLink in subdomain', (done) => { + const actual = isIPFS.url('http://a-dnslink-website.com.ipns.localhost:8080') + expect(actual).to.equal(true) done() }) }) From d9e7082587595a5c7a192405342fae18865c33f9 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 25 Mar 2020 01:05:56 +0100 Subject: [PATCH 06/10] refactor: merge DNSLink check into ipnsSubdomain() This makes subdomain checks follow what path gateway checks do, removing confusion. In both cases (IPNS and DNSLink) user needs to perform online record check, so this is just a handy way of detecting potential matches. License: MIT Signed-off-by: Marcin Rataj --- README.md | 24 +++++------------ src/index.js | 54 +++++++++++++------------------------ test/test-subdomain.spec.js | 31 ++++++++------------- 3 files changed, 36 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index d894be5..3eb1a0e 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,8 @@ isIPFS.ipfsSubdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27 isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') // true isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.dweb.link') // false isIPFS.ipnsSubdomain('http://QmcNioXSC1bfJj1dcFErhUfyjFzoX2HodkRccsFFVJJvg8.ipns.dweb.link') // false -isIPFS.ipnsSubdomain('http://foo-bar.ipns.dweb.link') // false (not a PeerID) - -isIPFS.dnslinkSubdomain('http://en.wikipedia-on-ipfs.org.ipns.localhost:8080') // true -isIPFS.dnslinkSubdomain('http//bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') // false -isIPFS.dnslinkSubdomain('http//hostname-without-tld.ipns.dweb.link') // false +isIPFS.ipnsSubdomain('http://en.wikipedia-on-ipfs.org.ipns.localhost:8080') // true (assuming DNSLink) +isIPFS.ipnsSubdomain('http://hostname-without-tld.ipns.dweb.link') // false (missing TLD) isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234') // true isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234/http') // true @@ -187,7 +184,7 @@ Validated subdomain convention: `cidv1b32.ip(f|n)s.domain.tld` ### `isIPFS.subdomain(url)` -Returns `true` if the provided `url` string includes a valid IPFS, looks like IPNS or DNSLink subdomain or `false` otherwise. +Returns `true` if the provided `url` string includes a valid IPFS, looks like IPNS/DNSLink subdomain or `false` otherwise. ### `isIPFS.ipfsSubdomain(url)` @@ -196,23 +193,16 @@ Returns `true` if the provided `url` string includes a valid IPFS subdomain (cas ### `isIPFS.ipnsSubdomain(url)` Returns `true` if the provided `url` string looks like a valid IPNS subdomain -(subdomain context requires CIDv1 with `libp2p-key` multicodec) or `false` +(CIDv1 with `libp2p-key` multicodec or something that looks like a FQDN, for example `en.wikipedia-on-ipfs.org.ipns.localhost:8080`) or `false` otherwise. **Note:** `ipnsSubdomain` method works in offline mode: it does not perform actual IPNS record lookup over DHT or other content routing method. It may -return false-positives. To ensure IPNS record exists, make a call to -`/api/v0/name/resolve?arg=` - -### `isIPFS.dnslinkSubdomain(url)` +return false-positives: -Returns `true` if the provided `url` string looks like a valid DNSLink -subdomain (such as `http://en.wikipedia-on-ipfs.org.ipns.localhost:8080`) or -`false` otherwise. +- To ensure IPNS record exists, make a call to `/api/v0/name/resolve?arg=` +- To ensure DNSLink exists, make a call to `/api/v0/dns?arg=` -**Note:** `dnslinkSubdomain` method works in offline mode: it does not perform -actual DNSLink lookup. It may return false-positives. To ensure DNSLink exists, -make a call to `/api/v0/dns?arg=` ## Multiaddrs diff --git a/src/index.js b/src/index.js index b58a868..9b03cbb 100644 --- a/src/index.js +++ b/src/index.js @@ -108,43 +108,23 @@ function isIpns (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch if (ipnsId && pattern === subdomainGatewayPattern) { // when doing checks for subdomain context - // ensure hash is case-insensitive + // ensure ipnsId is case-insensitive // (browsers force-lowercase authority compotent anyway) ipnsId = ipnsId.toLowerCase() - return isCID(ipnsId) - } - - return true -} - -function isDNSLink (input, pattern, protocolMatch = defaultProtocolMatch, idMatch) { - const formatted = convertToString(input) - if (!formatted) { - return false - } - - const match = formatted.match(pattern) - if (!match) { - return false - } - - if (match[protocolMatch] !== 'ipns') { - return false - } - - const fqdn = match[idMatch] - - if (fqdn && pattern === subdomainGatewayPattern) { + // Check if it is cidv1 + if (isCID(ipnsId)) return true + // Check if it looks like FQDN try { // URL implementation in web browsers forces lowercase of the hostname - const { hostname } = new URL(`http://${fqdn}`) // eslint-disable-line no-new - // Confirm fqdn has an explicit TLD + 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 false + + return true } function isString (input) { @@ -163,12 +143,15 @@ function convertToString (input) { return false } -const url = (url) => (isIpfs(url, pathGatewayPattern) || isIpns(url, pathGatewayPattern) || subdomain(url)) -const path = (path) => (isIpfs(path, pathPattern) || isIpns(path, pathPattern)) const ipfsSubdomain = (url) => isIpfs(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) const ipnsSubdomain = (url) => isIpns(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) -const dnslinkSubdomain = (url) => isDNSLink(url, subdomainGatewayPattern, subdomainProtocolMatch, subdomainIdMatch) -const subdomain = (url) => (ipfsSubdomain(url) || ipnsSubdomain(url) || dnslinkSubdomain(url)) +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, @@ -178,17 +161,16 @@ module.exports = { base32cid: (cid) => (isMultibase(cid) === 'base32' && isCID(cid)), ipfsSubdomain, ipnsSubdomain, - dnslinkSubdomain, subdomain, subdomainGatewayPattern, - ipfsUrl: (url) => isIpfs(url, pathGatewayPattern), - ipnsUrl: (url) => isIpns(url, pathGatewayPattern), + ipfsUrl, + ipnsUrl, url, pathGatewayPattern: pathGatewayPattern, ipfsPath: (path) => isIpfs(path, pathPattern), ipnsPath: (path) => isIpns(path, pathPattern), path, pathPattern, - urlOrPath: (x) => (url(x) || path(x)), + urlOrPath: (x) => url(x) || path(x), cidPath: path => isString(path) && !isCID(path) && isIpfs(`/ipfs/${path}`, pathPattern) } diff --git a/test/test-subdomain.spec.js b/test/test-subdomain.spec.js index 79eb186..89634a9 100644 --- a/test/test-subdomain.spec.js +++ b/test/test-subdomain.spec.js @@ -74,13 +74,6 @@ describe('ipfs subdomain', () => { done() }) - it('isIPFS.ipnsSubdomain should not match .ipns. zone with non-cid subdomain', (done) => { - // we do not support opaque strings in subdomains, only peerids - const actual = isIPFS.ipnsSubdomain('http://a-dnslink-website.com.ipns.dweb.link') - expect(actual).to.equal(false) - done() - }) - it('isIPFS.ipnsSubdomain should not match without .ipns. zone', (done) => { const actual = isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.dweb.link') expect(actual).to.equal(false) @@ -93,34 +86,32 @@ describe('ipfs subdomain', () => { done() }) - it('isIPFS.dnslinkSubdomain should match .ipns.localhost zone with FQDN', (done) => { - // we do not support opaque strings in subdomains, only peerids - const actual = isIPFS.dnslinkSubdomain('http://docs.ipfs.io.ipns.localhost:8080/some/path') + it('isIPFS.ipnsSubdomain should match .ipns.localhost zone with FQDN', (done) => { + const actual = isIPFS.ipnsSubdomain('http://docs.ipfs.io.ipns.localhost:8080/some/path') expect(actual).to.equal(true) done() }) - it('isIPFS.dnslinkSubdomain should match .ipns.sub.sub.domain.tld zone with FQDN', (done) => { - // we do not support opaque strings in subdomains, only peerids - const actual = isIPFS.dnslinkSubdomain('http://docs.ipfs.io.ipns.foo.bar.buzz.dweb.link') + it('isIPFS.ipnsSubdomain should match .ipns.sub.sub.domain.tld zone with FQDN', (done) => { + const actual = isIPFS.ipnsSubdomain('http://docs.ipfs.io.ipns.foo.bar.buzz.dweb.link') expect(actual).to.equal(true) done() }) - it('isIPFS.dnslinkSubdomain should match *.ipns. zone with FQDN', (done) => { - const actual = isIPFS.dnslinkSubdomain('http://docs.ipfs.io.ipns.locahost:8080') + it('isIPFS.ipnsSubdomain should match *.ipns. zone with FQDN', (done) => { + const actual = isIPFS.ipnsSubdomain('http://docs.ipfs.io.ipns.locahost:8080') expect(actual).to.equal(true) done() }) - it('isIPFS.dnslinkSubdomain should not match a .ipns. zone with cidv1b32', (done) => { - const actual = isIPFS.dnslinkSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') - expect(actual).to.equal(false) + it('isIPFS.ipnsSubdomain should match .ipns. zone with cidv1b32', (done) => { + const actual = isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') + expect(actual).to.equal(true) done() }) - it('isIPFS.dnslinkSubdomain should not match if *.ipns is not a fqdn with tld', (done) => { - const actual = isIPFS.dnslinkSubdomain('http://no-fqdn-with-tld.ipns.dweb.link') + it('isIPFS.ipnsSubdomain should not match if *.ipns is not a fqdn with tld', (done) => { + const actual = isIPFS.ipnsSubdomain('http://no-fqdn-with-tld.ipns.dweb.link') expect(actual).to.equal(false) done() }) From d5717e910d864d738faf39480897342c7cbf065d Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 26 Mar 2020 13:51:23 +0100 Subject: [PATCH 07/10] docs: update examples License: MIT Signed-off-by: Marcin Rataj --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3eb1a0e..b5c3ed1 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ isIPFS.url('https://github.com/ipfs/js-ipfs/blob/master/README.md') // false isIPFS.url('https://google.com') // false isIPFS.path('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true -isIPFS.path('/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?filename=guardian.jpg') // true +isIPFS.path('/ipfs/QmbcBPAwCDxRMB1Qe7CRQmxdrTSkxKwM9y6rZw2FjGtbsb/?weird-filename=test.jpg') // true isIPFS.path('/ipns/github.com') // true isIPFS.path('/ipfs/js-ipfs/blob/master/README.md') // false From 87d746a1d219abb61384eaf6cd8db74c54a9fc4a Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Sun, 5 Apr 2020 17:12:35 +0200 Subject: [PATCH 08/10] refactor: switch to iso-url License: MIT Signed-off-by: Marcin Rataj --- package.json | 1 + src/index.js | 1 + 2 files changed, 2 insertions(+) diff --git a/package.json b/package.json index 084e2a8..b5d2095 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "dependencies": { "bs58": "^4.0.1", "cids": "~0.7.0", + "iso-url": "~0.4.7", "mafmt": "^7.0.0", "multiaddr": "^7.2.1", "multibase": "~0.7.0", diff --git a/src/index.js b/src/index.js index 9b03cbb..b1f778e 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ const multibase = require('multibase') const Multiaddr = require('multiaddr') const mafmt = require('mafmt') const CID = require('cids') +const { URL } = require('iso-url') const pathGatewayPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/?#]+)/ const pathPattern = /^\/(ip[fn]s)\/([^/?#]+)/ From 404957eca32fc41bd13c70df3db59ee669116cc3 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Sun, 5 Apr 2020 17:32:51 +0200 Subject: [PATCH 09/10] refactor: lint-package-json License: MIT Signed-off-by: Marcin Rataj --- package.json | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index b5d2095..8bd3dcb 100644 --- a/package.json +++ b/package.json @@ -2,16 +2,37 @@ "name": "is-ipfs", "version": "0.6.3", "description": "A set of utilities to help identify IPFS resources on the web", + "keywords": [ + "js-ipfs", + "ipns", + "gateway", + "dnslink", + "ipfs" + ], + "homepage": "https://github.com/ipfs/is-ipfs", + "bugs": { + "url": "https://github.com/ipfs/is-ipfs/issues" + }, + "license": "MIT", + "author": "Francisco Dias (http://franciscodias.net/)", "leadMaintainer": "Marcin Rataj ", + "files": [ + "src", + "dist" + ], "main": "src/index.js", "browser": { "fs": false }, + "repository": { + "type": "git", + "url": "https://github.com/ipfs/is-ipfs.git" + }, "scripts": { "test:node": "aegir test --target node", "test:browser": "aegir test --target browser", "test": "aegir test", - "lint": "aegir lint", + "lint": "aegir lint && aegir lint-package-json", "release": "aegir release", "release-minor": "aegir release --type minor", "release-major": "aegir release --type major", @@ -19,16 +40,6 @@ "coverage": "aegir coverage", "coverage-publish": "aegir coverage --upload" }, - "pre-commit": [ - "test", - "lint" - ], - "keywords": [ - "js-ipfs", - "ipfs" - ], - "author": "Francisco Dias (http://franciscodias.net/)", - "license": "MIT", "dependencies": { "bs58": "^4.0.1", "cids": "~0.7.0", @@ -43,14 +54,14 @@ "chai": "^4.2.0", "pre-commit": "^1.2.2" }, - "repository": { - "type": "git", - "url": "https://github.com/ipfs/is-ipfs.git" - }, - "bugs": { - "url": "https://github.com/ipfs/is-ipfs/issues" + "engines": { + "node": ">=10.0.0", + "npm": ">=6.0.0" }, - "homepage": "https://github.com/ipfs/is-ipfs", + "pre-commit": [ + "test", + "lint" + ], "contributors": [ "Alan Shaw ", "David Dias ", From 18d80d4f3dd4d1f28c700fb156efbf52aeacbd69 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Sun, 5 Apr 2020 17:41:27 +0200 Subject: [PATCH 10/10] chore: update deps License: MIT Signed-off-by: Marcin Rataj --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8bd3dcb..717373e 100644 --- a/package.json +++ b/package.json @@ -42,15 +42,15 @@ }, "dependencies": { "bs58": "^4.0.1", - "cids": "~0.7.0", + "cids": "~0.8.0", "iso-url": "~0.4.7", - "mafmt": "^7.0.0", - "multiaddr": "^7.2.1", + "mafmt": "^7.1.0", + "multiaddr": "^7.4.3", "multibase": "~0.7.0", - "multihashes": "~0.4.13" + "multihashes": "~0.4.19" }, "devDependencies": { - "aegir": "^21.4.3", + "aegir": "^21.4.5", "chai": "^4.2.0", "pre-commit": "^1.2.2" },