Skip to content

Commit eb8723a

Browse files
committed
feat: per-site redirect opt-out
Changes: - moved global redirect toggle from Browser Action menu to the utility icon row, under "redirect" icon - added animation to utility icons - global redirect icon will enable integrations if clicked when in suspended state - menu items specific to the Active Tab are marked with additional border (just a prototype, needs refinement) - Redirect opt-out per site - new menu item in Active Tab section - 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 requests to URLs with with FQDN of the active tab, but also to all subresource requests that have it in `originUrl` (Firefox) or `Referer` header (Chrome)
1 parent 6578886 commit eb8723a

19 files changed

+606
-84
lines changed

add-on/_locales/en/messages.json

+18-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
"message": "Global toggle: Suspend all IPFS integrations",
1212
"description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)"
1313
},
14+
"panel_headerRedirectToggleTitle": {
15+
"message": "Global toggle: Stop all gateway redirections",
16+
"description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)"
17+
},
1418
"panel_statusOffline": {
1519
"message": "offline",
1620
"description": "A label in Node status section of Browser Action pop-up (panel_statusOffline)"
@@ -47,13 +51,21 @@
4751
"message": "Open Preferences of Browser Extension",
4852
"description": "A menu item in Browser Action pop-up (panel_openPreferences)"
4953
},
50-
"panel_switchToCustomGateway": {
51-
"message": "Switch to Custom Gateway",
52-
"description": "A menu item in Browser Action pop-up (panel_switchToCustomGateway)"
54+
"panel_globalRedirectEnable": {
55+
"message": "Enable All Redirects",
56+
"description": "A menu item in Browser Action pop-up (panel_globalRedirectEnable)"
57+
},
58+
"panel_activeTabSectionHeader": {
59+
"message": "Active Tab",
60+
"description": "A menu item in Browser Action pop-up (panel_activeTabSiteRedirectEnable)"
61+
},
62+
"panel_activeTabSiteRedirectEnable": {
63+
"message": "Restore Redirects on $1",
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_activeTabSiteRedirectDisable": {
67+
"message": "Disable Redirects on $1",
68+
"description": "A menu item in Browser Action pop-up (panel_activeTabSiteRedirectDisable)"
5769
},
5870
"panel_pinCurrentIpfsAddress": {
5971
"message": "Pin IPFS Resource",

add-on/src/lib/dnslink.js

+22
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,28 @@ 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 hostname, FQDN in /ipns/ path
181+
// and return original hostname if present
182+
findDNSLinkHostname (url) {
183+
const { hostname, pathname } = new URL(url)
184+
// check //foo.tld/ipns/<fqdn>
185+
if (IsIpfs.ipnsPath(pathname)) {
186+
// we may have false-positives here, so we do additional checks below
187+
const ipnsRoot = pathname.match(/^\/ipns\/([^/]+)/)[1]
188+
// console.log('findDNSLinkHostname ==> inspecting IPNS root', ipnsRoot)
189+
// Ignore PeerIDs, match DNSLink only
190+
if (!IsIpfs.cid(ipnsRoot) && dnslinkResolver.readAndCacheDnslink(ipnsRoot)) {
191+
// console.log('findDNSLinkHostname ==> found DNSLink for FQDN in url.pathname: ', ipnsRoot)
192+
return ipnsRoot
193+
}
194+
}
195+
// check //<fqdn>/foo/bar
196+
if (dnslinkResolver.readAndCacheDnslink(hostname)) {
197+
// console.log('findDNSLinkHostname ==> found DNSLink for url.hostname', hostname)
198+
return hostname
199+
}
178200
}
179201

180202
}

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

+10-2
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 {
@@ -232,6 +236,9 @@ module.exports = async function init () {
232236
}
233237
if (info.currentTab) {
234238
info.ipfsPageActionsContext = ipfsPathValidator.isIpfsPageActionsContext(info.currentTab.url)
239+
info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(info.currentTab.url)
240+
info.currentFqdn = info.currentDnslinkFqdn || new URL(info.currentTab.url).hostname
241+
info.currentTabRedirectOptOut = info.noRedirectHostnames && info.noRedirectHostnames.includes(info.currentFqdn)
235242
}
236243
// Still here?
237244
if (browserActionPort) {
@@ -641,6 +648,7 @@ module.exports = async function init () {
641648
case 'automaticMode':
642649
case 'detectIpfsPathHeader':
643650
case 'preloadAtPublicGateway':
651+
case 'noRedirectHostnames':
644652
state[key] = change.newValue
645653
break
646654
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ 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)
6970
}
@@ -110,6 +111,7 @@ function validIpnsPath (path, dnsLink) {
110111
// console.log('==> IPNS is a valid CID', ipnsRoot)
111112
return true
112113
}
114+
// then see if there is an DNSLink entry for 'ipnsRoot' hostname
113115
if (dnsLink.readAndCacheDnslink(ipnsRoot)) {
114116
// console.log('==> IPNS for FQDN with valid dnslink: ', ipnsRoot)
115117
return true

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

+16
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, Chrome: initiator
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+
// trigger DNSLink lookup if status for root domain is not in cache yet
80+
if (state.dnslinkPolicy && dnslinkResolver.canLookupURL(request.url)) {
81+
(async () => dnslinkResolver.readAndCacheDnslink(parentFqdn || fqdn))()
82+
}
83+
}
6884
return isIgnored(request.requestId)
6985
}
7086

add-on/src/lib/options.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ exports.optionDefaults = Object.freeze({
1212
}, null, 2),
1313
publicGatewayUrl: 'https://ipfs.io',
1414
useCustomGateway: true,
15+
noRedirectHostnames: [],
1516
automaticMode: true,
1617
linkify: false,
1718
dnslinkPolicy: 'best-effort',
@@ -22,7 +23,7 @@ exports.optionDefaults = Object.freeze({
2223
customGatewayUrl: 'http://127.0.0.1:8080',
2324
ipfsApiUrl: 'http://127.0.0.1:5001',
2425
ipfsApiPollMs: 3000,
25-
ipfsProxy: true
26+
ipfsProxy: true // window.ipfs
2627
})
2728

2829
// `storage` should be a browser.storage.local or similar

add-on/src/popup/browser-action/browser-action.css

+11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@
66
background-color: #F4F4F4;
77
}
88

9+
.header-icon:active {
10+
color: #edf0f4;
11+
transform: translateY(4px);
12+
}
13+
.header-icon[disabled],
14+
.header-icon[disabled]:active {
15+
cursor: not-allowed;
16+
pointer-events: none;
17+
transform: none;
18+
}
19+
920
.outline-0--focus:focus {
1021
outline: 0;
1122
}

add-on/src/popup/browser-action/context-actions.js

+55-4
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,30 @@ const html = require('choo/html')
66
const navItem = require('./nav-item')
77
const { contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress } = require('../../lib/context-menus')
88

9-
module.exports = function contextActions ({
9+
// Context Actions are displayed in Browser Action and Page Action (FF only)
10+
function contextActions ({
1011
active,
12+
globalRedirectEnabled,
13+
currentFqdn,
14+
currentTabRedirectOptOut,
1115
ipfsNodeType,
1216
isIpfsContext,
1317
isPinning,
1418
isUnPinning,
1519
isPinned,
1620
isIpfsOnline,
1721
isApiAvailable,
22+
onToggleSiteRedirect,
1823
onCopy,
1924
onPin,
2025
onUnPin
2126
}) {
22-
if (!isIpfsContext) return null
2327
const activePinControls = active && isIpfsOnline && isApiAvailable && !(isPinning || isUnPinning)
24-
return html`
25-
<div class='fade-in pv1'>
28+
const activeSiteRedirectSwitch = active && globalRedirectEnabled && ipfsNodeType === 'external'
29+
30+
const renderIpfsContextItems = () => {
31+
if (!isIpfsContext) return
32+
return html`<div>
2633
${navItem({
2734
text: browser.i18n.getMessage(contextMenuCopyAddressAtPublicGw),
2835
onClick: () => onCopy(contextMenuCopyAddressAtPublicGw)
@@ -50,6 +57,50 @@ module.exports = function contextActions ({
5057
onClick: onUnPin
5158
})
5259
) : null}
60+
</div>
61+
`
62+
}
63+
64+
const renderSiteRedirectToggle = () => {
65+
if (!activeSiteRedirectSwitch) return
66+
const siteRedirectToggleLabel = browser.i18n.getMessage(
67+
globalRedirectEnabled && !currentTabRedirectOptOut
68+
? 'panel_activeTabSiteRedirectDisable'
69+
: 'panel_activeTabSiteRedirectEnable',
70+
currentFqdn
71+
)
72+
return html`
73+
${navItem({
74+
text: siteRedirectToggleLabel,
75+
title: siteRedirectToggleLabel,
76+
addClass: 'truncate',
77+
disabled: !activeSiteRedirectSwitch,
78+
onClick: onToggleSiteRedirect
79+
})}
80+
`
81+
}
82+
83+
return html`
84+
<div class='fade-in pv1'>
85+
${renderIpfsContextItems()}
86+
${renderSiteRedirectToggle()}
5387
</div>
5488
`
5589
}
90+
module.exports.contextActions = contextActions
91+
92+
// "Active Tab" section is displayed in Browser Action only
93+
// if redirect can be toggled or current tab has any IPFS Context Actions
94+
function activeTabActions (state) {
95+
const showActiveTabSection = (state.active && state.globalRedirectEnabled && state.ipfsNodeType === 'external') || state.isIpfsContext
96+
if (!showActiveTabSection) return
97+
return html`
98+
<div class="no-select w-100 outline-0--focus tl ba b--dashed b--navy-muted">
99+
<div class="ph3 pv2 tr charcoal bg-snow-muted truncate tl">
100+
${browser.i18n.getMessage('panel_activeTabSectionHeader')}
101+
</div>
102+
${contextActions(state)}
103+
</div>
104+
`
105+
}
106+
module.exports.activeTabActions = activeTabActions

add-on/src/popup/browser-action/gateway-status.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module.exports = function gatewayStatus ({
2626
swarmPeers,
2727
isIpfsOnline,
2828
ipfsNodeType,
29-
redirectEnabled
29+
globalRedirectEnabled
3030
}) {
3131
const api = ipfsApiUrl && ipfsNodeType === 'embedded' ? 'js-ipfs' : ipfsApiUrl
3232
return html`

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ const browser = require('webextension-polyfill')
55
const html = require('choo/html')
66
const logo = require('../logo')
77
const powerIcon = require('./power-icon')
8+
const redirectIcon = require('./redirect-icon')
89
const optionsIcon = require('./options-icon')
910
const gatewayStatus = require('./gateway-status')
1011

1112
module.exports = function header (props) {
12-
const { ipfsNodeType, active, onToggleActive, onOpenPrefs, isIpfsOnline } = props
13+
const { ipfsNodeType, active, globalRedirectEnabled, onToggleActive, onToggleGlobalRedirect, onOpenPrefs, isIpfsOnline } = props
14+
const showGlobalRedirectSwitch = ipfsNodeType === 'external'
1315
return html`
1416
<div class="pt3 pb1 br2 br--top ba bw1 b--white" style="background-image: url('../../../images/stars.png'), linear-gradient(to bottom, #041727 0%,#043b55 100%); background-size: 100%; background-repeat: repeat;">
1517
<div class="no-user-select">
@@ -29,6 +31,10 @@ module.exports = function header (props) {
2931
title: 'panel_headerActiveToggleTitle',
3032
action: onToggleActive
3133
})}
34+
${showGlobalRedirectSwitch ? redirectIcon({ active: active && globalRedirectEnabled,
35+
title: 'panel_headerRedirectToggleTitle',
36+
action: onToggleGlobalRedirect
37+
}) : null}
3238
${optionsIcon({ active,
3339
title: 'panel_openPreferences',
3440
action: onOpenPrefs

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const browser = require('webextension-polyfill')
66

77
function icon ({ svg, title, active, action }) {
88
return html`
9-
<button class="pa0 ma0 dib bn bg-transparent pointer transition-all ${active ? 'aqua hover-snow' : 'gray hover-snow-muted'}"
9+
<button class="header-icon pa0 ma0 dib bn bg-transparent transition-all ${action ? 'pointer' : null} ${active ? 'aqua' : 'gray'}"
1010
style="outline:none;"
1111
title="${browser.i18n.getMessage(title)}"
1212
onclick=${action}>

add-on/src/popup/browser-action/nav-item.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
const html = require('choo/html')
55

6-
function navItem ({ icon, text, disabled, addClass, onClick }) {
6+
function navItem ({ icon, text, title, disabled, addClass, onClick }) {
77
let className = 'black button-reset db w-100 bg-white b--none outline-0--focus pv2 ph3 f5 tl'
88
if (disabled) {
99
className += ' o-40'
@@ -15,7 +15,7 @@ function navItem ({ icon, text, disabled, addClass, onClick }) {
1515
}
1616

1717
return html`
18-
<button class="${className}" onclick=${disabled ? null : onClick} ${disabled ? 'disabled' : ''}>
18+
<button class="${className}" onclick=${disabled ? null : onClick} title="${title || ''}" ${disabled ? 'disabled' : ''}>
1919
${text}
2020
</button>
2121
`

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

+1-13
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,12 @@ module.exports = function operations ({
99
active,
1010
ipfsNodeType,
1111
isIpfsOnline,
12-
redirectEnabled,
1312
isApiAvailable,
1413
onQuickUpload,
15-
onOpenWebUi,
16-
onToggleRedirect
14+
onOpenWebUi
1715
}) {
1816
const activeQuickUpload = active && isIpfsOnline && isApiAvailable
1917
const activeWebUI = active && isIpfsOnline && ipfsNodeType === 'external'
20-
const activeGatewaySwitch = active && ipfsNodeType === 'external'
2118

2219
return html`
2320
<div class="fade-in pv1">
@@ -27,15 +24,6 @@ module.exports = function operations ({
2724
disabled: !activeQuickUpload,
2825
onClick: onQuickUpload
2926
})}
30-
${navItem({
31-
text: browser.i18n.getMessage(
32-
redirectEnabled && activeGatewaySwitch
33-
? 'panel_switchToPublicGateway'
34-
: 'panel_switchToCustomGateway'
35-
),
36-
disabled: !activeGatewaySwitch,
37-
onClick: onToggleRedirect
38-
})}
3927
${navItem({
4028
text: browser.i18n.getMessage('panel_openWebui'),
4129
disabled: !activeWebUI,

0 commit comments

Comments
 (0)