Skip to content

Commit

Permalink
feat: support DNSLink subdomains
Browse files Browse the repository at this point in the history
This change adds support for DNSLink subdomains on localhost gateway
(ipfs/kubo#6096)

Example: en.wikipedia-on-ipfs.org.ipfs.localhost:8080

BREAKING CHANGE: `isIPFS.subdomain` now returns true for <domain.tld>.ipns.localhost
BREAKING CHANGE: `isIPFS.subdomainPattern` changed

License: MIT
Signed-off-by: Marcin Rataj <lidel@lidel.org>
  • Loading branch information
lidel committed Mar 20, 2020
1 parent 3823a89 commit cc40cd4
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 26 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ node_modules

dist
lib
docs
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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')
```


Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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 <lidel@lidel.org>",
"main": "src/index.js",
"browser": {
Expand Down Expand Up @@ -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"
},
Expand Down
78 changes: 60 additions & 18 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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'
}
Expand All @@ -128,19 +165,24 @@ 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,
multiaddr: isMultiaddr,
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)),
Expand Down
39 changes: 39 additions & 0 deletions test/test-subdomain.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit cc40cd4

Please sign in to comment.