Skip to content

Commit

Permalink
feat: per-site redirect opt-out
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
lidel committed Feb 20, 2019
1 parent 6578886 commit eb8723a
Show file tree
Hide file tree
Showing 19 changed files with 606 additions and 84 deletions.
24 changes: 18 additions & 6 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
"message": "Global toggle: Suspend all IPFS integrations",
"description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)"
},
"panel_headerRedirectToggleTitle": {
"message": "Global toggle: Stop all gateway redirections",
"description": "A label for an embedded IPFS node (panel_headerActiveToggleTitle)"
},
"panel_statusOffline": {
"message": "offline",
"description": "A label in Node status section of Browser Action pop-up (panel_statusOffline)"
Expand Down Expand Up @@ -47,13 +51,21 @@
"message": "Open Preferences of Browser Extension",
"description": "A menu item in Browser Action pop-up (panel_openPreferences)"
},
"panel_switchToCustomGateway": {
"message": "Switch to Custom Gateway",
"description": "A menu item in Browser Action pop-up (panel_switchToCustomGateway)"
"panel_globalRedirectEnable": {
"message": "Enable All Redirects",
"description": "A menu item in Browser Action pop-up (panel_globalRedirectEnable)"
},
"panel_activeTabSectionHeader": {
"message": "Active Tab",
"description": "A menu item in Browser Action pop-up (panel_activeTabSiteRedirectEnable)"
},
"panel_activeTabSiteRedirectEnable": {
"message": "Restore Redirects on $1",
"description": "A menu item in Browser Action pop-up (panel_activeTabSiteRedirectEnable)"
},
"panel_switchToPublicGateway": {
"message": "Switch to Public Gateway",
"description": "A menu item in Browser Action pop-up (panel_switchToPublicGateway)"
"panel_activeTabSiteRedirectDisable": {
"message": "Disable Redirects on $1",
"description": "A menu item in Browser Action pop-up (panel_activeTabSiteRedirectDisable)"
},
"panel_pinCurrentIpfsAddress": {
"message": "Pin IPFS Resource",
Expand Down
22 changes: 22 additions & 0 deletions add-on/src/lib/dnslink.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,28 @@ module.exports = function createDnslinkResolver (getState) {
}
const fqdn = url.hostname
return `/ipns/${fqdn}${url.pathname}${url.search}${url.hash}`
},

// Test if URL contains a valid DNSLink hostname, FQDN in /ipns/ path
// and return original hostname if present
findDNSLinkHostname (url) {
const { hostname, pathname } = new URL(url)
// check //foo.tld/ipns/<fqdn>
if (IsIpfs.ipnsPath(pathname)) {
// we may have false-positives here, so we do additional checks below
const ipnsRoot = pathname.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
if (dnslinkResolver.readAndCacheDnslink(hostname)) {
// console.log('findDNSLinkHostname ==> found DNSLink for url.hostname', hostname)
return hostname
}
}

}
Expand Down
12 changes: 10 additions & 2 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,17 @@ module.exports = async function init () {

async function sendStatusUpdateToBrowserAction () {
if (!browserActionPort) return
const dropSlash = url => url.replace(/\/$/, '')
const info = {
active: state.active,
ipfsNodeType: state.ipfsNodeType,
peerCount: state.peerCount,
gwURLString: state.gwURLString,
pubGwURLString: state.pubGwURLString,
gwURLString: dropSlash(state.gwURLString),
pubGwURLString: dropSlash(state.pubGwURLString),
webuiRootUrl: state.webuiRootUrl,
apiURLString: dropSlash(state.apiURLString),
redirect: state.redirect,
noRedirectHostnames: state.noRedirectHostnames,
currentTab: await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0])
}
try {
Expand All @@ -232,6 +236,9 @@ module.exports = async function init () {
}
if (info.currentTab) {
info.ipfsPageActionsContext = ipfsPathValidator.isIpfsPageActionsContext(info.currentTab.url)
info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(info.currentTab.url)
info.currentFqdn = info.currentDnslinkFqdn || new URL(info.currentTab.url).hostname
info.currentTabRedirectOptOut = info.noRedirectHostnames && info.noRedirectHostnames.includes(info.currentFqdn)
}
// Still here?
if (browserActionPort) {
Expand Down Expand Up @@ -641,6 +648,7 @@ module.exports = async function init () {
case 'automaticMode':
case 'detectIpfsPathHeader':
case 'preloadAtPublicGateway':
case 'noRedirectHostnames':
state[key] = change.newValue
break
}
Expand Down
2 changes: 2 additions & 0 deletions add-on/src/lib/ipfs-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function createIpfsPathValidator (getState, dnsLink) {
},

// Test if actions such as 'copy URL', 'pin/unpin' should be enabled for the URL
// TODO: include hostname check for DNSLink and display option to copy CID even if no redirect
isIpfsPageActionsContext (url) {
return (IsIpfs.url(url) && !url.startsWith(getState().apiURLString)) || IsIpfs.subdomain(url)
}
Expand Down Expand Up @@ -110,6 +111,7 @@ function validIpnsPath (path, dnsLink) {
// console.log('==> IPNS is a valid CID', ipnsRoot)
return true
}
// then see if there is an DNSLink entry for 'ipnsRoot' hostname
if (dnsLink.readAndCacheDnslink(ipnsRoot)) {
// console.log('==> IPNS for FQDN with valid dnslink: ', ipnsRoot)
return true
Expand Down
16 changes: 16 additions & 0 deletions add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ 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 redirect opt-out exists
const parentUrl = request.originUrl || request.initiator // FF: originUrl, Chrome: initiator
const fqdn = new URL(request.url).hostname
const parentFqdn = parentUrl && request.url !== parentUrl ? new URL(parentUrl).hostname : null
if (state.noRedirectHostnames.some(optout =>
fqdn.endsWith(optout) || (parentFqdn && parentFqdn.endsWith(optout)
))) {
ignore(request.requestId)
}
// additional checks limited to requests for root documents
if (request.type === 'main_frame') {
// trigger DNSLink lookup if status for root domain is not in cache yet
if (state.dnslinkPolicy && dnslinkResolver.canLookupURL(request.url)) {
(async () => dnslinkResolver.readAndCacheDnslink(parentFqdn || fqdn))()
}
}
return isIgnored(request.requestId)
}

Expand Down
3 changes: 2 additions & 1 deletion add-on/src/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exports.optionDefaults = Object.freeze({
}, null, 2),
publicGatewayUrl: 'https://ipfs.io',
useCustomGateway: true,
noRedirectHostnames: [],
automaticMode: true,
linkify: false,
dnslinkPolicy: 'best-effort',
Expand All @@ -22,7 +23,7 @@ exports.optionDefaults = Object.freeze({
customGatewayUrl: 'http://127.0.0.1:8080',
ipfsApiUrl: 'http://127.0.0.1:5001',
ipfsApiPollMs: 3000,
ipfsProxy: true
ipfsProxy: true // window.ipfs
})

// `storage` should be a browser.storage.local or similar
Expand Down
11 changes: 11 additions & 0 deletions add-on/src/popup/browser-action/browser-action.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
background-color: #F4F4F4;
}

.header-icon:active {
color: #edf0f4;
transform: translateY(4px);
}
.header-icon[disabled],
.header-icon[disabled]:active {
cursor: not-allowed;
pointer-events: none;
transform: none;
}

.outline-0--focus:focus {
outline: 0;
}
Expand Down
59 changes: 55 additions & 4 deletions add-on/src/popup/browser-action/context-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,30 @@ const html = require('choo/html')
const navItem = require('./nav-item')
const { contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress } = require('../../lib/context-menus')

module.exports = function contextActions ({
// Context Actions are displayed in Browser Action and Page Action (FF only)
function contextActions ({
active,
globalRedirectEnabled,
currentFqdn,
currentTabRedirectOptOut,
ipfsNodeType,
isIpfsContext,
isPinning,
isUnPinning,
isPinned,
isIpfsOnline,
isApiAvailable,
onToggleSiteRedirect,
onCopy,
onPin,
onUnPin
}) {
if (!isIpfsContext) return null
const activePinControls = active && isIpfsOnline && isApiAvailable && !(isPinning || isUnPinning)
return html`
<div class='fade-in pv1'>
const activeSiteRedirectSwitch = active && globalRedirectEnabled && ipfsNodeType === 'external'

const renderIpfsContextItems = () => {
if (!isIpfsContext) return
return html`<div>
${navItem({
text: browser.i18n.getMessage(contextMenuCopyAddressAtPublicGw),
onClick: () => onCopy(contextMenuCopyAddressAtPublicGw)
Expand Down Expand Up @@ -50,6 +57,50 @@ module.exports = function contextActions ({
onClick: onUnPin
})
) : null}
</div>
`
}

const renderSiteRedirectToggle = () => {
if (!activeSiteRedirectSwitch) return
const siteRedirectToggleLabel = browser.i18n.getMessage(
globalRedirectEnabled && !currentTabRedirectOptOut
? 'panel_activeTabSiteRedirectDisable'
: 'panel_activeTabSiteRedirectEnable',
currentFqdn
)
return html`
${navItem({
text: siteRedirectToggleLabel,
title: siteRedirectToggleLabel,
addClass: 'truncate',
disabled: !activeSiteRedirectSwitch,
onClick: onToggleSiteRedirect
})}
`
}

return html`
<div class='fade-in pv1'>
${renderIpfsContextItems()}
${renderSiteRedirectToggle()}
</div>
`
}
module.exports.contextActions = contextActions

// "Active Tab" section is displayed in Browser Action only
// if redirect can be toggled or current tab has any IPFS Context Actions
function activeTabActions (state) {
const showActiveTabSection = (state.active && state.globalRedirectEnabled && state.ipfsNodeType === 'external') || state.isIpfsContext
if (!showActiveTabSection) return
return html`
<div class="no-select w-100 outline-0--focus tl ba b--dashed b--navy-muted">
<div class="ph3 pv2 tr charcoal bg-snow-muted truncate tl">
${browser.i18n.getMessage('panel_activeTabSectionHeader')}
</div>
${contextActions(state)}
</div>
`
}
module.exports.activeTabActions = activeTabActions
2 changes: 1 addition & 1 deletion add-on/src/popup/browser-action/gateway-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = function gatewayStatus ({
swarmPeers,
isIpfsOnline,
ipfsNodeType,
redirectEnabled
globalRedirectEnabled
}) {
const api = ipfsApiUrl && ipfsNodeType === 'embedded' ? 'js-ipfs' : ipfsApiUrl
return html`
Expand Down
8 changes: 7 additions & 1 deletion add-on/src/popup/browser-action/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ const browser = require('webextension-polyfill')
const html = require('choo/html')
const logo = require('../logo')
const powerIcon = require('./power-icon')
const redirectIcon = require('./redirect-icon')
const optionsIcon = require('./options-icon')
const gatewayStatus = require('./gateway-status')

module.exports = function header (props) {
const { ipfsNodeType, active, onToggleActive, onOpenPrefs, isIpfsOnline } = props
const { ipfsNodeType, active, globalRedirectEnabled, onToggleActive, onToggleGlobalRedirect, onOpenPrefs, isIpfsOnline } = props
const showGlobalRedirectSwitch = ipfsNodeType === 'external'
return html`
<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;">
<div class="no-user-select">
Expand All @@ -29,6 +31,10 @@ module.exports = function header (props) {
title: 'panel_headerActiveToggleTitle',
action: onToggleActive
})}
${showGlobalRedirectSwitch ? redirectIcon({ active: active && globalRedirectEnabled,
title: 'panel_headerRedirectToggleTitle',
action: onToggleGlobalRedirect
}) : null}
${optionsIcon({ active,
title: 'panel_openPreferences',
action: onOpenPrefs
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/popup/browser-action/icon.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const browser = require('webextension-polyfill')

function icon ({ svg, title, active, action }) {
return html`
<button class="pa0 ma0 dib bn bg-transparent pointer transition-all ${active ? 'aqua hover-snow' : 'gray hover-snow-muted'}"
<button class="header-icon pa0 ma0 dib bn bg-transparent transition-all ${action ? 'pointer' : null} ${active ? 'aqua' : 'gray'}"
style="outline:none;"
title="${browser.i18n.getMessage(title)}"
onclick=${action}>
Expand Down
4 changes: 2 additions & 2 deletions add-on/src/popup/browser-action/nav-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

const html = require('choo/html')

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

return html`
<button class="${className}" onclick=${disabled ? null : onClick} ${disabled ? 'disabled' : ''}>
<button class="${className}" onclick=${disabled ? null : onClick} title="${title || ''}" ${disabled ? 'disabled' : ''}>
${text}
</button>
`
Expand Down
14 changes: 1 addition & 13 deletions add-on/src/popup/browser-action/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,12 @@ module.exports = function operations ({
active,
ipfsNodeType,
isIpfsOnline,
redirectEnabled,
isApiAvailable,
onQuickUpload,
onOpenWebUi,
onToggleRedirect
onOpenWebUi
}) {
const activeQuickUpload = active && isIpfsOnline && isApiAvailable
const activeWebUI = active && isIpfsOnline && ipfsNodeType === 'external'
const activeGatewaySwitch = active && ipfsNodeType === 'external'

return html`
<div class="fade-in pv1">
Expand All @@ -27,15 +24,6 @@ module.exports = function operations ({
disabled: !activeQuickUpload,
onClick: onQuickUpload
})}
${navItem({
text: browser.i18n.getMessage(
redirectEnabled && activeGatewaySwitch
? 'panel_switchToPublicGateway'
: 'panel_switchToCustomGateway'
),
disabled: !activeGatewaySwitch,
onClick: onToggleRedirect
})}
${navItem({
text: browser.i18n.getMessage('panel_openWebui'),
disabled: !activeWebUI,
Expand Down
Loading

0 comments on commit eb8723a

Please sign in to comment.