Skip to content

Commit

Permalink
fix: toggle per website on <fqdn>.ipfs.localhost
Browse files Browse the repository at this point in the history
  • Loading branch information
lidel committed Apr 3, 2020
1 parent a736a5f commit e5889f2
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 28 deletions.
15 changes: 7 additions & 8 deletions add-on/src/lib/dnslink.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ const IsIpfs = require('is-ipfs')
const LRU = require('lru-cache')
const { default: PQueue } = require('p-queue')
const { offlinePeerCount } = require('./state')
const { sameGateway, pathAtHttpGateway } = require('./ipfs-path')

// TODO: add Preferences toggle to disable redirect of DNSLink websites (while keeping async dnslink lookup)
const { ipfsContentPath, sameGateway, pathAtHttpGateway } = require('./ipfs-path')

module.exports = function createDnslinkResolver (getState) {
// DNSLink lookup result cache
Expand Down Expand Up @@ -203,19 +201,20 @@ module.exports = function createDnslinkResolver (getState) {
// in url.hostname OR in url.pathname (/ipns/<fqdn>)
// and return matching FQDN if present
findDNSLinkHostname (url) {
const { hostname, pathname } = new URL(url)
// check //foo.tld/ipns/<fqdn>
if (IsIpfs.ipnsPath(pathname)) {
// Normalize subdomain and path gateways to to /ipns/<fqdn>
const contentPath = ipfsContentPath(url)
if (IsIpfs.ipnsPath(contentPath)) {
// we may have false-positives here, so we do additional checks below
const ipnsRoot = pathname.match(/^\/ipns\/([^/]+)/)[1]
const ipnsRoot = contentPath.match(/^\/ipns\/([^/]+)/)[1]
// console.log('findDNSLinkHostname ==> inspecting IPNS root', ipnsRoot)
// Ignore PeerIDs, match DNSLink only
if (!IsIpfs.cid(ipnsRoot) && dnslinkResolver.readAndCacheDnslink(ipnsRoot)) {
// console.log('findDNSLinkHostname ==> found DNSLink for FQDN in url.pathname: ', ipnsRoot)
return ipnsRoot
}
}
// check //<fqdn>/foo/bar
// Check main hostname
const { hostname } = new URL(url)
if (dnslinkResolver.readAndCacheDnslink(hostname)) {
// console.log('findDNSLinkHostname ==> found DNSLink for url.hostname', hostname)
return hostname
Expand Down
3 changes: 2 additions & 1 deletion add-on/src/lib/http-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ async function registerSubdomainProxy (getState, runtime, notify) {
// Show pop-up only the first time, during init() when notify is passed
try {
if (notify) notify('notify_addonIssueTitle', 'notify_addonIssueMsg')
} catch (_) {}
} catch (_) {
}
}
}

Expand Down
9 changes: 5 additions & 4 deletions add-on/src/lib/ipfs-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {

// Test if actions such as 'per site redirect toggle' should be enabled for the URL
isRedirectPageActionsContext (url) {
const { ipfsNodeType, gwURL, apiURL } = getState()
return ipfsNodeType !== 'embedded' && // hide with embedded node
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
!sameGateway(url, gwURL) && // hide on /ipfs/* and *.ipfs.
Expand Down Expand Up @@ -204,8 +204,9 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {
// Instead, we resolve it to the canonical FQDN Origin
//
// Remove gateway suffix to get potential FQDN
const url = new URL(input)
const ipnsId = url.hostname.replace(`.ipns.${pubSubdomainGwURL.hostname}`, '')
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('.')) {
// Confirm DNSLink record is present and its not a false-positive
Expand Down
41 changes: 35 additions & 6 deletions add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,31 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
}
}

// Returns a canonical hostname representing the site from url
// Main reason for this is unwrapping DNSLink from local subdomain
// <fqdn>.ipns.localhost → <fqdn>
const findSiteFqdn = (url) => {
if (isIPFS.ipnsSubdomain(url)) {
// convert subdomain's <fqdn>.ipns.gateway.tld to <fqdn>
const fqdn = dnslinkResolver.findDNSLinkHostname(url)
if (fqdn) return fqdn
}
return new URL(url).hostname
}

// Finds canonical hostname of request.url and its parent page (if present)
const findSiteHostnames = (request) => {
const { url, originUrl, initiator } = request
const fqdn = findSiteFqdn(url)
// FF: originUrl (Referer-like Origin URL), Chrome: initiator (just Origin)
const parentUrl = originUrl || initiator
// String value 'null' is explicitly set by Chromium in some contexts
const parentFqdn = parentUrl && parentUrl !== 'null' && url !== parentUrl
? findSiteFqdn(parentUrl)
: null
return { fqdn, parentFqdn }
}

const preNormalizationSkip = (state, request) => {
// skip requests to the custom gateway or API (otherwise we have too much recursion)
if (sameGateway(request.url, state.gwURL) || sameGateway(request.url, state.apiURL)) {
Expand All @@ -76,15 +101,19 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
if (request.url.startsWith('http://127.0.0.1') || request.url.startsWith('http://localhost') || request.url.startsWith('http://[::1]')) {
ignore(request.requestId)
}

// skip if a per-site opt-out exists
const parentUrl = request.originUrl || request.initiator // FF: originUrl (Referer-like Origin URL), Chrome: initiator (just Origin)
const fqdn = new URL(request.url).hostname
const parentFqdn = parentUrl && parentUrl !== 'null' && request.url !== parentUrl ? new URL(parentUrl).hostname : null
if (state.noIntegrationsHostnames.some(optout =>
fqdn !== 'gateway.ipfs.io' && (fqdn.endsWith(optout) || (parentFqdn && parentFqdn.endsWith(optout))
))) {
const { fqdn, parentFqdn } = findSiteHostnames(request)
const triggerOptOut = (optout) => {
// Disable optout on canonical public gateway
if (fqdn === 'gateway.ipfs.io') return false
if (fqdn.endsWith(optout) || (parentFqdn && parentFqdn.endsWith(optout))) return true
return false
}
if (state.noIntegrationsHostnames.some(triggerOptOut)) {
ignore(request.requestId)
}

// additional checks limited to requests for root documents
if (request.type === 'main_frame') {
// lazily trigger DNSLink lookup (will do anything only if status for root domain is not in cache)
Expand Down
13 changes: 7 additions & 6 deletions add-on/src/popup/browser-action/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
/* eslint-env browser, webextensions */

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

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

// TODO: remove below? does it still make sense in "integrations toggle" context?
// Reload the current tab to apply updated redirect preference
if (!state.currentDnslinkFqdn || !IsIpfs.ipnsUrl(state.currentTab.url)) {
if (!state.currentDnslinkFqdn || !isIPFS.ipnsUrl(state.currentTab.url)) {
// No DNSLink, reload URL as-is
await browser.tabs.reload(state.currentTab.id)
} else {
// DNSLinked websites require URL change
// from http?://gateway.tld/ipns/{fqdn}/some/path
// from http?://gateway.tld/ipns/{fqdn}/some/path OR
// 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 originalUrl = state.currentTab.url.replace(/^.*\/ipns\//, 'http://')
const path = ipfsContentPath(state.currentTab.url, { keepURIParams: true })
const originalUrl = path.replace(/^.*\/ipns\//, 'http://')
await browser.tabs.update(state.currentTab.id, {
// FF only: loadReplace: true,
url: originalUrl
Expand Down
38 changes: 38 additions & 0 deletions test/functional/lib/dnslink.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,44 @@ describe('dnslinkResolver (dnslinkPolicy=enabled)', function () {
})
})

describe('findDNSLinkHostname(url)', function () {
it('should match <fqdn> directly', function () {
const fqdn = 'dnslink-site.com'
const url = new URL(`https://${fqdn}/some/path?ds=sdads#dfsdf`)
const dnslinkResolver = createDnslinkResolver(getState)
spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
})
/* TODO
it('should return null if no DNSLink record', function () {
const url = new URL(`https://no-dnslink.example.com/some/path?ds=sdads#dfsdf`)
const dnslinkResolver = createDnslinkResolver(getState)
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(undefined)
})
*/
it('should match /ipns/<fqdn> on path gateway', function () {
const fqdn = 'dnslink-site.com'
const url = `https://path-gateway.com/ipns/${fqdn}/some/path?ds=sdads#dfsdf`
const dnslinkResolver = createDnslinkResolver(getState)
spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
})
it('should match <fqdn>.ipns on local subdomain gateway', function () {
const fqdn = 'dnslink-site.com'
const url = `https://${fqdn}.ipns.localhost:8080/some/path?ds=sdads#dfsdf`
const dnslinkResolver = createDnslinkResolver(getState)
spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
})
it('should match <fqdn>.ipns on public subdomain gateway', function () {
const fqdn = 'dnslink-site.com'
const url = `https://${fqdn}.ipns.dweb.link/some/path?ds=sdads#dfsdf`
const dnslinkResolver = createDnslinkResolver(getState)
spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
})
})

after(() => {
delete global.URL
})
Expand Down
6 changes: 3 additions & 3 deletions test/functional/lib/ipfs-companion.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('init', function () {
browser.storage.local.set.returns(Promise.resolve())
const ipfsCompanion = await init()
browser.storage.local.get.calledWith(optionDefaults)
ipfsCompanion.destroy()
await ipfsCompanion.destroy()
})

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

after(function () {
Expand Down Expand Up @@ -86,7 +86,7 @@ describe.skip('onStorageChange()', function () {
const ipfs = global.window.ipfs
browser.storage.onChanged.dispatch(changes, area)
expect(ipfs).to.not.equal(window.ipfs)
ipfsCompanion.destroy()
await ipfsCompanion.destroy()
})

after(function () {
Expand Down

0 comments on commit e5889f2

Please sign in to comment.