diff --git a/.travis.yml b/.travis.yml index a46ba0e..c62306b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,42 @@ - language: node_js +cache: npm +stages: + - check + - test + - cov + node_js: - - node + - '10' + +os: + - linux + - osx + +script: npx nyc -s npm run test:node -- --bail +after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov -# Make sure we have new NPM. -before_install: - - npm install -g npm +jobs: + include: + - os: windows + cache: false -script: - - npm run lint - - npm test - - npm run coverage + - stage: check + script: + - npx aegir commitlint --travis + - npx aegir dep-check + - npm run lint -addons: - firefox: 'latest' + - stage: test + name: chrome + addons: + chrome: stable + script: npx aegir test -t browser -t webworker -before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start + - stage: test + name: firefox + addons: + firefox: latest + script: npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless -after_success: - - npm run coverage-publish +notifications: + email: false diff --git a/README.md b/README.md index 1be46fa..24f9706 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,17 @@ isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j5 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.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 +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('/ip4/127.0.0.1/udp/1234') // false ``` # API @@ -107,7 +118,7 @@ Detection of IPFS Paths and identifiers in URLs is a two-stage process: 2. proper CID validation is applied to remove false-positives -## Utils +## Content Identifiers ### `isIPFS.multihash(hash)` @@ -115,7 +126,7 @@ Returns `true` if the provided string is a valid `multihash` or `false` otherwis ### `isIPFS.cid(hash)` -Returns `true` if the provided string is a valid `CID` or `false` otherwise. +Returns `true` if the provided string or [`CID`](https://github.com/ipld/js-cid) object represents a valid [CID](https://docs.ipfs.io/guides/concepts/cid/) or `false` otherwise. ### `isIPFS.base32cid(hash)` @@ -172,6 +183,19 @@ Returns `true` if the provided string includes a valid IPFS subdomain or `false` Returns `true` if the provided string includes a valid IPNS subdomain or `false` otherwise. +## Multiaddrs + +Below methods provide basic detection of [multiaddr](https://github.com/multiformats/multiaddr)s: composable and future-proof network addresses. + +Complex validation of multiaddr can be built using `isIPFS.multiaddr` and [`mafmt`](https://github.com/multiformats/js-mafmt) library. + +### `isIPFS.multiaddr(addr)` + +Returns `true` if the provided `string`, [`Multiaddr`](https://github.com/multiformats/js-multiaddr) or `Buffer` represents a valid multiaddr or `false` otherwise. + +### `isIPFS.peerMultiaddr(addr)` + +Returns `true` if the provided `string`, [`Multiaddr`](https://github.com/multiformats/js-multiaddr) or `Buffer` represents a valid "IPFS Peer" multiaddr (matching [`IPFS` format from `mafmt`](https://github.com/multiformats/js-mafmt#api)) or `false` otherwise. # License diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile deleted file mode 100644 index c1e3f2c..0000000 --- a/ci/Jenkinsfile +++ /dev/null @@ -1 +0,0 @@ -javascript() diff --git a/package.json b/package.json index f9b08bd..7236aad 100644 --- a/package.json +++ b/package.json @@ -30,15 +30,17 @@ "author": "Francisco Dias (http://franciscodias.net/)", "license": "MIT", "dependencies": { - "bs58": "4.0.1", + "bs58": "^4.0.1", "cids": "~0.5.6", + "mafmt": "^v6.0.7", + "multiaddr": "^6.0.4", "multibase": "~0.6.0", "multihashes": "~0.4.13" }, "devDependencies": { - "aegir": "15.0.1", - "chai": "4.1.2", - "pre-commit": "1.2.2" + "aegir": "^18.2.0", + "chai": "^4.2.0", + "pre-commit": "^1.2.2" }, "repository": { "type": "git", diff --git a/src/index.js b/src/index.js index 58e9f67..91955ec 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,8 @@ const base58 = require('bs58') const multihash = require('multihashes') const multibase = require('multibase') +const Multiaddr = require('multiaddr') +const mafmt = require('mafmt') const CID = require('cids') const urlPattern = /^https?:\/\/[^/]+\/(ip(f|n)s)\/((\w+).*)/ @@ -42,6 +44,21 @@ function isCID (hash) { } } +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) { @@ -116,6 +133,8 @@ const ipnsSubdomain = (url) => isIpns(url, fqdnPattern, fqdnProtocolMatch, fqdnH module.exports = { multihash: isMultihash, + multiaddr: isMultiaddr, + peerMultiaddr: isPeerMultiaddr, cid: isCID, base32cid: (cid) => (isMultibase(cid) === 'base32' && isCID(cid)), ipfsSubdomain: ipfsSubdomain, diff --git a/test/test-cid.spec.js b/test/test-cid.spec.js index 8243a48..065e4d7 100644 --- a/test/test-cid.spec.js +++ b/test/test-cid.spec.js @@ -4,8 +4,16 @@ const base58 = require('bs58') const expect = require('chai').expect const isIPFS = require('../src/index') +const CID = require('cids') describe('ipfs cid', () => { + it('isIPFS.cid should match a valid CID instance', (done) => { + const cid = new CID('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') + const actual = isIPFS.cid(cid) + expect(actual).to.equal(true) + done() + }) + it('isIPFS.cid should match a valid CIDv0 (multihash)', (done) => { const actual = isIPFS.cid('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') expect(actual).to.equal(true) diff --git a/test/test-multiaddr.spec.js b/test/test-multiaddr.spec.js new file mode 100644 index 0000000..c70e944 --- /dev/null +++ b/test/test-multiaddr.spec.js @@ -0,0 +1,143 @@ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect +const Multiaddr = require('multiaddr') +const isIPFS = require('../src/index') + +describe('ipfs multiaddr', () => { + it('isIPFS.multiaddr should match a string with valid ip4 multiaddr', (done) => { + const actual = isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234/http') + expect(actual).to.equal(true) + done() + }) + + it('isIPFS.multiaddr should match a string with valid ip6 multiaddr', (done) => { + const actual = isIPFS.multiaddr('/ip6/::1/udp/1234/http') + expect(actual).to.equal(true) + done() + }) + + it('isIPFS.multiaddr should match a valid Multiaddr instance', (done) => { + const ma = new Multiaddr('/ip6/::1/udp/1234/http') + const actual = isIPFS.multiaddr(ma) + expect(actual).to.equal(true) + done() + }) + + it('isIPFS.multiaddr should match a Buffer with multiaddr', (done) => { + const ma = new Multiaddr('/ip6/::1/udp/1234/http') + const actual = isIPFS.multiaddr(Buffer.from(ma.buffer)) + expect(actual).to.equal(true) + done() + }) + + it('isIPFS.multiaddr should not match random Buffer', (done) => { + const actual = isIPFS.multiaddr(Buffer.from('randombuffer')) + expect(actual).to.equal(false) + done() + }) + + it('isIPFS.multiaddr should not match an invalid multiaddr (no initial slash)', (done) => { + const actual = isIPFS.multiaddr('ip4/127.0.0.1/udp/1234/http') + expect(actual).to.equal(false) + done() + }) + + it('isIPFS.multiaddr should not match an invalid multiaddr (unknown namespace)', (done) => { + const actual = isIPFS.multiaddr('/yoloinvalid/127.0.0.1/udp/1234/http') + expect(actual).to.equal(false) + done() + }) + + it('isIPFS.multiaddr should not match an invalid multiaddr', (done) => { + const actual = isIPFS.multiaddr('noop') + expect(actual).to.equal(false) + done() + }) + + it('isIPFS.multiaddr should not match an invalid multiaddr data type', (done) => { + const actual = isIPFS.multiaddr(4) + expect(actual).to.equal(false) + done() + }) +}) + +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/ip4/127.0.0.1/tcp/20008/ws/ipfs/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/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4', + '/p2p-circuit/ip4/127.0.0.1/tcp/4002/ipfs/QmddWMcQX6orJGHpETYMyPgXrCXCtYANMFVDCvhKoDwLqA', + '/p2p-circuit/ipfs/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/1.2.3.4/tcp/3456/ws/p2p-webrtc-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4', + '/ip4/1.2.3.4/tcp/3456/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4', + '/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4/p2p-circuit', + '/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4/p2p-circuit/ipfs/QmUjNmr8TgJCn1Ao7DvMy4cjoZU15b9bwSCBLE3vwXiwgj' + ].concat(goodCircuit) + + it('isIPFS.peerMultiaddr should match a string with a valid IPFS peer', (done) => { + for (let addr of validPeerMultiaddrs) { + const actual = isIPFS.peerMultiaddr(addr) + expect(actual, `isIPFS.peerMultiaddr(${addr})`).to.equal(true) + } + done() + }) + + it('isIPFS.peerMultiaddr should match a valid Multiaddr instance', (done) => { + for (let addr of validPeerMultiaddrs) { + const ma = new Multiaddr(addr) + const actual = isIPFS.peerMultiaddr(ma) + expect(actual, `isIPFS.peerMultiaddr(${addr})`).to.equal(true) + } + done() + }) + + it('isIPFS.peerMultiaddr should match a Buffer with multiaddr', (done) => { + for (let addr of validPeerMultiaddrs) { + const ma = new Multiaddr(addr) + const actual = isIPFS.peerMultiaddr((Buffer.from(ma.buffer))) + expect(actual, `isIPFS.peerMultiaddr(${addr})`).to.equal(true) + } + done() + }) + + it('isIPFS.peerMultiaddr should not match random Buffer', (done) => { + const actual = isIPFS.peerMultiaddr(Buffer.from('randombuffer')) + expect(actual).to.equal(false) + done() + }) + + it('isIPFS.peerMultiaddr should not match an invalid multiaddr (no initial slash)', (done) => { + const actual = isIPFS.peerMultiaddr('ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4') + expect(actual).to.equal(false) + done() + }) + + it('isIPFS.peerMultiaddr should not match an invalid multiaddr (unknown namespace)', (done) => { + const actual = isIPFS.peerMultiaddr('/yoloinvalid/1.2.3.4/tcp/3456/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4') + expect(actual).to.equal(false) + done() + }) + + it('isIPFS.peerMultiaddr should not match an invalid multiaddr', (done) => { + const actual = isIPFS.peerMultiaddr('noop') + expect(actual).to.equal(false) + done() + }) + + it('isIPFS.peerMultiaddr should not match an invalid multiaddr data type', (done) => { + const actual = isIPFS.peerMultiaddr(4) + expect(actual).to.equal(false) + done() + }) +})