Skip to content

Commit

Permalink
fix: inlined DNSLink and context actions for URIs (#961)
Browse files Browse the repository at this point in the history
This adds support for automatic redirect of inlined DNSLink names on
subdomain gateways + restores context actions for pages loaded via
native URIs.
  • Loading branch information
lidel authored Jan 18, 2021
1 parent 1a2ca94 commit faf3ddf
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 11 deletions.
37 changes: 31 additions & 6 deletions add-on/src/lib/ipfs-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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}`)
}

Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
))
},
Expand All @@ -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/<fqdn>
(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
},
Expand All @@ -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)) {
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/popup/browser-action/context-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
4 changes: 2 additions & 2 deletions add-on/src/popup/browser-action/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions test/functional/lib/dnslink.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,16 @@ describe('dnslinkResolver (dnslinkPolicy=enabled)', function () {
spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
})
it('should match <dns-label-inlined-fqdn>.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(() => {
Expand Down
12 changes: 12 additions & 0 deletions test/functional/lib/ipfs-path.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 13 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand All @@ -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"
Expand Down

0 comments on commit faf3ddf

Please sign in to comment.