From ebcc3fa3b4cec664e0deb209cefc3d7a40d9771b Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 12 Dec 2019 18:13:38 +0100 Subject: [PATCH] feat: disable integrations per website (#830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- add-on/_locales/en/messages.json | 26 +++++----- add-on/src/lib/ipfs-companion.js | 9 ++-- add-on/src/lib/ipfs-proxy/enable-command.js | 5 +- add-on/src/lib/ipfs-proxy/pre-acl.js | 7 +-- add-on/src/lib/ipfs-request.js | 4 +- add-on/src/lib/options.js | 9 +++- add-on/src/lib/state.js | 14 +++++- add-on/src/options/forms/gateways-form.js | 16 +++---- add-on/src/options/page.js | 2 +- .../popup/browser-action/context-actions.js | 22 ++++----- add-on/src/popup/browser-action/operations.js | 2 +- add-on/src/popup/browser-action/page.js | 6 +-- add-on/src/popup/browser-action/store.js | 21 +++++---- add-on/src/popup/browser-action/tools.js | 2 +- add-on/src/popup/page-action/page.js | 4 +- .../lib/ipfs-proxy/enable-command.test.js | 47 +++++++++++++++---- .../functional/lib/ipfs-proxy/pre-acl.test.js | 38 ++++++++++++--- test/functional/lib/state.test.js | 42 +++++++++++++++++ 18 files changed, 196 insertions(+), 80 deletions(-) create mode 100644 test/functional/lib/state.test.js diff --git a/add-on/_locales/en/messages.json b/add-on/_locales/en/messages.json index a923b424a..d65ab0eb9 100644 --- a/add-on/_locales/en/messages.json +++ b/add-on/_locales/en/messages.json @@ -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", @@ -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": { @@ -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": { diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index 4c49cccce..63aa50be5 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -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 { @@ -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? @@ -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}`) @@ -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 diff --git a/add-on/src/lib/ipfs-proxy/enable-command.js b/add-on/src/lib/ipfs-proxy/enable-command.js index 23d7a4605..ff87c0074 100644 --- a/add-on/src/lib/ipfs-proxy/enable-command.js +++ b/add-on/src/lib/ipfs-proxy/enable-command.js @@ -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 diff --git a/add-on/src/lib/ipfs-proxy/pre-acl.js b/add-on/src/lib/ipfs-proxy/pre-acl.js index da9bac61b..d99d14628 100644 --- a/add-on/src/lib/ipfs-proxy/pre-acl.js +++ b/add-on/src/lib/ipfs-proxy/pre-acl.js @@ -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) { diff --git a/add-on/src/lib/ipfs-request.js b/add-on/src/lib/ipfs-request.js index d0e870ef2..8901ad93b 100644 --- a/add-on/src/lib/ipfs-request.js +++ b/add-on/src/lib/ipfs-request.js @@ -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) diff --git a/add-on/src/lib/options.js b/add-on/src/lib/options.js index e747b5065..dc8ee2b00 100644 --- a/add-on/src/lib/options.js +++ b/add-on/src/lib/options.js @@ -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', @@ -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') + } } diff --git a/add-on/src/lib/state.js b/add-on/src/lib/state.js index c422f17bd..59d703305 100644 --- a/add-on/src/lib/state.js +++ b/add-on/src/lib/state.js @@ -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) @@ -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 } diff --git a/add-on/src/options/forms/gateways-form.js b/add-on/src/options/forms/gateways-form.js index 09e9f3d9d..1d6133f94 100644 --- a/add-on/src/options/forms/gateways-form.js +++ b/add-on/src/options/forms/gateways-form.js @@ -14,7 +14,7 @@ function gatewaysForm ({ ipfsNodeType, customGatewayUrl, useCustomGateway, - noRedirectHostnames, + noIntegrationsHostnames, publicGatewayUrl, publicSubdomainGatewayUrl, onOptionChange @@ -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' @@ -110,18 +110,18 @@ function gatewaysForm ({ ` : null} ${supportRedirectToCustomGateway ? html`
-
` : null} diff --git a/add-on/src/options/page.js b/add-on/src/options/page.js index a3965a1b0..c616327f9 100644 --- a/add-on/src/options/page.js +++ b/add-on/src/options/page.js @@ -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({ diff --git a/add-on/src/popup/browser-action/context-actions.js b/add-on/src/popup/browser-action/context-actions.js index f497a4574..3af22fdad 100644 --- a/add-on/src/popup/browser-action/context-actions.js +++ b/add-on/src/popup/browser-action/context-actions.js @@ -22,7 +22,7 @@ function contextActions ({ currentTab, currentFqdn, currentDnslinkFqdn, - currentTabRedirectOptOut, + currentTabIntegrationsOptOut, ipfsNodeType, isIpfsContext, isPinning, @@ -30,7 +30,7 @@ function contextActions ({ isPinned, isIpfsOnline, isApiAvailable, - onToggleSiteRedirect, + onToggleSiteIntegrations, onViewOnGateway, onCopy, onPin, @@ -69,24 +69,22 @@ function contextActions ({ ` } - /* 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`
+ ${renderSiteIntegrationsToggle()} ${renderIpfsContextItems()}
` @@ -101,7 +99,7 @@ function activeTabActions (state) { return html`
${navHeader('panel_activeTabSectionHeader')} -
+
${contextActions(state)}
diff --git a/add-on/src/popup/browser-action/operations.js b/add-on/src/popup/browser-action/operations.js index 9df83f0db..15761c034 100644 --- a/add-on/src/popup/browser-action/operations.js +++ b/add-on/src/popup/browser-action/operations.js @@ -13,7 +13,7 @@ module.exports = function operations ({ }) { const activeRedirectSwitch = active && ipfsNodeType !== 'embedded' return html` -
+
${navItem({ text: browser.i18n.getMessage('panel_redirectToggle'), title: browser.i18n.getMessage('panel_redirectToggleTooltip'), diff --git a/add-on/src/popup/browser-action/page.js b/add-on/src/popup/browser-action/page.js index e17a10339..81524a710 100644 --- a/add-on/src/popup/browser-action/page.js +++ b/add-on/src/popup/browser-action/page.js @@ -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`
${header(headerProps)} - ${operations(opsProps)} ${activeTabActions(activeTabActionsProps)} ${tools(opsProps)} + ${operations(opsProps)}
` } diff --git a/add-on/src/popup/browser-action/store.js b/add-on/src/popup/browser-action/store.js index c6999cc71..e1ae791f6 100644 --- a/add-on/src/popup/browser-action/store.js +++ b/add-on/src/popup/browser-action/store.js @@ -32,7 +32,7 @@ module.exports = (state, emitter) => { currentTab: null, currentFqdn: null, currentDnslinkFqdn: null, - noRedirectHostnames: [] + noIntegrationsHostnames: [] }) let port @@ -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 @@ -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') } }) diff --git a/add-on/src/popup/browser-action/tools.js b/add-on/src/popup/browser-action/tools.js index 03d2ccd0e..2322a958e 100644 --- a/add-on/src/popup/browser-action/tools.js +++ b/add-on/src/popup/browser-action/tools.js @@ -20,7 +20,7 @@ module.exports = function tools ({ return html`
${navHeader('panel_toolsSectionHeader')} -
+
${navItem({ text: browser.i18n.getMessage('panel_quickImport'), style: 'b', diff --git a/add-on/src/popup/page-action/page.js b/add-on/src/popup/page-action/page.js index 1b672e143..4c668dae2 100644 --- a/add-on/src/popup/page-action/page.js +++ b/add-on/src/popup/page-action/page.js @@ -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 diff --git a/test/functional/lib/ipfs-proxy/enable-command.test.js b/test/functional/lib/ipfs-proxy/enable-command.test.js index 9ac50241a..76137e336 100644 --- a/test/functional/lib/ipfs-proxy/enable-command.test.js +++ b/test/functional/lib/ipfs-proxy/enable-command.test.js @@ -10,6 +10,8 @@ const Sinon = require('sinon') const AccessControl = require('../../../../add-on/src/lib/ipfs-proxy/access-control') const createEnableCommand = require('../../../../add-on/src/lib/ipfs-proxy/enable-command') const createRequestAccess = require('../../../../add-on/src/lib/ipfs-proxy/request-access') +const { initState } = require('../../../../add-on/src/lib/state') +const { optionDefaults } = require('../../../../add-on/src/lib/options') describe('lib/ipfs-proxy/enable-command', () => { before(() => { @@ -18,7 +20,32 @@ describe('lib/ipfs-proxy/enable-command', () => { }) it('should throw if proxy access is disabled globally', async () => { - const getState = () => ({ ipfsProxy: false }) + const getState = () => initState(optionDefaults, { ipfsProxy: false }) + const accessControl = new AccessControl(new Storage()) + const getScope = () => 'https://1.foo.tld/path/' + const getIpfs = () => {} + const requestAccess = createRequestAccess(browser, screen) + const enable = createEnableCommand(getIpfs, getState, getScope, accessControl, requestAccess) + const permissions = { commands: ['files.mkdir', 'id', 'version'] } + + let error + + try { + await enable(permissions) + } catch (err) { + error = err + } + + expect(() => { if (error) throw error }).to.throw('User disabled access to API proxy in IPFS Companion') + expect(error.scope).to.equal(undefined) + expect(error.permissions).to.be.equal(undefined) + }) + + it('should throw if ALL IPFS integrations are disabled for requested scope', async () => { + const getState = () => initState(optionDefaults, { + ipfsProxy: true, + noIntegrationsHostnames: ['foo.tld'] + }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://1.foo.tld/path/' const getIpfs = () => {} @@ -40,7 +67,7 @@ describe('lib/ipfs-proxy/enable-command', () => { }) it('should throw if access to unknown command is requested', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://2.foo.tld/path/' const getIpfs = () => {} @@ -59,7 +86,7 @@ describe('lib/ipfs-proxy/enable-command', () => { }) it('should return without prompt if called without any arguments', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://3.foo.tld/path/' const getIpfs = () => {} @@ -73,7 +100,7 @@ describe('lib/ipfs-proxy/enable-command', () => { }) it('should request access if no grant exists', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://4.foo.tld/path/' const getIpfs = () => {} @@ -89,7 +116,7 @@ describe('lib/ipfs-proxy/enable-command', () => { }) it('should request access if partial grant exists', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://4.foo.tld/path/' const getIpfs = () => {} @@ -110,7 +137,7 @@ describe('lib/ipfs-proxy/enable-command', () => { }) it('should deny access if any partial deny already exists', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://4.foo.tld/path/' const getIpfs = () => {} @@ -141,7 +168,7 @@ describe('lib/ipfs-proxy/enable-command', () => { }) it('should deny access when user denies request', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://5.foo.tld/path/' const getIpfs = () => {} @@ -162,7 +189,7 @@ describe('lib/ipfs-proxy/enable-command', () => { }) it('should not re-request if denied', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://6.foo.tld/path/' const getIpfs = () => {} @@ -195,7 +222,7 @@ describe('lib/ipfs-proxy/enable-command', () => { }) it('should have a well-formed Error if denied', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://7.foo.tld/path/' const getIpfs = () => {} @@ -222,7 +249,7 @@ describe('lib/ipfs-proxy/enable-command', () => { }) it('should not re-request if allowed', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://8.foo.tld/path/' const getIpfs = () => {} diff --git a/test/functional/lib/ipfs-proxy/pre-acl.test.js b/test/functional/lib/ipfs-proxy/pre-acl.test.js index 658cb6e7b..c9134149f 100644 --- a/test/functional/lib/ipfs-proxy/pre-acl.test.js +++ b/test/functional/lib/ipfs-proxy/pre-acl.test.js @@ -6,6 +6,8 @@ const Storage = require('mem-storage-area/Storage') const Sinon = require('sinon') const AccessControl = require('../../../../add-on/src/lib/ipfs-proxy/access-control') const { createPreAcl } = require('../../../../add-on/src/lib/ipfs-proxy/pre-acl') +const { initState } = require('../../../../add-on/src/lib/state') +const { optionDefaults } = require('../../../../add-on/src/lib/options') describe('lib/ipfs-proxy/pre-acl', () => { before(() => { @@ -13,7 +15,7 @@ describe('lib/ipfs-proxy/pre-acl', () => { }) it('should throw if access is disabled', async () => { - const getState = () => ({ ipfsProxy: false }) + const getState = () => initState(optionDefaults, { ipfsProxy: false }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://ipfs.io/' const permission = 'files.add' @@ -33,8 +35,32 @@ describe('lib/ipfs-proxy/pre-acl', () => { expect(error.permissions).to.be.equal(undefined) }) + it('should throw if ALL IPFS integrations are disabled for requested scope', async () => { + const getState = () => initState(optionDefaults, { + ipfsProxy: true, + noIntegrationsHostnames: ['foo.tld'] + }) + const accessControl = new AccessControl(new Storage()) + const getScope = () => 'https://2.foo.tld/bar/buzz/' + const permission = 'files.add' + + const preAcl = createPreAcl(permission, getState, getScope, accessControl) + + let error + + try { + await preAcl() + } catch (err) { + error = err + } + + expect(() => { if (error) throw error }).to.throw('User disabled access to API proxy in IPFS Companion') + expect(error.scope).to.equal(undefined) + expect(error.permissions).to.be.equal(undefined) + }) + it('should request access if no grant exists', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://ipfs.io/' const permission = 'files.add' @@ -47,7 +73,7 @@ describe('lib/ipfs-proxy/pre-acl', () => { }) it('should deny access when user denies request', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://ipfs.io/' const permission = 'files.add' @@ -67,7 +93,7 @@ describe('lib/ipfs-proxy/pre-acl', () => { }) it('should not re-request if denied', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://ipfs.io/' const permission = 'files.add' @@ -99,7 +125,7 @@ describe('lib/ipfs-proxy/pre-acl', () => { }) it('should have a well-formed Error if denied', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://ipfs.io/' const permission = 'files.add' @@ -124,7 +150,7 @@ describe('lib/ipfs-proxy/pre-acl', () => { }) it('should not re-request if allowed', async () => { - const getState = () => ({ ipfsProxy: true }) + const getState = () => initState(optionDefaults, { ipfsProxy: true }) const accessControl = new AccessControl(new Storage()) const getScope = () => 'https://ipfs.io/' const permission = 'files.add' diff --git a/test/functional/lib/state.test.js b/test/functional/lib/state.test.js new file mode 100644 index 000000000..dea2b55e0 --- /dev/null +++ b/test/functional/lib/state.test.js @@ -0,0 +1,42 @@ +'use strict' +const { describe, it, beforeEach } = require('mocha') +const { expect } = require('chai') +const { URL } = require('url') +const { initState } = require('../../../add-on/src/lib/state') +const { optionDefaults } = require('../../../add-on/src/lib/options') + +describe('state.js', function () { + let state + + beforeEach(function () { + global.URL = URL + state = Object.assign(initState(optionDefaults), { peerCount: 1 }) + }) + + describe('activeIntegrations(url)', function () { + it('should return false if input is undefined', async function () { + expect(state.activeIntegrations(undefined)).to.equal(false) + }) + it('should return true if host is not on the opt-out list', async function () { + state.noIntegrationsHostnames = ['pl.wikipedia.org'] + const url = 'https://en.wikipedia.org/wiki/Main_Page' + expect(state.activeIntegrations(url)).to.equal(true) + }) + it('should return false if host is not on the opt-out list but global toggle is off', async function () { + state.noIntegrationsHostnames = ['pl.wikipedia.org'] + state.active = false + const url = 'https://en.wikipedia.org/wiki/Main_Page' + expect(state.activeIntegrations(url)).to.equal(false) + }) + it('should return false if host is on the opt-out list', async function () { + state.noIntegrationsHostnames = ['example.com', 'pl.wikipedia.org'] + const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna' + expect(state.activeIntegrations(url)).to.equal(false) + }) + it('should return false if parent host of a subdomain is on the opt-out list', async function () { + state.noIntegrationsHostnames = ['wikipedia.org'] + const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna' + expect(state.activeIntegrations(url)).to.equal(false) + }) + }) +})