Skip to content

Commit 678c714

Browse files
authored
Merge pull request #687 from ipfs-shipyard/feat/redirect-toggle-per-website
Per-site Redirect Opt-out This PR merges reworked redirect controls and adds opt-out per site - menu item in Active Tab section enables user to disable gateway redirect on current website - when clicked on regular site toggles redirect for current FQDN and all its subdomains - when clicked on /ipns/<fqdn>/ (DNSLink) website, toggles redirect for <fqdn> - after redirect preference changes for current website, the tab is reloaded - DNSLink websites are reloaded to with URL change between IPNS path and original URL - Redirect preference applies not only to request with FQDN of the active tab, but also to all subresource requests that have it in `originUrl` (Firefox) or `Referer` header (Chrome). This means toggle is useful to restore functionality of complex websites such as d.tube (which uses IPFS subresources from various subdomains) - reworked UI of browser action menu, labels no longer change, instead we introduce toggle-switch
2 parents 6578886 + f17ae7c commit 678c714

28 files changed

+612
-178
lines changed

README.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,10 @@ More details: [`x-ipfs-path` Header Support in IPFS Companion](https://github.co
6969

7070
#### Redirect Opt-Out
7171

72-
It is possible to opt-out from redirect by
73-
a) suspending extension via global toggle
74-
b) including `x-ipfs-companion-no-redirect` in the URL (as a [hash](https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR#x-ipfs-companion-no-redirect) or [query](https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?x-ipfs-companion-no-redirect) parameter).
72+
It is possible to opt-out from Gateway redirect by:
73+
- a) suspending redirect via global toggle (see [_Disable All Redirects_](#disable-all-redirects) below)
74+
- b) suspending redirect for via per website opt-out (in [_Active Tab_ section of _Browser Action_](#disable-gateway-redirect-per-website) or _Preferences_)
75+
- c) including `x-ipfs-companion-no-redirect` in the URL (as a [hash](https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR#x-ipfs-companion-no-redirect) or [query](https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?x-ipfs-companion-no-redirect) parameter).
7576

7677
### IPFS API as `window.ipfs`
7778

@@ -80,11 +81,30 @@ Websites can detect if `window.ipfs` exists and opt-in to use it instead of crea
8081
It saves system resources and battery (on mobile), avoids the overhead of peer discovery/connection, enables shared repository access and more!
8182
Make sure to read our [notes on `window.ipfs`](https://github.com/ipfs-shipyard/ipfs-companion/blob/master/docs/window.ipfs.md), where we explain it in-depth and provide examples on how to use it your own dapp.
8283

83-
### Toggle IPFS Integrations
84+
### Quick Toggles
8485

85-
> ![screenshot of suspend toggle](https://user-images.githubusercontent.com/157609/42685002-18c7cee4-8692-11e8-9171-970866d91ae0.gif)
86+
The Browser Action pop-up provides handy toggles for often used operations.
8687

87-
The Browser Action pop-up provides a toggle for suspending all active IPFS integrations with a single click.
88+
#### Disable Gateway Redirect Per Website
89+
90+
> _Active Tab_ actions include option to opt-out current website from Gateway redirect of any IPFS subresources.
91+
> Disabling redirect for DNSLink website will restore original URL as well:
92+
>
93+
> ![per-site-peek 2019-02-26 00-23](https://user-images.githubusercontent.com/157609/53376094-86557500-395d-11e9-837f-a4712aa19236.gif)
94+
95+
#### Disable All Redirects
96+
97+
> A handy toggle to disable all gateway redirects while keeping all other features enabled:
98+
>
99+
> ![redirect](https://user-images.githubusercontent.com/157609/53376263-0976cb00-395e-11e9-8536-d83d28ffeee9.gif)
100+
101+
#### Suspend IPFS Extension
102+
103+
> The "power" icon can be used to temporarily suspend all IPFS integrations
104+
> (redirects, API status, content scripts, protocol handlers etc).
105+
> Useful during testing. Extension can be re-enabled with a single click:
106+
>
107+
> ![screenshot of suspend toggle](https://user-images.githubusercontent.com/157609/53376196-d6343c00-395d-11e9-83f2-04c16b3a008f.gif)
88108
89109
### IPFS Status and Context Actions
90110

add-on/_locales/en/messages.json

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"description": "A label for IPFS icon (panel_headerIpfsNodeIconLabel)"
99
},
1010
"panel_headerActiveToggleTitle": {
11-
"message": "Global toggle: Suspend all IPFS integrations",
11+
"message": "Toggle all IPFS integrations",
1212
"description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)"
1313
},
1414
"panel_statusOffline": {
@@ -43,25 +43,41 @@
4343
"message": "Open Web UI",
4444
"description": "A menu item in Browser Action pop-up (panel_openWebui)"
4545
},
46+
"panel_redirectToggle": {
47+
"message": "Redirect to Gateway",
48+
"description": "A menu item in Browser Action pop-up (panel_redirectToggle)"
49+
},
50+
"panel_redirectToggleTooltip": {
51+
"message": "Click to toggle all gateway redirects",
52+
"description": "A menu item in Browser Action pop-up (panel_redirectToggleTooltip)"
53+
},
54+
"panel_toolsSectionHeader": {
55+
"message": "Tools",
56+
"description": "A menu item in Browser Action pop-up (panel_toolsSectionHeader)"
57+
},
4658
"panel_openPreferences": {
4759
"message": "Open Preferences of Browser Extension",
4860
"description": "A menu item in Browser Action pop-up (panel_openPreferences)"
4961
},
50-
"panel_switchToCustomGateway": {
51-
"message": "Switch to Custom Gateway",
52-
"description": "A menu item in Browser Action pop-up (panel_switchToCustomGateway)"
62+
"panel_activeTabSectionHeader": {
63+
"message": "Active Tab",
64+
"description": "A menu item in Browser Action pop-up (panel_activeTabSiteRedirectEnable)"
5365
},
54-
"panel_switchToPublicGateway": {
55-
"message": "Switch to Public Gateway",
56-
"description": "A menu item in Browser Action pop-up (panel_switchToPublicGateway)"
66+
"panel_activeTabSiteRedirectToggle": {
67+
"message": "Redirect on $1",
68+
"description": "A menu item in Browser Action pop-up (panel_activeTabSiteRedirectToggle)"
69+
},
70+
"panel_activeTabSiteRedirectToggleTooltip": {
71+
"message": "Click to toggle gateway redirects on $1",
72+
"description": "A menu item tooltip in Browser Action pop-up (panel_activeTabSiteRedirectToggleTooltip)"
5773
},
5874
"panel_pinCurrentIpfsAddress": {
5975
"message": "Pin IPFS Resource",
6076
"description": "A menu item in Browser Action pop-up (panel_pinCurrentIpfsAddress)"
6177
},
62-
"panel_unpinCurrentIpfsAddress": {
63-
"message": "Unpin IPFS Resource",
64-
"description": "A menu item in Browser Action pop-up (panel_unpinCurrentIpfsAddress)"
78+
"panel_pinCurrentIpfsAddressTooltip": {
79+
"message": "Pinning a CID tells your IPFS node that this data is important and mustn’t be thrown away.",
80+
"description": "A menu item tooltip in Browser Action pop-up (panel_pinCurrentIpfsAddressTooltip)"
6581
},
6682
"panelCopy_currentIpfsAddress": {
6783
"message": "Copy IPFS Path",
@@ -251,6 +267,14 @@
251267
"message": "Redirect requests for IPFS resources to the Custom gateway",
252268
"description": "An option description on the Preferences screen (option_useCustomGateway_description)"
253269
},
270+
"option_noRedirectHostnames_title": {
271+
"message": "Redirect Opt-Outs",
272+
"description": "An option title on the Preferences screen (option_noRedirectHostnames_title)"
273+
},
274+
"option_noRedirectHostnames_description": {
275+
"message": "List of websites that should not be redirected to the Custom Gateway (includes subresources from other domains). One hostname per line.",
276+
"description": "An option description on the Preferences screen (option_noRedirectHostnames_description)"
277+
},
254278
"option_publicGatewayUrl_title": {
255279
"message": "Default Public Gateway",
256280
"description": "An option title on the Preferences screen (option_publicGatewayUrl_title)"

add-on/src/lib/dnslink.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,29 @@ module.exports = function createDnslinkResolver (getState) {
175175
}
176176
const fqdn = url.hostname
177177
return `/ipns/${fqdn}${url.pathname}${url.search}${url.hash}`
178+
},
179+
180+
// Test if URL contains a valid DNSLink FQDN
181+
// in url.hostname OR in url.pathname (/ipns/<fqdn>)
182+
// and return matching FQDN if present
183+
findDNSLinkHostname (url) {
184+
const { hostname, pathname } = new URL(url)
185+
// check //foo.tld/ipns/<fqdn>
186+
if (IsIpfs.ipnsPath(pathname)) {
187+
// we may have false-positives here, so we do additional checks below
188+
const ipnsRoot = pathname.match(/^\/ipns\/([^/]+)/)[1]
189+
// console.log('findDNSLinkHostname ==> inspecting IPNS root', ipnsRoot)
190+
// Ignore PeerIDs, match DNSLink only
191+
if (!IsIpfs.cid(ipnsRoot) && dnslinkResolver.readAndCacheDnslink(ipnsRoot)) {
192+
// console.log('findDNSLinkHostname ==> found DNSLink for FQDN in url.pathname: ', ipnsRoot)
193+
return ipnsRoot
194+
}
195+
}
196+
// check //<fqdn>/foo/bar
197+
if (dnslinkResolver.readAndCacheDnslink(hostname)) {
198+
// console.log('findDNSLinkHostname ==> found DNSLink for url.hostname', hostname)
199+
return hostname
200+
}
178201
}
179202

180203
}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,13 +213,17 @@ module.exports = async function init () {
213213

214214
async function sendStatusUpdateToBrowserAction () {
215215
if (!browserActionPort) return
216+
const dropSlash = url => url.replace(/\/$/, '')
216217
const info = {
217218
active: state.active,
218219
ipfsNodeType: state.ipfsNodeType,
219220
peerCount: state.peerCount,
220-
gwURLString: state.gwURLString,
221-
pubGwURLString: state.pubGwURLString,
221+
gwURLString: dropSlash(state.gwURLString),
222+
pubGwURLString: dropSlash(state.pubGwURLString),
222223
webuiRootUrl: state.webuiRootUrl,
224+
apiURLString: dropSlash(state.apiURLString),
225+
redirect: state.redirect,
226+
noRedirectHostnames: state.noRedirectHostnames,
223227
currentTab: await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0])
224228
}
225229
try {
@@ -231,7 +235,12 @@ module.exports = async function init () {
231235
info.gatewayVersion = null
232236
}
233237
if (info.currentTab) {
234-
info.ipfsPageActionsContext = ipfsPathValidator.isIpfsPageActionsContext(info.currentTab.url)
238+
const url = info.currentTab.url
239+
info.isIpfsContext = ipfsPathValidator.isIpfsPageActionsContext(url)
240+
info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(url)
241+
info.currentFqdn = info.currentDnslinkFqdn || new URL(url).hostname
242+
info.currentTabRedirectOptOut = info.noRedirectHostnames && info.noRedirectHostnames.includes(info.currentFqdn)
243+
info.isRedirectContext = info.currentFqdn && ipfsPathValidator.isRedirectPageActionsContext(url)
235244
}
236245
// Still here?
237246
if (browserActionPort) {
@@ -641,6 +650,7 @@ module.exports = async function init () {
641650
case 'automaticMode':
642651
case 'detectIpfsPathHeader':
643652
case 'preloadAtPublicGateway':
653+
case 'noRedirectHostnames':
644654
state[key] = change.newValue
645655
break
646656
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,19 @@ function createIpfsPathValidator (getState, dnsLink) {
6464
},
6565

6666
// Test if actions such as 'copy URL', 'pin/unpin' should be enabled for the URL
67+
// TODO: include hostname check for DNSLink and display option to copy CID even if no redirect
6768
isIpfsPageActionsContext (url) {
6869
return (IsIpfs.url(url) && !url.startsWith(getState().apiURLString)) || IsIpfs.subdomain(url)
70+
},
71+
72+
// Test if actions such as 'per site redirect toggle' should be enabled for the URL
73+
isRedirectPageActionsContext (url) {
74+
const state = getState()
75+
return state.ipfsNodeType === 'external' && // hide with embedded node
76+
(IsIpfs.ipnsUrl(url) || // show on /ipns/<fqdn>
77+
(url.startsWith('http') && // hide on non-HTTP pages
78+
!url.startsWith(state.gwURLString) && // hide on /ipfs/*
79+
!url.startsWith(state.apiURLString))) // hide on api port
6980
}
7081
}
7182

@@ -110,6 +121,7 @@ function validIpnsPath (path, dnsLink) {
110121
// console.log('==> IPNS is a valid CID', ipnsRoot)
111122
return true
112123
}
124+
// then see if there is an DNSLink entry for 'ipnsRoot' hostname
113125
if (dnsLink.readAndCacheDnslink(ipnsRoot)) {
114126
// console.log('==> IPNS for FQDN with valid dnslink: ', ipnsRoot)
115127
return true

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
6565
if (request.url.startsWith('http://127.0.0.1') || request.url.startsWith('http://localhost') || request.url.startsWith('http://[::1]')) {
6666
ignore(request.requestId)
6767
}
68+
// skip if a per-site redirect opt-out exists
69+
const parentUrl = request.originUrl || request.initiator // FF: originUrl (Referer-like Origin URL), Chrome: initiator (just Origin)
70+
const fqdn = new URL(request.url).hostname
71+
const parentFqdn = parentUrl && request.url !== parentUrl ? new URL(parentUrl).hostname : null
72+
if (state.noRedirectHostnames.some(optout =>
73+
fqdn.endsWith(optout) || (parentFqdn && parentFqdn.endsWith(optout)
74+
))) {
75+
ignore(request.requestId)
76+
}
77+
// additional checks limited to requests for root documents
78+
if (request.type === 'main_frame') {
79+
// lazily trigger DNSLink lookup (will do anything only if status for root domain is not in cache)
80+
if (state.dnslinkPolicy && dnslinkResolver.canLookupURL(request.url)) {
81+
dnslinkResolver.preloadDnslink(request.url)
82+
}
83+
}
6884
return isIgnored(request.requestId)
6985
}
7086

add-on/src/lib/options.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict'
22

3+
const isFQDN = require('is-fqdn')
4+
35
exports.optionDefaults = Object.freeze({
46
active: true, // global ON/OFF switch, overrides everything else
57
ipfsNodeType: 'external', // or 'embedded'
@@ -12,6 +14,7 @@ exports.optionDefaults = Object.freeze({
1214
}, null, 2),
1315
publicGatewayUrl: 'https://ipfs.io',
1416
useCustomGateway: true,
17+
noRedirectHostnames: [],
1518
automaticMode: true,
1619
linkify: false,
1720
dnslinkPolicy: 'best-effort',
@@ -22,7 +25,7 @@ exports.optionDefaults = Object.freeze({
2225
customGatewayUrl: 'http://127.0.0.1:8080',
2326
ipfsApiUrl: 'http://127.0.0.1:5001',
2427
ipfsApiPollMs: 3000,
25-
ipfsProxy: true
28+
ipfsProxy: true // window.ipfs
2629
})
2730

2831
// `storage` should be a browser.storage.local or similar
@@ -64,6 +67,23 @@ function normalizeGatewayURL (url) {
6467
exports.normalizeGatewayURL = normalizeGatewayURL
6568
exports.safeURL = (url) => new URL(normalizeGatewayURL(url))
6669

70+
// convert JS array to multiline textarea
71+
function hostArrayCleanup (array) {
72+
array = array.map(host => host.trim().toLowerCase())
73+
array = [...new Set(array)] // dedup
74+
array = array.filter(Boolean).filter(isFQDN)
75+
array.sort()
76+
return array
77+
}
78+
function hostArrayToText (array) {
79+
return hostArrayCleanup(array).join('\n')
80+
}
81+
function hostTextToArray (text) {
82+
return hostArrayCleanup(text.split('\n'))
83+
}
84+
exports.hostArrayToText = hostArrayToText
85+
exports.hostTextToArray = hostTextToArray
86+
6787
exports.migrateOptions = async (storage) => {
6888
// <= v2.4.4
6989
// DNSLINK: convert old on/off 'dnslink' flag to text-based 'dnslinkPolicy'

add-on/src/options/forms/gateways-form.js

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
const browser = require('webextension-polyfill')
55
const html = require('choo/html')
6-
const { normalizeGatewayURL } = require('../../lib/options')
6+
const { normalizeGatewayURL, hostTextToArray, hostArrayToText } = require('../../lib/options')
77

88
// Warn about mixed content issues when changing the gateway
99
// https://github.com/ipfs-shipyard/ipfs-companion/issues/648
@@ -13,19 +13,40 @@ function gatewaysForm ({
1313
ipfsNodeType,
1414
customGatewayUrl,
1515
useCustomGateway,
16+
noRedirectHostnames,
1617
publicGatewayUrl,
1718
onOptionChange
1819
}) {
1920
const onCustomGatewayUrlChange = onOptionChange('customGatewayUrl', normalizeGatewayURL)
2021
const onUseCustomGatewayChange = onOptionChange('useCustomGateway')
2122
const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', normalizeGatewayURL)
23+
const onNoRedirectHostnamesChange = onOptionChange('noRedirectHostnames', hostTextToArray)
2224
const mixedContentWarning = !secureContextUrl.test(customGatewayUrl)
25+
const supportRedirectToCustomGateway = ipfsNodeType === 'external'
2326

2427
return html`
2528
<form>
2629
<fieldset>
2730
<legend>${browser.i18n.getMessage('option_header_gateways')}</legend>
28-
${ipfsNodeType === 'external' ? html`
31+
<div>
32+
<label for="publicGatewayUrl">
33+
<dl>
34+
<dt>${browser.i18n.getMessage('option_publicGatewayUrl_title')}</dt>
35+
<dd>${browser.i18n.getMessage('option_publicGatewayUrl_description')}</dd>
36+
</dl>
37+
</label>
38+
<input
39+
id="publicGatewayUrl"
40+
type="url"
41+
inputmode="url"
42+
required
43+
pattern="^https?://[^/]+/?$"
44+
spellcheck="false"
45+
title="Enter URL without any sub-path"
46+
onchange=${onPublicGatewayUrlChange}
47+
value=${publicGatewayUrl} />
48+
</div>
49+
${supportRedirectToCustomGateway ? html`
2950
<div>
3051
<label for="customGatewayUrl">
3152
<dl>
@@ -48,7 +69,7 @@ function gatewaysForm ({
4869
4970
</div>
5071
` : null}
51-
${ipfsNodeType === 'external' ? html`
72+
${supportRedirectToCustomGateway ? html`
5273
<div>
5374
<label for="useCustomGateway">
5475
<dl>
@@ -63,24 +84,22 @@ function gatewaysForm ({
6384
checked=${useCustomGateway} />
6485
</div>
6586
` : null}
66-
<div>
67-
<label for="publicGatewayUrl">
68-
<dl>
69-
<dt>${browser.i18n.getMessage('option_publicGatewayUrl_title')}</dt>
70-
<dd>${browser.i18n.getMessage('option_publicGatewayUrl_description')}</dd>
71-
</dl>
72-
</label>
73-
<input
74-
id="publicGatewayUrl"
75-
type="url"
76-
inputmode="url"
77-
required
78-
pattern="^https?://[^/]+/?$"
79-
spellcheck="false"
80-
title="Enter URL without any sub-path"
81-
onchange=${onPublicGatewayUrlChange}
82-
value=${publicGatewayUrl} />
83-
</div>
87+
${supportRedirectToCustomGateway ? html`
88+
<div>
89+
<label for="noRedirectHostnames">
90+
<dl>
91+
<dt>${browser.i18n.getMessage('option_noRedirectHostnames_title')}</dt>
92+
<dd>${browser.i18n.getMessage('option_noRedirectHostnames_description')}</dd>
93+
</dl>
94+
</label>
95+
<textarea
96+
id="noRedirectHostnames"
97+
spellcheck="false"
98+
onchange=${onNoRedirectHostnamesChange}
99+
rows="4"
100+
>${hostArrayToText(noRedirectHostnames)}</textarea>
101+
</div>
102+
` : null}
84103
</fieldset>
85104
</form>
86105
`

0 commit comments

Comments
 (0)