2
2
/* eslint-env browser */
3
3
4
4
const IsIpfs = require ( 'is-ipfs' )
5
+ const isFQDN = require ( 'is-fqdn' )
5
6
6
- function safeIpfsPath ( urlOrPath ) {
7
+ function normalizedIpfsPath ( urlOrPath ) {
8
+ let result = urlOrPath
9
+ // Convert CID-in-subdomain URL to /ipns/<fqdn>/ path
7
10
if ( IsIpfs . subdomain ( urlOrPath ) ) {
8
- urlOrPath = subdomainToIpfsPath ( urlOrPath )
11
+ result = subdomainToIpfsPath ( urlOrPath )
9
12
}
10
- // better safe than sorry: https://github.com/ipfs/ipfs-companion/issues/303
11
- return decodeURIComponent ( urlOrPath . replace ( / ^ .* ( \/ i p ( f | n ) s \/ .+ ) $ / , '$1' ) )
13
+ // Drop everything before the IPFS path
14
+ result = result . replace ( / ^ .* ( \/ i p ( f | n ) s \/ .+ ) $ / , '$1' )
15
+ // Remove Unescape special characters
16
+ // https://github.com/ipfs/ipfs-companion/issues/303
17
+ result = decodeURIComponent ( result )
18
+ // Return a valid IPFS path or null otherwise
19
+ return IsIpfs . path ( result ) ? result : null
12
20
}
13
- exports . safeIpfsPath = safeIpfsPath
21
+ exports . normalizedIpfsPath = normalizedIpfsPath
14
22
15
23
function subdomainToIpfsPath ( url ) {
16
24
if ( typeof url === 'string' ) {
17
25
url = new URL ( url )
18
26
}
19
27
const fqdn = url . hostname . split ( '.' )
28
+ // TODO: support CID split with commas
20
29
const cid = fqdn [ 0 ]
30
+ // TODO: support .ip(f|n)s. being at deeper levels
21
31
const protocol = fqdn [ 1 ]
22
- return `/${ protocol } /${ cid } ${ url . pathname } `
32
+ return `/${ protocol } /${ cid } ${ url . pathname } ${ url . search } ${ url . hash } `
23
33
}
24
34
25
35
function pathAtHttpGateway ( path , gatewayUrl ) {
@@ -39,34 +49,37 @@ function trimHashAndSearch (urlString) {
39
49
}
40
50
exports . trimHashAndSearch = trimHashAndSearch
41
51
42
- function createIpfsPathValidator ( getState , dnsLink ) {
52
+ function createIpfsPathValidator ( getState , getIpfs , dnslinkResolver ) {
43
53
const ipfsPathValidator = {
44
54
// Test if URL is a Public IPFS resource
45
55
// (pass validIpfsOrIpnsUrl(url) and not at the local gateway or API)
46
56
publicIpfsOrIpnsResource ( url ) {
47
57
// exclude custom gateway and api, otherwise we have infinite loops
48
58
if ( ! url . startsWith ( getState ( ) . gwURLString ) && ! url . startsWith ( getState ( ) . apiURLString ) ) {
49
- return validIpfsOrIpnsUrl ( url , dnsLink )
59
+ return validIpfsOrIpnsUrl ( url , dnslinkResolver )
50
60
}
51
61
return false
52
62
} ,
53
63
54
64
// Test if URL is a valid IPFS or IPNS
55
65
// (IPFS needs to be a CID, IPNS can be PeerId or have dnslink entry)
56
66
validIpfsOrIpnsUrl ( url ) {
57
- return validIpfsOrIpnsUrl ( url , dnsLink )
67
+ return validIpfsOrIpnsUrl ( url , dnslinkResolver )
58
68
} ,
59
69
60
70
// Same as validIpfsOrIpnsUrl (url) but for paths
61
71
// (we have separate methods to avoid 'new URL' where possible)
62
72
validIpfsOrIpnsPath ( path ) {
63
- return validIpfsOrIpnsPath ( path , dnsLink )
73
+ return validIpfsOrIpnsPath ( path , dnslinkResolver )
64
74
} ,
65
75
66
76
// Test if actions such as 'copy URL', 'pin/unpin' should be enabled for the URL
67
- // TODO: include hostname check for DNSLink and display option to copy CID even if no redirect
68
77
isIpfsPageActionsContext ( url ) {
69
- return ( IsIpfs . url ( url ) && ! url . startsWith ( getState ( ) . apiURLString ) ) || IsIpfs . subdomain ( url )
78
+ return Boolean ( url && ! url . startsWith ( getState ( ) . apiURLString ) && (
79
+ IsIpfs . url ( url ) ||
80
+ IsIpfs . subdomain ( url ) ||
81
+ dnslinkResolver . cachedDnslink ( new URL ( url ) . hostname )
82
+ ) )
70
83
} ,
71
84
72
85
// Test if actions such as 'per site redirect toggle' should be enabled for the URL
@@ -77,12 +90,146 @@ function createIpfsPathValidator (getState, dnsLink) {
77
90
( url . startsWith ( 'http' ) && // hide on non-HTTP pages
78
91
! url . startsWith ( state . gwURLString ) && // hide on /ipfs/*
79
92
! url . startsWith ( state . apiURLString ) ) ) // hide on api port
93
+ } ,
94
+
95
+ // Resolve URL or path to HTTP URL:
96
+ // - IPFS paths are attached to HTTP Gateway root
97
+ // - URL of DNSLinked websites are returned as-is
98
+ // The purpose of this resolver is to always return a meaningful, publicly
99
+ // accessible URL that can be accessed without the need of IPFS client.
100
+ resolveToPublicUrl ( urlOrPath , optionalGatewayUrl ) {
101
+ const input = urlOrPath
102
+ // CID-in-subdomain is good as-is
103
+ if ( IsIpfs . subdomain ( input ) ) return input
104
+ // IPFS Paths should be attached to the public gateway
105
+ const ipfsPath = normalizedIpfsPath ( input )
106
+ const gateway = optionalGatewayUrl || getState ( ) . pubGwURLString
107
+ if ( ipfsPath ) return pathAtHttpGateway ( ipfsPath , gateway )
108
+ // Return original URL (eg. DNSLink domains) or null if not an URL
109
+ return input . startsWith ( 'http' ) ? input : null
110
+ } ,
111
+
112
+ // Resolve URL or path to IPFS Path:
113
+ // - The path can be /ipfs/ or /ipns/
114
+ // - Keeps pathname + ?search + #hash from original URL
115
+ // - Returns null if no valid path can be produced
116
+ // The purpose of this resolver is to return a valid IPFS path
117
+ // that can be accessed with IPFS client.
118
+ resolveToIpfsPath ( urlOrPath ) {
119
+ const input = urlOrPath
120
+ // Try to normalize to IPFS path (gateway path or CID-in-subdomain)
121
+ const ipfsPath = normalizedIpfsPath ( input )
122
+ if ( ipfsPath ) return ipfsPath
123
+ // Check URL for DNSLink
124
+ if ( ! input . startsWith ( 'http' ) ) return null
125
+ const { hostname } = new URL ( input )
126
+ const dnslink = dnslinkResolver . cachedDnslink ( hostname )
127
+ if ( dnslink ) {
128
+ // Return full IPNS path (keeps pathname + ?search + #hash)
129
+ return dnslinkResolver . convertToIpnsPath ( input )
130
+ }
131
+ // No IPFS path by this point
132
+ return null
133
+ } ,
134
+
135
+ // Resolve URL or path to Immutable IPFS Path:
136
+ // - Same as resolveToIpfsPath, but the path is always immutable /ipfs/
137
+ // - Keeps pathname + ?search + #hash from original URL
138
+ // - Returns null if no valid path can be produced
139
+ // The purpose of this resolver is to return immutable /ipfs/ address
140
+ // even if /ipns/ is present in its input.
141
+ async resolveToImmutableIpfsPath ( urlOrPath ) {
142
+ const path = ipfsPathValidator . resolveToIpfsPath ( urlOrPath )
143
+ // Fail fast if no IPFS Path
144
+ if ( ! path ) return null
145
+ // Resolve /ipns/ → /ipfs/
146
+ if ( IsIpfs . ipnsPath ( path ) ) {
147
+ const labels = path . split ( '/' )
148
+ // We resolve /ipns/<fqdn> as value in DNSLink cache may be out of date
149
+ const ipnsRoot = `/ipns/${ labels [ 2 ] } `
150
+
151
+ // js-ipfs v0.34 does not support DNSLinks in ipfs.name.resolve: https://github.com/ipfs/js-ipfs/issues/1918
152
+ // TODO: remove ipfsNameResolveWithDnslinkFallback when js-ipfs implements DNSLink support in ipfs.name.resolve
153
+ const ipfsNameResolveWithDnslinkFallback = async ( resolve ) => {
154
+ try {
155
+ return await resolve ( )
156
+ } catch ( err ) {
157
+ const fqdn = ipnsRoot . replace ( / ^ .* \/ i p n s \/ ( [ ^ / ] + ) .* / , '$1' )
158
+ if ( err . message === 'Non-base58 character' && isFQDN ( fqdn ) ) {
159
+ // js-ipfs without dnslink support, fallback to the value read from DNSLink
160
+ const dnslink = dnslinkResolver . readAndCacheDnslink ( fqdn )
161
+ if ( dnslink ) {
162
+ // swap problematic /ipns/{fqdn} with /ipfs/{cid} and retry lookup
163
+ const safePath = trimDoubleSlashes ( ipnsRoot . replace ( / ^ .* ( \/ i p n s \/ [ ^ / ] + ) / , dnslink ) )
164
+ if ( ipnsRoot !== safePath ) {
165
+ return ipfsPathValidator . resolveToImmutableIpfsPath ( safePath )
166
+ }
167
+ }
168
+ }
169
+ throw err
170
+ }
171
+ }
172
+ const result = await ipfsNameResolveWithDnslinkFallback ( async ( ) =>
173
+ // dhtt/dhtrc optimize for lookup time
174
+ getIpfs ( ) . name . resolve ( ipnsRoot , { recursive : true , dhtt : '5s' , dhtrc : 1 } )
175
+ )
176
+
177
+ // Old API returned object, latest one returns string ¯\_(ツ)_/¯
178
+ const ipfsRoot = result . Path ? result . Path : result
179
+ // Return original path with swapped root (keeps pathname + ?search + #hash)
180
+ return path . replace ( ipnsRoot , ipfsRoot )
181
+ }
182
+ // Return /ipfs/ path
183
+ return path
184
+ } ,
185
+
186
+ // Resolve URL or path to a raw CID:
187
+ // - Result is the direct CID
188
+ // - Ignores ?search and #hash from original URL
189
+ // - Returns null if no CID can be produced
190
+ // The purpose of this resolver is to return direct CID without anything else.
191
+ async resolveToCid ( urlOrPath ) {
192
+ const path = ipfsPathValidator . resolveToIpfsPath ( urlOrPath )
193
+ // Fail fast if no IPFS Path
194
+ if ( ! path ) return null
195
+ // Drop unused parts
196
+ const rawPath = trimHashAndSearch ( path )
197
+
198
+ // js-ipfs v0.34 does not support DNSLinks in ipfs.resolve: https://github.com/ipfs/js-ipfs/issues/1918
199
+ // TODO: remove ipfsResolveWithDnslinkFallback when js-ipfs implements DNSLink support in ipfs.resolve
200
+ const ipfsResolveWithDnslinkFallback = async ( resolve ) => {
201
+ try {
202
+ return await resolve ( )
203
+ } catch ( err ) {
204
+ const fqdn = rawPath . replace ( / ^ .* \/ i p n s \/ ( [ ^ / ] + ) .* / , '$1' )
205
+ if ( err . message === 'resolve non-IPFS names is not implemented' && isFQDN ( fqdn ) ) {
206
+ // js-ipfs without dnslink support, fallback to the value read from DNSLink
207
+ const dnslink = dnslinkResolver . readAndCacheDnslink ( fqdn )
208
+ if ( dnslink ) {
209
+ // swap problematic /ipns/{fqdn} with /ipfs/{cid} and retry lookup
210
+ const safePath = trimDoubleSlashes ( rawPath . replace ( / ^ .* ( \/ i p n s \/ [ ^ / ] + ) / , dnslink ) )
211
+ if ( rawPath !== safePath ) {
212
+ const result = await ipfsPathValidator . resolveToCid ( safePath )
213
+ // return in format of ipfs.resolve()
214
+ return IsIpfs . cid ( result ) ? `/ipfs/${ result } ` : result
215
+ }
216
+ }
217
+ }
218
+ throw err
219
+ }
220
+ }
221
+ const result = await ipfsResolveWithDnslinkFallback ( async ( ) =>
222
+ // dhtt/dhtrc optimize for lookup time
223
+ getIpfs ( ) . resolve ( rawPath , { recursive : true , dhtt : '5s' , dhtrc : 1 } )
224
+ )
225
+
226
+ const directCid = IsIpfs . ipfsPath ( result ) ? result . split ( '/' ) [ 2 ] : result
227
+ return directCid
80
228
}
81
229
}
82
230
83
231
return ipfsPathValidator
84
232
}
85
-
86
233
exports . createIpfsPathValidator = createIpfsPathValidator
87
234
88
235
function validIpfsOrIpnsUrl ( url , dnsLink ) {
@@ -122,6 +269,7 @@ function validIpnsPath (path, dnsLink) {
122
269
return true
123
270
}
124
271
// then see if there is an DNSLink entry for 'ipnsRoot' hostname
272
+ // TODO: use dnslink cache only
125
273
if ( dnsLink . readAndCacheDnslink ( ipnsRoot ) ) {
126
274
// console.log('==> IPNS for FQDN with valid dnslink: ', ipnsRoot)
127
275
return true
0 commit comments