Skip to content

Commit

Permalink
feat: disable integrations per website (#830)
Browse files Browse the repository at this point in the history
* feat: disable integrations per website

This change replaces per-site redirect toggle with one that disables ALL
IPFS integrations: redirect, content scripts, and access to API via window.ipfs

It also changes the order of menu items to prioritize actions
related to a current tab.

* chore: migrate noRedirectHostnames → noIntegrationsHostnames
  • Loading branch information
lidel authored Dec 12, 2019
1 parent 9730b81 commit ebcc3fa
Show file tree
Hide file tree
Showing 18 changed files with 196 additions and 80 deletions.
26 changes: 13 additions & 13 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@
"message": "Active Tab",
"description": "A menu item in Browser Action pop-up (panel_activeTabSiteRedirectEnable)"
},
"panel_activeTabSiteRedirectToggle": {
"message": "Redirect on $1",
"description": "A menu item in Browser Action pop-up (panel_activeTabSiteRedirectToggle)"
"panel_activeTabSiteIntegrationsToggle": {
"message": "Enable on $1",
"description": "A menu item in Browser Action pop-up (panel_activeTabSiteIntegrationsToggle)"
},
"panel_activeTabSiteRedirectToggleTooltip": {
"message": "Click to toggle gateway redirects on $1",
"description": "A menu item tooltip in Browser Action pop-up (panel_activeTabSiteRedirectToggleTooltip)"
"panel_activeTabSiteIntegrationsToggleTooltip": {
"message": "Click to toggle all IPFS integrations on $1",
"description": "A menu item tooltip in Browser Action pop-up (panel_activeTabSiteIntegrationsToggleTooltip)"
},
"panel_pinCurrentIpfsAddress": {
"message": "Pin IPFS Resource",
Expand Down Expand Up @@ -280,7 +280,7 @@
"description": "An option description on the Preferences screen (option_useCustomGateway_description)"
},
"option_dnslinkRedirect_title": {
"message": "Force page load from custom gateway",
"message": "Load websites from Custom Gateway",
"description": "An option title on the Preferences screen (option_dnslinkRedirect_title)"
},
"option_dnslinkRedirect_description": {
Expand All @@ -296,15 +296,15 @@
"description": "An option description on the Preferences screen (option_dnslinkDataPreload_description)"
},
"option_dnslinkRedirect_warning": {
"message": "Redirecting to a path-based gateway breaks Origin-based security isolation of DNSLink website! Please leave this disabled unless you are aware of (and ok with) related risks.",
"message": "Redirecting to a path-based gateway breaks Origin-based security isolation of DNSLink websites. Make sure you understand related risks.",
"description": "A warning on the Preferences screen, displayed when URL does not belong to Secure Context (option_customGatewayUrl_warning)"
},
"option_noRedirectHostnames_title": {
"message": "Redirect Opt-Outs",
"description": "An option title on the Preferences screen (option_noRedirectHostnames_title)"
"option_noIntegrationsHostnames_title": {
"message": "IPFS Integrations Opt-Outs",
"description": "An option title on the Preferences screen (option_noIntegrationsHostnames_title)"
},
"option_noRedirectHostnames_description": {
"message": "List of websites that should not be redirected to the Custom Gateway (includes subresources from other domains). One hostname per line.",
"option_noIntegrationsHostnames_description": {
"message": "List of websites that should not have any IPFS integrations enabled. One hostname per line.",
"description": "An option description on the Preferences screen (option_noRedirectHostnames_description)"
},
"option_publicGatewayUrl_title": {
Expand Down
9 changes: 5 additions & 4 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ module.exports = async function init () {
openViaWebUI: state.openViaWebUI,
apiURLString: dropSlash(state.apiURLString),
redirect: state.redirect,
noRedirectHostnames: state.noRedirectHostnames,
noIntegrationsHostnames: state.noIntegrationsHostnames,
currentTab: await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0])
}
try {
Expand All @@ -257,7 +257,7 @@ module.exports = async function init () {
info.isIpfsContext = ipfsPathValidator.isIpfsPageActionsContext(url)
info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(url)
info.currentFqdn = info.currentDnslinkFqdn || new URL(url).hostname
info.currentTabRedirectOptOut = info.noRedirectHostnames && info.noRedirectHostnames.includes(info.currentFqdn)
info.currentTabIntegrationsOptOut = info.noIntegrationsHostnames && info.noIntegrationsHostnames.includes(info.currentFqdn)
info.isRedirectContext = info.currentFqdn && ipfsPathValidator.isRedirectPageActionsContext(url)
}
// Still here?
Expand Down Expand Up @@ -365,7 +365,8 @@ module.exports = async function init () {

async function onDOMContentLoaded (details) {
if (!state.active) return // skip content script injection when off
if (!details.url.startsWith('http')) return // skip special pages
if (!details.url || !details.url.startsWith('http')) return // skip empty and special pages
if (!state.activeIntegrations(details.url)) return // skip if opt-out exists
// console.info(`[ipfs-companion] onDOMContentLoaded`, details)
if (state.linkify) {
console.info(`[ipfs-companion] Running linkfy experiment for ${details.url}`)
Expand Down Expand Up @@ -679,7 +680,7 @@ module.exports = async function init () {
case 'detectIpfsPathHeader':
case 'preloadAtPublicGateway':
case 'openViaWebUI':
case 'noRedirectHostnames':
case 'noIntegrationsHostnames':
case 'dnslinkRedirect':
state[key] = change.newValue
break
Expand Down
5 changes: 3 additions & 2 deletions add-on/src/lib/ipfs-proxy/enable-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ const { createProxyAclError } = require('./pre-acl')
function createEnableCommand (getIpfs, getState, getScope, accessControl, requestAccess) {
return async (opts) => {
const scope = await getScope()
const state = getState()
log(`received window.ipfs.enable request from ${scope}`, opts)

// Check if all access to the IPFS node is disabled
if (!getState().ipfsProxy) throw new Error('User disabled access to API proxy in IPFS Companion')
// Check if access to the IPFS node is disabled
if (!state.ipfsProxy || !state.activeIntegrations(scope)) throw new Error('User disabled access to API proxy in IPFS Companion')

// NOOP if .enable() was called without any arguments
if (!opts) return
Expand Down
7 changes: 4 additions & 3 deletions add-on/src/lib/ipfs-proxy/pre-acl.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
// no access decision has been made yet.
function createPreAcl (permission, getState, getScope, accessControl, requestAccess) {
return async (...args) => {
// Check if all access to the IPFS node is disabled
if (!getState().ipfsProxy) {
const scope = await getScope()
const state = getState()
// Check if access to the IPFS node is disabled
if (!state.ipfsProxy || !state.activeIntegrations(scope)) {
throw createProxyAclError(undefined, undefined, 'User disabled access to API proxy in IPFS Companion')
}

const scope = await getScope()
const access = await getAccessWithPrompt(accessControl, requestAccess, scope, permission)

if (!access.allow) {
Expand Down
4 changes: 2 additions & 2 deletions add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ 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
// 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.noRedirectHostnames.some(optout =>
if (state.noIntegrationsHostnames.some(optout =>
fqdn !== 'gateway.ipfs.io' && (fqdn.endsWith(optout) || (parentFqdn && parentFqdn.endsWith(optout))
))) {
ignore(request.requestId)
Expand Down
9 changes: 8 additions & 1 deletion add-on/src/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ exports.optionDefaults = Object.freeze({
publicGatewayUrl: 'https://ipfs.io',
publicSubdomainGatewayUrl: 'https://dweb.link',
useCustomGateway: true,
noRedirectHostnames: [],
noIntegrationsHostnames: [],
automaticMode: true,
linkify: false,
dnslinkPolicy: 'best-effort',
Expand Down Expand Up @@ -132,4 +132,11 @@ exports.migrateOptions = async (storage) => {
ipfsNodeConfig: buildDefaultIpfsNodeConfig()
})
}
// ~ v2.9.x: migrating noRedirectHostnames → noIntegrationsHostnames
// https://github.com/ipfs-shipyard/ipfs-companion/pull/830
const { noRedirectHostnames } = await storage.get('noRedirectHostnames')
if (noRedirectHostnames) {
await storage.set({ noIntegrationsHostnames: noRedirectHostnames })
await storage.remove('noRedirectHostnames')
}
}
14 changes: 13 additions & 1 deletion add-on/src/lib/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const offlinePeerCount = -1
// which should work without setting CORS headers
const webuiCid = 'Qmexhq2sBHnXQbvyP2GfUdbnY7HCagH2Mw5vUNSBn2nxip' // v2.7.2

function initState (options) {
function initState (options, overrides) {
// we store options and some pregenerated values to avoid async storage
// reads and minimize performance impact on overall browsing experience
const state = Object.assign({}, options)
Expand All @@ -31,6 +31,18 @@ function initState (options) {
state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy
state.webuiCid = webuiCid
state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/`
// attach helper functions
state.activeIntegrations = (url) => {
if (!state.active) return false
try {
const fqdn = new URL(url).hostname
return !(state.noIntegrationsHostnames.find(host => fqdn.endsWith(host)))
} catch (_) {
return false
}
}
// apply optional overrides
if (overrides) Object.assign(state, overrides)
return state
}

Expand Down
16 changes: 8 additions & 8 deletions add-on/src/options/forms/gateways-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function gatewaysForm ({
ipfsNodeType,
customGatewayUrl,
useCustomGateway,
noRedirectHostnames,
noIntegrationsHostnames,
publicGatewayUrl,
publicSubdomainGatewayUrl,
onOptionChange
Expand All @@ -23,7 +23,7 @@ function gatewaysForm ({
const onUseCustomGatewayChange = onOptionChange('useCustomGateway')
const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', normalizeGatewayURL)
const onPublicSubdomainGatewayUrlChange = onOptionChange('publicSubdomainGatewayUrl', normalizeGatewayURL)
const onNoRedirectHostnamesChange = onOptionChange('noRedirectHostnames', hostTextToArray)
const onNoIntegrationsHostnamesChange = onOptionChange('noIntegrationsHostnames', hostTextToArray)
const mixedContentWarning = !secureContextUrl.test(customGatewayUrl)
const supportRedirectToCustomGateway = ipfsNodeType !== 'embedded'
const allowChangeOfCustomGateway = ipfsNodeType !== 'embedded:chromesockets'
Expand Down Expand Up @@ -110,18 +110,18 @@ function gatewaysForm ({
` : null}
${supportRedirectToCustomGateway ? html`
<div>
<label for="noRedirectHostnames">
<label for="noIntegrationsHostnames">
<dl>
<dt>${browser.i18n.getMessage('option_noRedirectHostnames_title')}</dt>
<dd>${browser.i18n.getMessage('option_noRedirectHostnames_description')}</dd>
<dt>${browser.i18n.getMessage('option_noIntegrationsHostnames_title')}</dt>
<dd>${browser.i18n.getMessage('option_noIntegrationsHostnames_description')}</dd>
</dl>
</label>
<textarea
id="noRedirectHostnames"
id="noIntegrationsHostnames"
spellcheck="false"
onchange=${onNoRedirectHostnamesChange}
onchange=${onNoIntegrationsHostnamesChange}
rows="4"
>${hostArrayToText(noRedirectHostnames)}</textarea>
>${hostArrayToText(noIntegrationsHostnames)}</textarea>
</div>
` : null}
</fieldset>
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/options/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ module.exports = function optionsPage (state, emit) {
useCustomGateway: state.options.useCustomGateway,
publicGatewayUrl: state.options.publicGatewayUrl,
publicSubdomainGatewayUrl: state.options.publicSubdomainGatewayUrl,
noRedirectHostnames: state.options.noRedirectHostnames,
noIntegrationsHostnames: state.options.noIntegrationsHostnames,
onOptionChange
})}
${fileImportForm({
Expand Down
22 changes: 10 additions & 12 deletions add-on/src/popup/browser-action/context-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ function contextActions ({
currentTab,
currentFqdn,
currentDnslinkFqdn,
currentTabRedirectOptOut,
currentTabIntegrationsOptOut,
ipfsNodeType,
isIpfsContext,
isPinning,
isUnPinning,
isPinned,
isIpfsOnline,
isApiAvailable,
onToggleSiteRedirect,
onToggleSiteIntegrations,
onViewOnGateway,
onCopy,
onPin,
Expand Down Expand Up @@ -69,24 +69,22 @@ function contextActions ({
</div>
`
}
/* TODO: change "redirect on {fqdn}" to "disable on {fqdn}" and disable all integrations
// removed per site toggle for now: ${renderSiteRedirectToggle()}
const renderSiteRedirectToggle = () => {
const renderSiteIntegrationsToggle = () => {
if (!isRedirectContext) return
return html`
${navItem({
text: browser.i18n.getMessage('panel_activeTabSiteRedirectToggle', currentFqdn),
title: browser.i18n.getMessage('panel_activeTabSiteRedirectToggleTooltip', currentFqdn),
text: browser.i18n.getMessage('panel_activeTabSiteIntegrationsToggle', currentFqdn),
title: browser.i18n.getMessage('panel_activeTabSiteIntegrationsToggleTooltip', currentFqdn),
style: 'truncate',
disabled: !(active && redirect),
switchValue: active && redirect && !currentTabRedirectOptOut,
onClick: onToggleSiteRedirect
disabled: !(active),
switchValue: active && !currentTabIntegrationsOptOut,
onClick: onToggleSiteIntegrations
})}
`
}
*/
return html`
<div class='fade-in pv1'>
${renderSiteIntegrationsToggle()}
${renderIpfsContextItems()}
</div>
`
Expand All @@ -101,7 +99,7 @@ function activeTabActions (state) {
return html`
<div>
${navHeader('panel_activeTabSectionHeader')}
<div class="fade-in pv1 bb b--black-10">
<div class="fade-in pv0 bb b--black-10">
${contextActions(state)}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/popup/browser-action/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = function operations ({
}) {
const activeRedirectSwitch = active && ipfsNodeType !== 'embedded'
return html`
<div class="fade-in pv1 bb b--black-10">
<div class="fade-in pb1">
${navItem({
text: browser.i18n.getMessage('panel_redirectToggle'),
title: browser.i18n.getMessage('panel_redirectToggleTooltip'),
Expand Down
6 changes: 3 additions & 3 deletions add-on/src/popup/browser-action/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ module.exports = function browserActionPage (state, emit) {
const onOpenWebUi = () => emit('openWebUi')
const onOpenPrefs = () => emit('openPrefs')
const onToggleGlobalRedirect = () => emit('toggleGlobalRedirect')
const onToggleSiteRedirect = () => emit('toggleSiteRedirect')
const onToggleSiteIntegrations = () => emit('toggleSiteIntegrations')
const onToggleActive = () => emit('toggleActive')

const headerProps = Object.assign({ onToggleActive, onOpenPrefs }, state)
const activeTabActionsProps = Object.assign({ onViewOnGateway, onToggleSiteRedirect, onCopy, onPin, onUnPin }, state)
const activeTabActionsProps = Object.assign({ onViewOnGateway, onToggleSiteIntegrations, onCopy, onPin, onUnPin }, state)
const opsProps = Object.assign({ onQuickImport, onOpenWebUi, onToggleGlobalRedirect }, state)

return html`
<div class="sans-serif" style="text-rendering: optimizeLegibility;">
${header(headerProps)}
${operations(opsProps)}
${activeTabActions(activeTabActionsProps)}
${tools(opsProps)}
${operations(opsProps)}
</div>
`
}
21 changes: 11 additions & 10 deletions add-on/src/popup/browser-action/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = (state, emitter) => {
currentTab: null,
currentFqdn: null,
currentDnslinkFqdn: null,
noRedirectHostnames: []
noIntegrationsHostnames: []
})

let port
Expand Down Expand Up @@ -166,22 +166,23 @@ module.exports = (state, emitter) => {
}
})

emitter.on('toggleSiteRedirect', async () => {
state.currentTabRedirectOptOut = !state.currentTabRedirectOptOut
emitter.on('toggleSiteIntegrations', async () => {
state.currentTabIntegrationsOptOut = !state.currentTabIntegrationsOptOut
emitter.emit('render')

try {
let noRedirectHostnames = state.noRedirectHostnames
let noIntegrationsHostnames = state.noIntegrationsHostnames
// if we are on /ipns/fqdn.tld/ then use hostname from DNSLink
const fqdn = state.currentDnslinkFqdn || state.currentFqdn
if (noRedirectHostnames.includes(fqdn)) {
noRedirectHostnames = noRedirectHostnames.filter(host => !host.endsWith(fqdn))
if (noIntegrationsHostnames.includes(fqdn)) {
noIntegrationsHostnames = noIntegrationsHostnames.filter(host => !host.endsWith(fqdn))
} else {
noRedirectHostnames.push(fqdn)
noIntegrationsHostnames.push(fqdn)
}
// console.dir('toggleSiteRedirect', state)
await browser.storage.local.set({ noRedirectHostnames })
// 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)) {
// No DNSLink, reload URL as-is
Expand All @@ -198,7 +199,7 @@ module.exports = (state, emitter) => {
})
}
} catch (error) {
console.error(`Unable to update redirect state due to ${error}`)
console.error(`Unable to update integrations state due to ${error}`)
emitter.emit('render')
}
})
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/popup/browser-action/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = function tools ({
return html`
<div>
${navHeader('panel_toolsSectionHeader')}
<div class="fade-in pv1 bb b--black-10">
<div class="fade-in pt1">
${navItem({
text: browser.i18n.getMessage('panel_quickImport'),
style: 'b',
Expand Down
4 changes: 2 additions & 2 deletions add-on/src/popup/page-action/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ module.exports = function pageActionPage (state, emit) {
const onCopy = (copyAction) => emit('copy', copyAction)
const onPin = () => emit('pin')
const onUnPin = () => emit('unPin')
const onToggleSiteRedirect = () => emit('toggleSiteRedirect')
const onToggleSiteIntegrations = () => emit('toggleSiteIntegrations')

const contextActionsProps = Object.assign({ onViewOnGateway, onCopy, onPin, onUnPin, onToggleSiteRedirect }, state)
const contextActionsProps = Object.assign({ onViewOnGateway, onCopy, onPin, onUnPin, onToggleSiteIntegrations }, state)

// Instant init: page-action is shown only in ipfsContext
contextActionsProps.isIpfsContext = true
Expand Down
Loading

0 comments on commit ebcc3fa

Please sign in to comment.