Skip to content

Commit f23bc2e

Browse files
committed
fix: dnslink resolver under js-ipfs
js-ipfs v0.34 does not support DNSLinks in ipfs*resolve methods: ipfs/js-ipfs#1918 This temporary workaround detects known errors and falls back to using path from DNSLink cache. It won't be as fresh as original, but at least resolv will work.
1 parent 712926c commit f23bc2e

File tree

4 files changed

+103
-13
lines changed

4 files changed

+103
-13
lines changed

add-on/src/lib/copier.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ function createCopier (notify, ipfsPathValidator) {
4040
},
4141

4242
async copyRawCid (context, contextType) {
43+
const url = await findValueForContext(context, contextType)
4344
try {
44-
const url = await findValueForContext(context, contextType)
4545
const cid = await ipfsPathValidator.resolveToCid(url)
4646
await copyTextToClipboard(cid, notify)
4747
} catch (error) {

add-on/src/lib/ipfs-path.js

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/* eslint-env browser */
33

44
const IsIpfs = require('is-ipfs')
5+
const isFQDN = require('is-fqdn')
56

67
function normalizedIpfsPath (urlOrPath) {
78
let result = urlOrPath
@@ -74,7 +75,6 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {
7475

7576
// Test if actions such as 'copy URL', 'pin/unpin' should be enabled for the URL
7677
isIpfsPageActionsContext (url) {
77-
console.log(url)
7878
return Boolean(url && !url.startsWith(getState().apiURLString) && (
7979
IsIpfs.url(url) ||
8080
IsIpfs.subdomain(url) ||
@@ -147,7 +147,34 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {
147147
const labels = path.split('/')
148148
// We resolve /ipns/<fqdn> as value in DNSLink cache may be out of date
149149
const ipnsRoot = `/ipns/${labels[2]}`
150-
const result = await getIpfs().name.resolve(ipnsRoot, { recursive: true, nocache: false })
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(/^.*\/ipns\/([^/]+).*/, '$1')
158+
console.log('fqdn:' + ipnsRoot, fqdn)
159+
if (err.message === 'Non-base58 character' && isFQDN(fqdn)) {
160+
// js-ipfs without dnslink support, fallback to the value read from DNSLink
161+
const dnslink = dnslinkResolver.readAndCacheDnslink(fqdn)
162+
if (dnslink) {
163+
// swap problematic /ipns/{fqdn} with /ipfs/{cid} and retry lookup
164+
const safePath = trimDoubleSlashes(ipnsRoot.replace(/^.*(\/ipns\/[^/]+)/, dnslink))
165+
if (ipnsRoot !== safePath) {
166+
return ipfsPathValidator.resolveToImmutableIpfsPath(safePath)
167+
}
168+
}
169+
}
170+
throw err
171+
}
172+
}
173+
const result = await ipfsNameResolveWithDnslinkFallback(async () =>
174+
// dhtt/dhtrc optimize for lookup time
175+
getIpfs().name.resolve(ipnsRoot, { recursive: true, dhtt: '5s', dhtrc: 1 })
176+
)
177+
151178
// Old API returned object, latest one returns string ¯\_(ツ)_/¯
152179
const ipfsRoot = result.Path ? result.Path : result
153180
// Return original path with swapped root (keeps pathname + ?search + #hash)
@@ -157,27 +184,53 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {
157184
return path
158185
},
159186

160-
// TODO: add description and tests
161187
// Resolve URL or path to a raw CID:
162188
// - Result is the direct CID
163189
// - Ignores ?search and #hash from original URL
164190
// - Returns null if no CID can be produced
191+
// The purpose of this resolver is to return direct CID without anything else.
165192
async resolveToCid (urlOrPath) {
166193
const path = ipfsPathValidator.resolveToIpfsPath(urlOrPath)
167194
// Fail fast if no IPFS Path
168195
if (!path) return null
169-
// Resolve to raw CID
196+
// Drop unused parts
170197
const rawPath = trimHashAndSearch(path)
171-
const result = await getIpfs().resolve(rawPath, { recursive: true, dhtt: '5s', dhtrc: 1 })
172-
const directCid = result.split('/')[2]
198+
199+
// js-ipfs v0.34 does not support DNSLinks in ipfs.resolve: https://github.com/ipfs/js-ipfs/issues/1918
200+
// TODO: remove ipfsResolveWithDnslinkFallback when js-ipfs implements DNSLink support in ipfs.resolve
201+
const ipfsResolveWithDnslinkFallback = async (resolve) => {
202+
try {
203+
return await resolve()
204+
} catch (err) {
205+
const fqdn = rawPath.replace(/^.*\/ipns\/([^/]+).*/, '$1')
206+
if (err.message === 'resolve non-IPFS names is not implemented' && isFQDN(fqdn)) {
207+
// js-ipfs without dnslink support, fallback to the value read from DNSLink
208+
const dnslink = dnslinkResolver.readAndCacheDnslink(fqdn)
209+
if (dnslink) {
210+
// swap problematic /ipns/{fqdn} with /ipfs/{cid} and retry lookup
211+
const safePath = trimDoubleSlashes(rawPath.replace(/^.*(\/ipns\/[^/]+)/, dnslink))
212+
if (rawPath !== safePath) {
213+
const result = await ipfsPathValidator.resolveToCid(safePath)
214+
// return in format of ipfs.resolve()
215+
return IsIpfs.cid(result) ? `/ipfs/${result}` : result
216+
}
217+
}
218+
}
219+
throw err
220+
}
221+
}
222+
const result = await ipfsResolveWithDnslinkFallback(async () =>
223+
// dhtt/dhtrc optimize for lookup time
224+
getIpfs().resolve(rawPath, { recursive: true, dhtt: '5s', dhtrc: 1 })
225+
)
226+
227+
const directCid = IsIpfs.ipfsPath(result) ? result.split('/')[2] : result
173228
return directCid
174229
}
175-
176230
}
177231

178232
return ipfsPathValidator
179233
}
180-
181234
exports.createIpfsPathValidator = createIpfsPathValidator
182235

183236
function validIpfsOrIpnsUrl (url, dnsLink) {

add-on/src/popup/browser-action/store.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,6 @@ async function resolveToPinPath (ipfs, url) {
322322
// https://github.com/ipfs-shipyard/ipfs-companion/issues/567
323323
// https://github.com/ipfs/ipfs-companion/issues/303
324324
const pathValidator = await getIpfsPathValidator()
325-
const pinPath = trimHashAndSearch(pathValidator.resolveToImmutableIpfsPath(url))
325+
const pinPath = trimHashAndSearch(await pathValidator.resolveToImmutableIpfsPath(url))
326326
return pinPath
327327
}

test/functional/lib/ipfs-path.test.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22
const { stub } = require('sinon')
3-
const { describe, it, beforeEach } = require('mocha')
3+
const { describe, it, beforeEach, afterEach } = require('mocha')
44
const { expect } = require('chai')
55
const { URL } = require('url')
66
const { normalizedIpfsPath, createIpfsPathValidator } = require('../../../add-on/src/lib/ipfs-path')
@@ -10,14 +10,12 @@ const { optionDefaults } = require('../../../add-on/src/lib/options')
1010
const { spoofCachedDnslink } = require('./dnslink.test.js')
1111

1212
function spoofIpnsRecord (ipfs, ipnsPath, value) {
13-
if (ipfs.name.resolve.reset) ipfs.name.resolve.reset()
1413
const resolve = stub(ipfs.name, 'resolve')
1514
resolve.withArgs(ipnsPath).resolves(value)
1615
resolve.throws((arg) => new Error(`Unexpected stubbed call ipfs.name.resolve(${arg})`))
1716
}
1817

1918
function spoofIpfsResolve (ipfs, path, value) {
20-
if (ipfs.resolve.reset) ipfs.resolve.reset()
2119
const resolve = stub(ipfs, 'resolve')
2220
resolve.withArgs(path).resolves(value)
2321
resolve.throws((arg) => new Error(`Unexpected stubbed call ipfs.resolve(${arg})`))
@@ -44,6 +42,11 @@ describe('ipfs-path.js', function () {
4442
ipfsPathValidator = createIpfsPathValidator(() => state, () => ipfs, dnslinkResolver)
4543
})
4644

45+
afterEach(function () {
46+
if (ipfs.name.resolve.reset) ipfs.name.resolve.reset()
47+
if (ipfs.resolve.reset) ipfs.resolve.reset()
48+
})
49+
4750
describe('normalizedIpfsPath', function () {
4851
it('should detect /ipfs/ path in URL from a public gateway', function () {
4952
const url = 'https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
@@ -338,6 +341,22 @@ describe('ipfs-path.js', function () {
338341
spoofIpnsRecord(ipfs, `/ipns/${hostname}`, ipnsPointer)
339342
expect(await ipfsPathValidator.resolveToImmutableIpfsPath(url)).to.equal(ipnsPointer + '/guides/concepts/dnslink/?argTest#hashTest')
340343
})
344+
// TODO: remove when https://github.com/ipfs/js-ipfs/issues/1918 is addressed
345+
it('should resolve URL of a DNSLink website to the immutable /ipfs/ address behind mutable /ipns/ DNSLink in cache (js-ipfs fallback)', async function () {
346+
const url = 'https://docs.ipfs.io/guides/concepts/dnslink/?argTest#hashTest'
347+
// Use IPNS in DNSLINK to ensure resolveToImmutableIpfsPath does resursive resolv to immutable address
348+
const dnslinkValue = '/ipns/QmRV5iNhGoxBaAcbucMAW9WtVHbeehXhAdr5CZQDhL55Xk'
349+
const ipnsPointer = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
350+
const { hostname } = new URL(url)
351+
spoofCachedDnslink(hostname, dnslinkResolver, dnslinkValue)
352+
// js-ipfs v0.34 does not support DNSLinks in ipfs.name.resolve: https://github.com/ipfs/js-ipfs/issues/1918
353+
const resolve = stub(ipfs.name, 'resolve')
354+
resolve.withArgs(`/ipns/${hostname}`).throws(new Error('Non-base58 character'))
355+
// until it is implemented, we have a workaround that falls back to value from dnslink
356+
resolve.withArgs(dnslinkValue).resolves(ipnsPointer)
357+
resolve.throws((arg) => new Error(`Unexpected stubbed call ipfs.name.resolve(${arg})`))
358+
expect(await ipfsPathValidator.resolveToImmutableIpfsPath(url)).to.equal(ipnsPointer + '/guides/concepts/dnslink/?argTest#hashTest')
359+
})
341360
it('should resolve to null if input is an invalid path', async function () {
342361
const path = '/foo/bar/?argTest#hashTest'
343362
expect(await ipfsPathValidator.resolveToImmutableIpfsPath(path)).to.equal(null)
@@ -398,6 +417,24 @@ describe('ipfs-path.js', function () {
398417
spoofIpfsResolve(ipfs, `/ipns/docs.ipfs.io/guides/concepts/dnslink/`, `/ipfs/${expectedCid}`)
399418
expect(await ipfsPathValidator.resolveToCid(url)).to.equal(expectedCid)
400419
})
420+
// TODO: remove when https://github.com/ipfs/js-ipfs/issues/1918 is addressed
421+
it('should resolve URL of a DNSLink website if DNSLink is in cache (js-ipfs fallback)', async function () {
422+
const url = 'https://docs.ipfs.io/guides/concepts/dnslink/?argTest#hashTest'
423+
// Use IPNS in DNSLINK to ensure resolveToImmutableIpfsPath does resursive resolv to immutable address
424+
const expectedCid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
425+
const dnslinkValue = '/ipns/QmRV5iNhGoxBaAcbucMAW9WtVHbeehXhAdr5CZQDhL55Xk'
426+
const { hostname } = new URL(url)
427+
spoofCachedDnslink(hostname, dnslinkResolver, dnslinkValue)
428+
// Note the DNSLink value is ignored, and /ipns/<fqdn> is passed to ipfs.resolv internally
429+
// This ensures the latest pointer is returned, instead of stale value from DNSLink cache
430+
// js-ipfs v0.34 does not support DNSLinks in ipfs.name.resolve: https://github.com/ipfs/js-ipfs/issues/1918
431+
const resolve = stub(ipfs, 'resolve')
432+
resolve.withArgs(`/ipns/docs.ipfs.io/guides/concepts/dnslink/`).throws(new Error('resolve non-IPFS names is not implemented'))
433+
// until it is implemented, we have a workaround that falls back to value from dnslink
434+
resolve.withArgs('/ipns/QmRV5iNhGoxBaAcbucMAW9WtVHbeehXhAdr5CZQDhL55Xk/guides/concepts/dnslink/').resolves(`/ipfs/${expectedCid}`)
435+
resolve.throws((arg) => new Error(`Unexpected stubbed call ipfs.resolve(${arg})`))
436+
expect(await ipfsPathValidator.resolveToCid(url)).to.equal(expectedCid)
437+
})
401438
it('should resolve to null if input is an invalid path', async function () {
402439
const path = '/foo/bar/?argTest#hashTest'
403440
expect(await ipfsPathValidator.resolveToCid(path)).to.equal(null)

0 commit comments

Comments
 (0)