Skip to content

Commit e5889f2

Browse files
committed
fix: toggle per website on <fqdn>.ipfs.localhost
1 parent a736a5f commit e5889f2

File tree

7 files changed

+97
-28
lines changed

7 files changed

+97
-28
lines changed

add-on/src/lib/dnslink.js

+7-8
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ const IsIpfs = require('is-ipfs')
99
const LRU = require('lru-cache')
1010
const { default: PQueue } = require('p-queue')
1111
const { offlinePeerCount } = require('./state')
12-
const { sameGateway, pathAtHttpGateway } = require('./ipfs-path')
13-
14-
// TODO: add Preferences toggle to disable redirect of DNSLink websites (while keeping async dnslink lookup)
12+
const { ipfsContentPath, sameGateway, pathAtHttpGateway } = require('./ipfs-path')
1513

1614
module.exports = function createDnslinkResolver (getState) {
1715
// DNSLink lookup result cache
@@ -203,19 +201,20 @@ module.exports = function createDnslinkResolver (getState) {
203201
// in url.hostname OR in url.pathname (/ipns/<fqdn>)
204202
// and return matching FQDN if present
205203
findDNSLinkHostname (url) {
206-
const { hostname, pathname } = new URL(url)
207-
// check //foo.tld/ipns/<fqdn>
208-
if (IsIpfs.ipnsPath(pathname)) {
204+
// Normalize subdomain and path gateways to to /ipns/<fqdn>
205+
const contentPath = ipfsContentPath(url)
206+
if (IsIpfs.ipnsPath(contentPath)) {
209207
// we may have false-positives here, so we do additional checks below
210-
const ipnsRoot = pathname.match(/^\/ipns\/([^/]+)/)[1]
208+
const ipnsRoot = contentPath.match(/^\/ipns\/([^/]+)/)[1]
211209
// console.log('findDNSLinkHostname ==> inspecting IPNS root', ipnsRoot)
212210
// Ignore PeerIDs, match DNSLink only
213211
if (!IsIpfs.cid(ipnsRoot) && dnslinkResolver.readAndCacheDnslink(ipnsRoot)) {
214212
// console.log('findDNSLinkHostname ==> found DNSLink for FQDN in url.pathname: ', ipnsRoot)
215213
return ipnsRoot
216214
}
217215
}
218-
// check //<fqdn>/foo/bar
216+
// Check main hostname
217+
const { hostname } = new URL(url)
219218
if (dnslinkResolver.readAndCacheDnslink(hostname)) {
220219
// console.log('findDNSLinkHostname ==> found DNSLink for url.hostname', hostname)
221220
return hostname

add-on/src/lib/http-proxy.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ async function registerSubdomainProxy (getState, runtime, notify) {
4646
// Show pop-up only the first time, during init() when notify is passed
4747
try {
4848
if (notify) notify('notify_addonIssueTitle', 'notify_addonIssueMsg')
49-
} catch (_) {}
49+
} catch (_) {
50+
}
5051
}
5152
}
5253

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

+5-4
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {
170170

171171
// Test if actions such as 'per site redirect toggle' should be enabled for the URL
172172
isRedirectPageActionsContext (url) {
173-
const { ipfsNodeType, gwURL, apiURL } = getState()
174-
return ipfsNodeType !== 'embedded' && // hide with embedded node
173+
const { localGwAvailable, gwURL, apiURL } = getState()
174+
return localGwAvailable && // show only when redirect is possible
175175
(isIPFS.ipnsUrl(url) || // show on /ipns/<fqdn>
176176
(url.startsWith('http') && // hide on non-HTTP pages
177177
!sameGateway(url, gwURL) && // hide on /ipfs/* and *.ipfs.
@@ -204,8 +204,9 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {
204204
// Instead, we resolve it to the canonical FQDN Origin
205205
//
206206
// Remove gateway suffix to get potential FQDN
207-
const url = new URL(input)
208-
const ipnsId = url.hostname.replace(`.ipns.${pubSubdomainGwURL.hostname}`, '')
207+
const url = new URL(subdomainUrl)
208+
// TODO: replace below with regex that match any subdomain gw
209+
const { id: ipnsId } = subdomainPatternMatch(url)
209210
// Ensure it includes .tld (needs at least one dot)
210211
if (ipnsId.includes('.')) {
211212
// Confirm DNSLink record is present and its not a false-positive

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

+35-6
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,31 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
6363
}
6464
}
6565

66+
// Returns a canonical hostname representing the site from url
67+
// Main reason for this is unwrapping DNSLink from local subdomain
68+
// <fqdn>.ipns.localhost → <fqdn>
69+
const findSiteFqdn = (url) => {
70+
if (isIPFS.ipnsSubdomain(url)) {
71+
// convert subdomain's <fqdn>.ipns.gateway.tld to <fqdn>
72+
const fqdn = dnslinkResolver.findDNSLinkHostname(url)
73+
if (fqdn) return fqdn
74+
}
75+
return new URL(url).hostname
76+
}
77+
78+
// Finds canonical hostname of request.url and its parent page (if present)
79+
const findSiteHostnames = (request) => {
80+
const { url, originUrl, initiator } = request
81+
const fqdn = findSiteFqdn(url)
82+
// FF: originUrl (Referer-like Origin URL), Chrome: initiator (just Origin)
83+
const parentUrl = originUrl || initiator
84+
// String value 'null' is explicitly set by Chromium in some contexts
85+
const parentFqdn = parentUrl && parentUrl !== 'null' && url !== parentUrl
86+
? findSiteFqdn(parentUrl)
87+
: null
88+
return { fqdn, parentFqdn }
89+
}
90+
6691
const preNormalizationSkip = (state, request) => {
6792
// skip requests to the custom gateway or API (otherwise we have too much recursion)
6893
if (sameGateway(request.url, state.gwURL) || sameGateway(request.url, state.apiURL)) {
@@ -76,15 +101,19 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
76101
if (request.url.startsWith('http://127.0.0.1') || request.url.startsWith('http://localhost') || request.url.startsWith('http://[::1]')) {
77102
ignore(request.requestId)
78103
}
104+
79105
// skip if a per-site opt-out exists
80-
const parentUrl = request.originUrl || request.initiator // FF: originUrl (Referer-like Origin URL), Chrome: initiator (just Origin)
81-
const fqdn = new URL(request.url).hostname
82-
const parentFqdn = parentUrl && parentUrl !== 'null' && request.url !== parentUrl ? new URL(parentUrl).hostname : null
83-
if (state.noIntegrationsHostnames.some(optout =>
84-
fqdn !== 'gateway.ipfs.io' && (fqdn.endsWith(optout) || (parentFqdn && parentFqdn.endsWith(optout))
85-
))) {
106+
const { fqdn, parentFqdn } = findSiteHostnames(request)
107+
const triggerOptOut = (optout) => {
108+
// Disable optout on canonical public gateway
109+
if (fqdn === 'gateway.ipfs.io') return false
110+
if (fqdn.endsWith(optout) || (parentFqdn && parentFqdn.endsWith(optout))) return true
111+
return false
112+
}
113+
if (state.noIntegrationsHostnames.some(triggerOptOut)) {
86114
ignore(request.requestId)
87115
}
116+
88117
// additional checks limited to requests for root documents
89118
if (request.type === 'main_frame') {
90119
// lazily trigger DNSLink lookup (will do anything only if status for root domain is not in cache)

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

+7-6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
/* eslint-env browser, webextensions */
33

44
const browser = require('webextension-polyfill')
5-
const IsIpfs = require('is-ipfs')
6-
const { trimHashAndSearch } = require('../../lib/ipfs-path')
5+
const isIPFS = require('is-ipfs')
6+
const { trimHashAndSearch, ipfsContentPath } = require('../../lib/ipfs-path')
77
const { contextMenuViewOnGateway, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress } = require('../../lib/context-menus')
88

99
// The store contains and mutates the state for the app
@@ -182,17 +182,18 @@ module.exports = (state, emitter) => {
182182
// console.dir('toggleSiteIntegrations', state)
183183
await browser.storage.local.set({ noIntegrationsHostnames })
184184

185-
// TODO: remove below? does it still make sense in "integrations toggle" context?
186185
// Reload the current tab to apply updated redirect preference
187-
if (!state.currentDnslinkFqdn || !IsIpfs.ipnsUrl(state.currentTab.url)) {
186+
if (!state.currentDnslinkFqdn || !isIPFS.ipnsUrl(state.currentTab.url)) {
188187
// No DNSLink, reload URL as-is
189188
await browser.tabs.reload(state.currentTab.id)
190189
} else {
191190
// DNSLinked websites require URL change
192-
// from http?://gateway.tld/ipns/{fqdn}/some/path
191+
// from http?://gateway.tld/ipns/{fqdn}/some/path OR
192+
// from http?://{fqdn}.ipns.gateway.tld/some/path
193193
// to http://{fqdn}/some/path
194194
// (defaulting to http: https websites will have HSTS or a redirect)
195-
const originalUrl = state.currentTab.url.replace(/^.*\/ipns\//, 'http://')
195+
const path = ipfsContentPath(state.currentTab.url, { keepURIParams: true })
196+
const originalUrl = path.replace(/^.*\/ipns\//, 'http://')
196197
await browser.tabs.update(state.currentTab.id, {
197198
// FF only: loadReplace: true,
198199
url: originalUrl

test/functional/lib/dnslink.test.js

+38
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,44 @@ describe('dnslinkResolver (dnslinkPolicy=enabled)', function () {
209209
})
210210
})
211211

212+
describe('findDNSLinkHostname(url)', function () {
213+
it('should match <fqdn> directly', function () {
214+
const fqdn = 'dnslink-site.com'
215+
const url = new URL(`https://${fqdn}/some/path?ds=sdads#dfsdf`)
216+
const dnslinkResolver = createDnslinkResolver(getState)
217+
spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
218+
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
219+
})
220+
/* TODO
221+
it('should return null if no DNSLink record', function () {
222+
const url = new URL(`https://no-dnslink.example.com/some/path?ds=sdads#dfsdf`)
223+
const dnslinkResolver = createDnslinkResolver(getState)
224+
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(undefined)
225+
})
226+
*/
227+
it('should match /ipns/<fqdn> on path gateway', function () {
228+
const fqdn = 'dnslink-site.com'
229+
const url = `https://path-gateway.com/ipns/${fqdn}/some/path?ds=sdads#dfsdf`
230+
const dnslinkResolver = createDnslinkResolver(getState)
231+
spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
232+
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
233+
})
234+
it('should match <fqdn>.ipns on local subdomain gateway', function () {
235+
const fqdn = 'dnslink-site.com'
236+
const url = `https://${fqdn}.ipns.localhost:8080/some/path?ds=sdads#dfsdf`
237+
const dnslinkResolver = createDnslinkResolver(getState)
238+
spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
239+
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
240+
})
241+
it('should match <fqdn>.ipns on public subdomain gateway', function () {
242+
const fqdn = 'dnslink-site.com'
243+
const url = `https://${fqdn}.ipns.dweb.link/some/path?ds=sdads#dfsdf`
244+
const dnslinkResolver = createDnslinkResolver(getState)
245+
spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
246+
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
247+
})
248+
})
249+
212250
after(() => {
213251
delete global.URL
214252
})

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('init', function () {
2525
browser.storage.local.set.returns(Promise.resolve())
2626
const ipfsCompanion = await init()
2727
browser.storage.local.get.calledWith(optionDefaults)
28-
ipfsCompanion.destroy()
28+
await ipfsCompanion.destroy()
2929
})
3030

3131
it('should fixup migrated files APIs', async function () {
@@ -42,7 +42,7 @@ describe('init', function () {
4242
expect(typeof ipfsCompanion.ipfs[cmd], `ipfs.${cmd} expected to be a function`).to.equal('function')
4343
expect(typeof ipfsCompanion.ipfs.files[cmd], `ipfs.files.${cmd} expected to be a function`).to.equal('function')
4444
}
45-
ipfsCompanion.destroy()
45+
await ipfsCompanion.destroy()
4646
})
4747

4848
after(function () {
@@ -86,7 +86,7 @@ describe.skip('onStorageChange()', function () {
8686
const ipfs = global.window.ipfs
8787
browser.storage.onChanged.dispatch(changes, area)
8888
expect(ipfs).to.not.equal(window.ipfs)
89-
ipfsCompanion.destroy()
89+
await ipfsCompanion.destroy()
9090
})
9191

9292
after(function () {

0 commit comments

Comments
 (0)