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

Add validation of CID in Subdomains #20

Merged
merged 7 commits into from
Jul 23, 2018
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ lib-cov

# Coverage directory used by tools like istanbul
coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
Expand Down
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@

language: node_js
node_js:
- 4
- 5
- node

# Make sure we have new NPM.
Expand Down
90 changes: 75 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
is-ipfs
====

[![build status](https://secure.travis-ci.org/ipfs/is-ipfs.svg)](http://travis-ci.org/ipfs/is-ipfs)
[![dignified.js](https://img.shields.io/badge/follows-dignified.js-blue.svg?style=flat-square)](https://github.com/dignifiedquire/dignified.js)
[![](https://img.shields.io/github/release/ipfs/is-ipfs.svg)](https://github.com/ipfs/is-ipfs/releases/latest)
[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](https://webchat.freenode.net/?channels=%23ipfs)

A set of utilities to help identify [IPFS](https://ipfs.io/) resources.
> A set of utilities to help identify [IPFS](https://ipfs.io/) resources

## Lead Maintainer

## Install
[Marcin Rataj](https://github.com/lidel)

# Install

### In Node.js through npm

Expand All @@ -34,17 +37,25 @@ Loading this module through a script tag will make the ```IsIpfs``` obj availabl
<script src="https://unpkg.com/is-ipfs/dist/index.js"></script>
```

## Usage
# Usage
```javascript
const isIPFS = require('is-ipfs')

isIPFS.multihash('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
isIPFS.multihash('noop') // false

isIPFS.multibase('bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va') // 'base32'
isIPFS.multibase('zdj7WWeQ43G6JJvLWQWZpyHuAMq6uYWRjkBXFad11vE2LHhQ7') // 'base58btc'
isIPFS.multibase('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false (no multibase prefix in CIDv0)
isIPFS.multibase('noop') // false
Copy link
Member

Choose a reason for hiding this comment

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

This feels out of place to me. The rest of the API returns true/false. This is a simple call-through to multibase.isEncoded.

Copy link
Member Author

@lidel lidel Jul 23, 2018

Choose a reason for hiding this comment

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

Should it be changed to true/false or just removed? Not sure how useful it is, maybe just remove it?
(I think I've added it only because isIPFS.base32cid introduced multibase as a dependency)

Copy link
Member

Choose a reason for hiding this comment

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

Personally, I'd remove it. I considered that isIPFS.base32cid uses multibase as a dependency, and was thinking of suggesting to check like:

typeof cid === 'string' && cid.slice(0, 4) === 'bafy' && isCID(cid)

...but js-cid depends on multibase so you're not saving any bundle bytes by depending on it and using it.


isIPFS.cid('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true (CIDv0)
isIPFS.cid('zdj7WWeQ43G6JJvLWQWZpyHuAMq6uYWRjkBXFad11vE2LHhQ7') // true (CIDv1)
isIPFS.cid('noop') // false

isIPFS.base32cid('bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va') // true
isIPFS.base32cid('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false

isIPFS.url('https://ipfs.io/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // true
isIPFS.url('https://ipfs.io/ipns/github.com') // true
isIPFS.url('https://github.com/ipfs/js-ipfs/blob/master/README.md') // false
Expand All @@ -71,29 +82,52 @@ isIPFS.ipfsPath('/ipfs/invalid-hash') // false

isIPFS.ipnsPath('/ipfs/QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o') // false
isIPFS.ipnsPath('/ipns/github.com') // true

isIPFS.subdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') // true
Copy link
Member

Choose a reason for hiding this comment

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

Is this true for http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link also? Maybe add an example here :D

isIPFS.subdomain('http://www.bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // false
isIPFS.subdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.dweb.link') // false

isIPFS.ipfsSubdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link') // true
isIPFS.ipfsSubdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.dweb.link') // false

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

## API
# API

A suite of util methods provides efficient validation.
Copy link

Choose a reason for hiding this comment

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

...that provides...


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


## Utils

### `isIPFS.multihash(hash)`

Returns `true` if the provided string is a valid `multihash` or `false` otherwise.

### `isIPFS.multibase(cid)`

Returns a string with multibase name if the provided CID has `multibase` prefix or `false` otherwise.

### `isIPFS.cid(hash)`

Returns `true` if the provided string is a valid `CID` or `false` otherwise.

### `isIPFS.url(url)`
### `isIPFS.base32cid(hash)`

Returns `true` if the provided string is a valid IPFS or IPNS url or `false` otherwise.
Returns `true` if the provided string is a valid `CID` in Base32 encoding or `false` otherwise.

### `isIPFS.path(path)`
## URLs

Returns `true` if the provided string is a valid IPFS or IPNS path or `false` otherwise.

### `isIPFS.urlOrPath(path)`
### `isIPFS.url(url)`

Returns `true` if the provided string is a valid IPFS or IPNS url or path or `false` otherwise.
Returns `true` if the provided string is a valid IPFS or IPNS url or `false` otherwise.

### `isIPFS.ipfsUrl(url)`

Expand All @@ -103,6 +137,19 @@ Returns `true` if the provided string is a valid IPFS url or `false` otherwise.

Returns `true` if the provided string is a valid IPNS url or `false` otherwise.

## Paths

Standalone validation of IPFS Paths: `/ip(f|n)s/<cid>/..`

### `isIPFS.path(path)`

Returns `true` if the provided string is a valid IPFS or IPNS path or `false` otherwise.

### `isIPFS.urlOrPath(path)`

Returns `true` if the provided string is a valid IPFS or IPNS url or path or `false` otherwise.


### `isIPFS.ipfsPath(path)`

Returns `true` if the provided string is a valid IPFS path or `false` otherwise.
Expand All @@ -111,10 +158,23 @@ Returns `true` if the provided string is a valid IPFS path or `false` otherwise.

Returns `true` if the provided string is a valid IPNS path or `false` otherwise.

## Subdomains

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.

### `isIPFS.ipfsSubdomain(url)`

Returns `true` if the provided string includes a valid IPFS subdomain or `false` otherwise.

### `isIPFS.ipnsSubdomain(url)`

**Note:** the regex used for these checks is also exported as `isIPFS.urlPattern`
Returns `true` if the provided string includes a valid IPNS subdomain or `false` otherwise.

## License

# License

MIT
1 change: 1 addition & 0 deletions ci/Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
javascript()
44 changes: 23 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,54 +1,56 @@
{
"name": "is-ipfs",
"version": "0.3.2",
"version": "0.4.0",
"description": "A set of utilities to help identify IPFS resources",
"main": "src/index.js",
"browser": {
"fs": false
},
"scripts": {
"test:node": "aegir-test node",
"test:browser": "aegir-test browser",
"test": "aegir-test",
"lint": "aegir-lint",
"release": "aegir-release",
"release-minor": "aegir-release --type minor",
"release-major": "aegir-release --type major",
"build": "aegir-build",
"coverage": "aegir-coverage",
"coverage-publish": "aegir-coverage publish"
"test:node": "aegir test --target node",
"test:browser": "aegir test --target browser",
"test": "aegir test",
"lint": "aegir lint",
"release": "aegir release",
"release-minor": "aegir release --type minor",
"release-major": "aegir release --type major",
"build": "aegir build",
"coverage": "aegir coverage",
"coverage-publish": "aegir coverage --upload"
},
"pre-commit": [
"test",
"lint"
],
"keywords": [
"js-ipfs",
"ipfs"
],
"author": "Francisco Dias <francisco@baiodias.com> (http://franciscodias.net/)",
"license": "MIT",
"dependencies": {
"cids": "~0.5.1",
"bs58": "^4.0.1",
"multihashes": "~0.4.9"
"bs58": "4.0.1",
"cids": "0.5.3",
"multibase": "0.4.0",
"multihashes": "0.4.13"
},
"devDependencies": {
"aegir": "^11.0.2",
"chai": "^4.1.2",
"pre-commit": "^1.2.2"
"aegir": "15.0.1",
"chai": "4.1.2",
"pre-commit": "1.2.2"
},
"repository": {
"type": "git",
"url": "https://github.com/xicombd/is-ipfs.git"
"url": "https://github.com/ipfs/is-ipfs.git"
},
"bugs": {
"url": "https://github.com/xicombd/is-ipfs/issues"
"url": "https://github.com/ipfs/is-ipfs/issues"
},
"homepage": "https://github.com/xicombd/is-ipfs",
"homepage": "https://github.com/ipfs/is-ipfs",
"contributors": [
"David Dias <daviddias.p@gmail.com>",
"Francisco Baio Dias <xicombd@gmail.com>",
"Marcin Rataj <lidel@lidel.org>",
"nginnever <ginneversource@gmail.com>"
]
}
}
50 changes: 44 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,37 @@

const base58 = require('bs58')
const multihash = require('multihashes')
const multibase = require('multibase')
Copy link
Member

Choose a reason for hiding this comment

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

You still depend directly on multibase - it should be in dependencies

const CID = require('cids')

const urlPattern = /^https?:\/\/[^/]+\/(ip(f|n)s)\/((\w+).*)/
const pathPattern = /^\/(ip(f|n)s)\/((\w+).*)/
const defaultProtocolMatch = 1
const defaultHashMath = 4

const fqdnPattern = /^https?:\/\/([^/]+)\.(ip(?:f|n)s)\.[^/]+/
const fqdnHashMatch = 1
const fqdnProtocolMatch = 2

function isMultihash (hash) {
const formatted = convertToString(hash)
try {
const buffer = new Buffer(base58.decode(formatted))
const buffer = Buffer.from(base58.decode(formatted))
multihash.decode(buffer)
return true
} catch (e) {
return false
}
}

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

function isCID (hash) {
try {
return CID.isCID(new CID(hash))
Expand All @@ -26,7 +41,7 @@ function isCID (hash) {
}
}

function isIpfs (input, pattern) {
function isIpfs (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch = defaultHashMath) {
const formatted = convertToString(input)
if (!formatted) {
return false
Expand All @@ -37,15 +52,23 @@ function isIpfs (input, pattern) {
return false
}

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

const hash = match[4]
let hash = match[hashMatch]

if (hash && pattern === fqdnPattern) {
// 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) {
function isIpns (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch) {
const formatted = convertToString(input)
if (!formatted) {
return false
Expand All @@ -55,10 +78,19 @@ function isIpns (input, pattern) {
return false
}

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

if (hashMatch && pattern === fqdnPattern) {
let hash = match[hashMatch]
// when doing checks for subdomain context
// ensure hash is case-insensitive
// (browsers force-lowercase authority compotent anyway)
hash = hash.toLowerCase()
return isCID(hash)
}

return true
}

Expand All @@ -76,7 +108,13 @@ function convertToString (input) {

module.exports = {
multihash: isMultihash,
multibase: isMultibase,
cid: isCID,
base32cid: (cid) => (isMultibase(cid) === 'base32' && isCID(cid)),
ipfsSubdomain: (url) => isIpfs(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch),
ipnsSubdomain: (url) => isIpns(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch),
subdomain: (url) => (isIpfs(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch) || isIpns(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch)),
Copy link

Choose a reason for hiding this comment

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

As you're calling (ipfsSubdomain || ipnsSubdomain), you should declare them before the module.exports. That way if any of those change, you won't need to change that code in two places.

To make things clearer:

const ipfsSubdomain = (url) => isIpfs(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch)
const ipnsSubdomain = (url) => isIpns(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch)

module.exports = {
	multihash: isMultihash,
	cid: isCID,
	base32cid: (cid) => (isMultibase(cid) === 'base32' && isCID(cid)),
	ipfsSubdomain: ipfsSubdomain,
	ipnsSubdomain: ipnsSubdomain,
	subdomain: (url) => (ipfsSubdomain(url) || ipnsSubdomain(url)),
	...
}

subdomainPattern: fqdnPattern,
ipfsUrl: (url) => isIpfs(url, urlPattern),
ipnsUrl: (url) => isIpns(url, urlPattern),
url: (url) => (isIpfs(url, urlPattern) || isIpns(url, urlPattern)),
Expand Down
10 changes: 8 additions & 2 deletions test/test-cid.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ describe('ipfs cid', () => {
})

it('isIPFS.cid should match a valid CIDv0 (multihash) buffer', (done) => {
const actual = isIPFS.cid(new Buffer(base58.decode('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o')))
const actual = isIPFS.cid(Buffer.from(base58.decode('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o')))
expect(actual).to.equal(true)
done()
})

it('isIPFS.cid should not match a broken CIDv0 buffer', (done) => {
const actual = isIPFS.cid(new Buffer('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE70'))
const actual = isIPFS.cid(Buffer.from('QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE70'))
expect(actual).to.equal(false)
done()
})
Expand All @@ -36,6 +36,12 @@ describe('ipfs cid', () => {
done()
})

it('isIPFS.cid should match a valid CIDv1 in Base32', (done) => {
const actual = isIPFS.cid('bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va')
expect(actual).to.equal(true)
done()
})

it('isIPFS.cid should not match an invalid CIDv1 (with a typo)', (done) => {
const actual = isIPFS.cid('zdj7WWeQ43G6JJvLWQWZpyHuAMq6uYWRjkBXFad11vE2LHhQ')
expect(actual).to.equal(false)
Expand Down
Loading