Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit cc40cd4

Browse files
committedMar 20, 2020
feat: support DNSLink subdomains
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>
1 parent 3823a89 commit cc40cd4

File tree

5 files changed

+115
-26
lines changed

5 files changed

+115
-26
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ node_modules
3838

3939
dist
4040
lib
41+
docs

‎README.md

+12-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ $ npm install --save is-ipfs
2323
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.
2424

2525
```js
26-
var isIPFS = require('is-ipfs')
26+
const isIPFS = require('is-ipfs')
2727
```
2828

2929

@@ -98,6 +98,9 @@ isIPFS.ipnsSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j5
9898
isIPFS.ipnsSubdomain('http://QmcNioXSC1bfJj1dcFErhUfyjFzoX2HodkRccsFFVJJvg8.ipns.dweb.link') // false
9999
isIPFS.ipnsSubdomain('http://foo-bar.ipns.dweb.link') // false (not a PeerID)
100100

101+
isIPFS.dnslinkSubdomain('http://en.wikipedia-on-ipfs.org.ipns.localhost:8080') // true
102+
isIPFS.dnslinkSubdomain('http//bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link') // false
103+
101104
isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234') // true
102105
isIPFS.multiaddr('/ip4/127.0.0.1/udp/1234/http') // true
103106
isIPFS.multiaddr('/ip6/::1/udp/1234') // true
@@ -116,7 +119,7 @@ A suite of util methods that provides efficient validation.
116119

117120
Detection of IPFS Paths and identifiers in URLs is a two-stage process:
118121
1. `urlPattern`/`pathPattern`/`subdomainPattern` regex is applied to quickly identify potential candidates
119-
2. proper CID validation is applied to remove false-positives
122+
2. proper CID/FQDN validation is applied to remove false-positives
120123

121124

122125
## Content Identifiers
@@ -178,15 +181,19 @@ Validated subdomain convention: `cidv1b32.ip(f|n)s.domain.tld`
178181

179182
### `isIPFS.subdomain(url)`
180183

181-
Returns `true` if the provided string includes a valid IPFS or IPNS subdomain or `false` otherwise.
184+
Returns `true` if the provided `url` string includes a valid IPFS, IPNS or DNSLink subdomain or `false` otherwise.
182185

183186
### `isIPFS.ipfsSubdomain(url)`
184187

185-
Returns `true` if the provided string includes a valid IPFS subdomain or `false` otherwise.
188+
Returns `true` if the provided `url` string includes a valid IPFS subdomain (case-insensitive CIDv1) or `false` otherwise.
186189

187190
### `isIPFS.ipnsSubdomain(url)`
188191

189-
Returns `true` if the provided string includes a valid IPNS subdomain or `false` otherwise.
192+
Returns `true` if the provided `url` string includes a valid IPNS subdomain (CIDv1 with `libp2p-key` multicodec) or `false` otherwise.
193+
194+
### `isIPFS.dnslinkSubdomain(url)`
195+
196+
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.
190197

191198
## Multiaddrs
192199

‎package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "is-ipfs",
33
"version": "0.6.3",
4-
"description": "A set of utilities to help identify IPFS resources",
4+
"description": "A set of utilities to help identify IPFS resources in URLs and paths",
55
"leadMaintainer": "Marcin Rataj <lidel@lidel.org>",
66
"main": "src/index.js",
77
"browser": {
@@ -34,11 +34,11 @@
3434
"cids": "~0.7.0",
3535
"mafmt": "^7.0.0",
3636
"multiaddr": "^7.2.1",
37-
"multibase": "~0.6.0",
37+
"multibase": "~0.7.0",
3838
"multihashes": "~0.4.13"
3939
},
4040
"devDependencies": {
41-
"aegir": "^20.5.0",
41+
"aegir": "^21.4.3",
4242
"chai": "^4.2.0",
4343
"pre-commit": "^1.2.2"
4444
},

‎src/index.js

+60-18
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,22 @@ const Multiaddr = require('multiaddr')
77
const mafmt = require('mafmt')
88
const CID = require('cids')
99

10-
const urlPattern = /^https?:\/\/[^/]+\/(ip(f|n)s)\/((\w+).*)/
11-
const pathPattern = /^\/(ip(f|n)s)\/((\w+).*)/
10+
const urlPattern = /^https?:\/\/[^/]+\/(ip[fn]s)\/([^/]+)/
11+
const pathPattern = /^\/(ip[fn]s)\/([^/]+)/
1212
const defaultProtocolMatch = 1
13-
const defaultHashMath = 4
14-
15-
const fqdnPattern = /^https?:\/\/([^/]+)\.(ip(?:f|n)s)\.[^/]+/
16-
const fqdnHashMatch = 1
17-
const fqdnProtocolMatch = 2
13+
const defaultHashMath = 2
14+
15+
// CID, libp2p-key or DNSLink
16+
const subdomainPattern = /^https?:\/\/([^/]+)\.(ip[fs]s)\.[^/]+/
17+
const subdomainIdMatch = 1
18+
const subdomainProtocolMatch = 2
19+
// /ipfs/$cid represented as subdomain
20+
const ipfsSubdomainPattern = /^https?:\/\/([^/]+)\.(ipfs)\.[^/]+/
21+
// /ipns/$libp2p-key represented as subdomain
22+
const libp2pKeySubdomainPattern = /^https?:\/\/([^/]+)\.(ipns)\.[^/]+/
23+
// /ipns/$fqdn represented as subdomain
24+
// (requires at least two DNS labels separated by ".")
25+
const dnslinkSubdomainPattern = /^https?:\/\/([^.]+\.[^/]+)\.(ipns)\.[^/]+/
1826

1927
function isMultihash (hash) {
2028
const formatted = convertToString(hash)
@@ -76,7 +84,7 @@ function isIpfs (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch
7684

7785
let hash = match[hashMatch]
7886

79-
if (hash && pattern === fqdnPattern) {
87+
if (hash && pattern === ipfsSubdomainPattern) {
8088
// when doing checks for subdomain context
8189
// ensure hash is case-insensitive
8290
// (browsers force-lowercase authority compotent anyway)
@@ -100,18 +108,47 @@ function isIpns (input, pattern, protocolMatch = defaultProtocolMatch, hashMatch
100108
return false
101109
}
102110

103-
if (hashMatch && pattern === fqdnPattern) {
104-
let hash = match[hashMatch]
111+
let ipnsId = match[hashMatch]
112+
113+
if (ipnsId && pattern === libp2pKeySubdomainPattern) {
105114
// when doing checks for subdomain context
106115
// ensure hash is case-insensitive
107116
// (browsers force-lowercase authority compotent anyway)
108-
hash = hash.toLowerCase()
109-
return isCID(hash)
117+
ipnsId = ipnsId.toLowerCase()
118+
return isCID(ipnsId)
110119
}
111120

112121
return true
113122
}
114123

124+
function isDNSLink (input, pattern, protocolMatch = defaultProtocolMatch, idMatch) {
125+
const formatted = convertToString(input)
126+
if (!formatted) {
127+
return false
128+
}
129+
130+
const match = formatted.match(pattern)
131+
if (!match) {
132+
return false
133+
}
134+
135+
if (match[protocolMatch] !== 'ipns') {
136+
return false
137+
}
138+
139+
const fqdn = match[idMatch]
140+
141+
if (fqdn && pattern === dnslinkSubdomainPattern) {
142+
try {
143+
const { hostname } = new URL(`http://${fqdn}`) // eslint-disable-line no-new
144+
return fqdn === hostname
145+
} catch (e) {
146+
return false
147+
}
148+
}
149+
return false
150+
}
151+
115152
function isString (input) {
116153
return typeof input === 'string'
117154
}
@@ -128,19 +165,24 @@ function convertToString (input) {
128165
return false
129166
}
130167

131-
const ipfsSubdomain = (url) => isIpfs(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch)
132-
const ipnsSubdomain = (url) => isIpns(url, fqdnPattern, fqdnProtocolMatch, fqdnHashMatch)
168+
const ipfsSubdomain = (url) => isIpfs(url, ipfsSubdomainPattern, subdomainProtocolMatch, subdomainIdMatch)
169+
const ipnsSubdomain = (url) => isIpns(url, libp2pKeySubdomainPattern, subdomainProtocolMatch, subdomainIdMatch)
170+
const dnslinkSubdomain = (url) => isDNSLink(url, dnslinkSubdomainPattern, subdomainProtocolMatch, subdomainIdMatch)
133171

134172
module.exports = {
135173
multihash: isMultihash,
136174
multiaddr: isMultiaddr,
137175
peerMultiaddr: isPeerMultiaddr,
138176
cid: isCID,
139177
base32cid: (cid) => (isMultibase(cid) === 'base32' && isCID(cid)),
140-
ipfsSubdomain: ipfsSubdomain,
141-
ipnsSubdomain: ipnsSubdomain,
142-
subdomain: (url) => (ipfsSubdomain(url) || ipnsSubdomain(url)),
143-
subdomainPattern: fqdnPattern,
178+
ipfsSubdomain,
179+
ipnsSubdomain,
180+
dnslinkSubdomain,
181+
subdomain: (url) => (ipfsSubdomain(url) || ipnsSubdomain(url) || dnslinkSubdomain(url)),
182+
subdomainPattern,
183+
ipfsSubdomainPattern,
184+
libp2pKeySubdomainPattern,
185+
dnslinkSubdomainPattern,
144186
ipfsUrl: (url) => isIpfs(url, urlPattern),
145187
ipnsUrl: (url) => isIpns(url, urlPattern),
146188
url: (url) => (isIpfs(url, urlPattern) || isIpns(url, urlPattern)),

‎test/test-subdomain.spec.js

+39
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ describe('ipfs subdomain', () => {
1818
done()
1919
})
2020

21+
it('isIPFS.ipfsSubdomain should match localhost with port', (done) => {
22+
const actual = isIPFS.ipfsSubdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.localhost:8080')
23+
expect(actual).to.equal(true)
24+
done()
25+
})
26+
2127
it('isIPFS.ipfsSubdomain should not match non-cid subdomains', (done) => {
2228
const actual = isIPFS.ipfsSubdomain('http://not-a-cid.ipfs.dweb.link')
2329
expect(actual).to.equal(false)
@@ -87,6 +93,32 @@ describe('ipfs subdomain', () => {
8793
done()
8894
})
8995

96+
it('isIPFS.dnslinkSubdomain should match .ipns.localhost zone with FQDN', (done) => {
97+
// we do not support opaque strings in subdomains, only peerids
98+
const actual = isIPFS.dnslinkSubdomain('http://docs.ipfs.io.ipns.localhost:8080/some/path')
99+
expect(actual).to.equal(true)
100+
done()
101+
})
102+
103+
it('isIPFS.dnslinkSubdomain should match .ipns.sub.sub.domain.tld zone with FQDN', (done) => {
104+
// we do not support opaque strings in subdomains, only peerids
105+
const actual = isIPFS.dnslinkSubdomain('http://docs.ipfs.io.ipns.foo.bar.buzz.dweb.link')
106+
expect(actual).to.equal(true)
107+
done()
108+
})
109+
110+
it('isIPFS.dnslinkSubdomain should match *.ipns. zone with FQDN', (done) => {
111+
const actual = isIPFS.dnslinkSubdomain('http://docs.ipfs.io.ipns.locahost:8080')
112+
expect(actual).to.equal(true)
113+
done()
114+
})
115+
116+
it('isIPFS.dnslinkSubdomain should not match a .ipns. zone with cidv1b32', (done) => {
117+
const actual = isIPFS.dnslinkSubdomain('http://bafybeiabc2xofh6tdi6vutusorpumwcikw3hf3st4ecjugo6j52f6xwc6q.ipns.dweb.link')
118+
expect(actual).to.equal(false)
119+
done()
120+
})
121+
90122
it('isIPFS.subdomain should match an ipfs subdomain', (done) => {
91123
const actual = isIPFS.subdomain('http://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link')
92124
expect(actual).to.equal(true)
@@ -99,6 +131,13 @@ describe('ipfs subdomain', () => {
99131
done()
100132
})
101133

134+
it('isIPFS.subdomain should match .ipns.localhost zone with FQDN', (done) => {
135+
// we do not support opaque strings in subdomains, only peerids
136+
const actual = isIPFS.subdomain('http://docs.ipfs.io.ipns.dweb.link')
137+
expect(actual).to.equal(true)
138+
done()
139+
})
140+
102141
it('isIPFS.subdomain should not match if fqdn does not start with cidv1b32', (done) => {
103142
const actual = isIPFS.subdomain('http://www.bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va.ipfs.dweb.link')
104143
expect(actual).to.equal(false)

0 commit comments

Comments
 (0)
Please sign in to comment.