diff --git a/add-on/src/lib/ipfs-path.js b/add-on/src/lib/ipfs-path.js index f3723ea11..fe31423d9 100644 --- a/add-on/src/lib/ipfs-path.js +++ b/add-on/src/lib/ipfs-path.js @@ -12,6 +12,11 @@ const RESULT_TTL_MS = 300000 // 5 minutes function ipfsContentPath (urlOrPath, opts) { opts = opts || {} + // ipfs:// → /ipfs/ + if (urlOrPath && urlOrPath.toString().startsWith('ip')) { + urlOrPath = urlOrPath.replace(/^(ip[n|f]s):\/\//, '/$1/') + } + // Fail fast if no content path can be extracted from input if (!isIPFS.urlOrPath(urlOrPath)) return null @@ -23,7 +28,8 @@ function ipfsContentPath (urlOrPath, opts) { if (isIPFS.subdomain(urlOrPath)) { // Move CID-in-subdomain to URL pathname - const { id, ns } = subdomainPatternMatch(url) + let { id, ns } = subdomainPatternMatch(url) + id = dnsLabelToFqdn(id) url = new URL(`https://localhost/${ns}/${id}${url.pathname}${url.search}${url.hash}`) } @@ -61,6 +67,16 @@ function subdomainPatternMatch (url) { return { id, ns } } +function dnsLabelToFqdn (label) { + if (label && !label.includes('.') && label.includes('-') && !isIPFS.cid(label)) { + // no '.' means the subdomain name is most likely an inlined DNSLink into single DNS label + // en-wikipedia--on--ipfs-org → en.wikipedia-on-ipfs.org + // (https://github.com/ipfs/in-web-browsers/issues/169) + label = label.replace(/--/g, '@').replace(/-/g, '.').replace(/@/g, '-') + } + return label +} + function pathAtHttpGateway (path, gatewayUrl) { // return URL without duplicated slashes return trimDoubleSlashes(new URL(`${gatewayUrl}${path}`).toString()) @@ -155,6 +171,7 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { if (isIPFS.ipfsPath(path)) { return true } + // `/ipns/` requires multiple stages/branches (can be FQDN with dnslink or CID) if (isIPFS.ipnsPath(path)) { // we may have false-positives here, so we do additional checks below @@ -183,7 +200,7 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { const { apiURLString } = getState() const { hostname } = new URL(url) return Boolean(url && !url.startsWith(apiURLString) && ( - isIPFS.url(url) || + !!ipfsContentPath(url) || dnslinkResolver.cachedDnslink(hostname) )) }, @@ -193,7 +210,7 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { const { localGwAvailable, gwURL, apiURL } = getState() return localGwAvailable && // show only when redirect is possible (isIPFS.ipnsUrl(url) || // show on /ipns/ - (url.startsWith('http') && // hide on non-HTTP pages + ((url.startsWith('http') || url.startsWith('ip')) && // hide on non-HTTP/native pages !sameGateway(url, gwURL) && // hide on /ipfs/* and *.ipfs. !sameGateway(url, apiURL))) // hide on api port }, @@ -210,6 +227,16 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { const { pubSubdomainGwURL, pubGwURLString } = getState() const input = urlOrPath + // NATIVE ipns:// with DNSLink requires simple protocol swap + if (input.startsWith('ipns://')) { + const dnslinkUrl = new URL(input) + dnslinkUrl.protocol = 'https:' + const dnslink = dnslinkResolver.readAndCacheDnslink(dnslinkUrl.hostname) + if (dnslink) { + return dnslinkUrl.toString() + } + } + // SUBDOMAINS // Detect *.dweb.link and other subdomain gateways if (isIPFS.subdomain(input)) { @@ -225,10 +252,8 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { // // Remove gateway suffix to get potential FQDN const url = new URL(subdomainUrl) - // TODO: replace below with regex that match any subdomain gw const { id: ipnsId } = subdomainPatternMatch(url) - // Ensure it includes .tld (needs at least one dot) - if (ipnsId.includes('.')) { + if (!isIPFS.cid(ipnsId)) { // Confirm DNSLink record is present and its not a false-positive const dnslink = dnslinkResolver.readAndCacheDnslink(ipnsId) if (dnslink) { diff --git a/add-on/src/popup/browser-action/context-actions.js b/add-on/src/popup/browser-action/context-actions.js index 2f3dcb500..7450ca028 100644 --- a/add-on/src/popup/browser-action/context-actions.js +++ b/add-on/src/popup/browser-action/context-actions.js @@ -52,7 +52,7 @@ function contextActions ({ const activeViewOnGateway = (currentTab) => { if (!currentTab) return false const { url } = currentTab - return !(sameGateway(url, gwURLString) || sameGateway(url, pubGwURLString)) + return !(url.startsWith('ip') || sameGateway(url, gwURLString) || sameGateway(url, pubGwURLString)) } const renderIpfsContextItems = () => { diff --git a/add-on/src/popup/browser-action/store.js b/add-on/src/popup/browser-action/store.js index 3ba3df38e..608e66964 100644 --- a/add-on/src/popup/browser-action/store.js +++ b/add-on/src/popup/browser-action/store.js @@ -218,8 +218,9 @@ module.exports = (state, emitter) => { // console.dir('toggleSiteIntegrations', state) await browser.storage.local.set({ disabledOn, enabledOn }) + const path = ipfsContentPath(currentTab.url, { keepURIParams: true }) // Reload the current tab to apply updated redirect preference - if (!currentDnslinkFqdn || !isIPFS.ipnsUrl(currentTab.url)) { + if (!currentDnslinkFqdn || !isIPFS.ipnsPath(path)) { // No DNSLink, reload URL as-is await browser.tabs.reload(currentTab.id) } else { @@ -228,7 +229,6 @@ module.exports = (state, emitter) => { // from http?://{fqdn}.ipns.gateway.tld/some/path // to http://{fqdn}/some/path // (defaulting to http: https websites will have HSTS or a redirect) - const path = ipfsContentPath(currentTab.url, { keepURIParams: true }) const originalUrl = path.replace(/^.*\/ipns\//, 'http://') await browser.tabs.update(currentTab.id, { // FF only: loadReplace: true, diff --git a/package.json b/package.json index b5a2715ab..5a6b166f1 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "ipfs-postmsg-proxy": "3.1.1", "is-fqdn": "2.0.1", "is-ip": "3.1.0", - "is-ipfs": "2.0.0", + "is-ipfs": "https://github.com/ipfs/is-ipfs/tarball/5d6d1a2aa2fc64b61f374532c1f0766ce38725f3", "it-all": "1.0.4", "it-concat": "1.0.2", "it-tar": "1.2.2", diff --git a/test/functional/lib/dnslink.test.js b/test/functional/lib/dnslink.test.js index 35c7079e3..1e52cb763 100644 --- a/test/functional/lib/dnslink.test.js +++ b/test/functional/lib/dnslink.test.js @@ -243,6 +243,16 @@ describe('dnslinkResolver (dnslinkPolicy=enabled)', function () { spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue) expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn) }) + it('should match .ipns on public subdomain gateway', function () { + // Context: https://github.com/ipfs/in-web-browsers/issues/169 + const fqdn = 'dnslink-site.com' + const fqdnInDNSLabel = 'dnslink--site-com' + const url = `https://${fqdnInDNSLabel}.ipns.dweb.link/some/path?ds=sdads#dfsdf` + const dnslinkResolver = createDnslinkResolver(getState) + spoofCachedDnslink(fqdnInDNSLabel, dnslinkResolver, false) + spoofCachedDnslink(fqdn, dnslinkResolver, dnslinkValue) + expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn) + }) }) after(() => { diff --git a/test/functional/lib/ipfs-path.test.js b/test/functional/lib/ipfs-path.test.js index e7b0e2ce5..50b5d748f 100644 --- a/test/functional/lib/ipfs-path.test.js +++ b/test/functional/lib/ipfs-path.test.js @@ -87,6 +87,18 @@ describe('ipfs-path.js', function () { const url = 'https://bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa.ipfs.dweb.link/wiki/Mars.html?argTest#hashTest' expect(ipfsContentPath(url)).to.equal('/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html') }) + it('should resolve CID(libp2p-key)-in-subdomain URL to IPNS path', function () { + const url = 'https://k2k4r8ncs1yoluq95unsd7x2vfhgve0ncjoggwqx9vyh3vl8warrcp15.ipns.dweb.link/wiki/Mars.html?argTest#hashTest' + expect(ipfsContentPath(url)).to.equal('/ipns/k2k4r8ncs1yoluq95unsd7x2vfhgve0ncjoggwqx9vyh3vl8warrcp15/wiki/Mars.html') + }) + it('should resolve dnslink-in-subdomain URL to IPNS path', function () { + const url = 'http://en.wikipedia-on-ipfs.org.ipns.localhost:8080/wiki/Mars.html?argTest#hashTest' + expect(ipfsContentPath(url)).to.equal('/ipns/en.wikipedia-on-ipfs.org/wiki/Mars.html') + }) + it('should resolve inlined-dnslink-in-subdomain URL to IPNS path', function () { + const url = 'https://en-wikipedia--on--ipfs-org.ipns.dweb.link/wiki/Mars.html?argTest#hashTest' + expect(ipfsContentPath(url)).to.equal('/ipns/en.wikipedia-on-ipfs.org/wiki/Mars.html') + }) it('should return null if there is no valid path for input URL', function () { const url = 'https://foo.io/invalid/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest' expect(ipfsContentPath(url)).to.equal(null) diff --git a/yarn.lock b/yarn.lock index b51062b01..1ca301dc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8095,7 +8095,7 @@ is-ip@^2.0.0: dependencies: ip-regex "^2.0.0" -is-ipfs@2.0.0, is-ipfs@^2.0.0: +is-ipfs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ipfs/-/is-ipfs-2.0.0.tgz#c046622e4daf5435b671aeb9739a832107e06805" integrity sha512-X4Cg/JO+h/ygBCrIQSMgicHRLo5QpB+i5tHLhFgGBksKi3zvX6ByFCshDxNBvcq4NFxF3coI2AaLqwzugNzKcw== @@ -8108,6 +8108,18 @@ is-ipfs@2.0.0, is-ipfs@^2.0.0: multihashes "^3.0.1" uint8arrays "^1.1.0" +"is-ipfs@https://github.com/ipfs/is-ipfs/tarball/5d6d1a2aa2fc64b61f374532c1f0766ce38725f3": + version "2.0.0" + resolved "https://github.com/ipfs/is-ipfs/tarball/5d6d1a2aa2fc64b61f374532c1f0766ce38725f3#91d1f03f4127094aef9e0738c43e67ea7f2c2b72" + dependencies: + cids "^1.0.0" + iso-url "~0.4.7" + mafmt "^8.0.0" + multiaddr "^8.0.0" + multibase "^3.0.0" + multihashes "^3.0.1" + uint8arrays "^1.1.0" + is-ipfs@~0.4.2: version "0.4.8" resolved "https://registry.yarnpkg.com/is-ipfs/-/is-ipfs-0.4.8.tgz#ea229aef6230433ad1e8df930c49c5e773422c3f"