Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remember manual opt-ins and opt-outs per site #929

Merged
merged 10 commits into from
Oct 16, 2020
20 changes: 14 additions & 6 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,21 @@
"message": "Do not use if your IPFS node does not support *.ipfs.localhost. Redirecting to a path-based gateway breaks the origin-based security isolation of DNSLink websites, so make sure you understand the related risks.",
"description": "A warning on the Preferences screen, displayed when URL does not belong to Secure Context (option_customGatewayUrl_warning)"
},
"option_noIntegrationsHostnames_title": {
"message": "Site Opt-Out List",
"description": "An option title on the Preferences screen (option_noIntegrationsHostnames_title)"
"option_disabledOn_title": {
"message": "Always Disable IPFS Integrations",
"description": "An option title on the Preferences screen (option_disabledOn_title)"
},
"option_noIntegrationsHostnames_description": {
"message": "Sites in this list (one hostname per line) will have all IPFS integrations disabled.",
"description": "An option description on the Preferences screen (option_noRedirectHostnames_description)"
"option_disabledOn_description": {
"message": "Sites in this list (one hostname per line) will always load with IPFS integrations disabled. Toggling \"Enable on ...\" from your browser's action menu while visiting a site will also add or remove it from this list.",
"description": "An option description on the Preferences screen (option_disabledOn_description)"
},
"option_enabledOn_title": {
"message": "Always Enable IPFS Integrations",
"description": "An option title on the Preferences screen (option_enabledOn_title)"
},
"option_enabledOn_description": {
"message": "Sites in this list (one hostname per line) will always load with IPFS integrations enabled. Toggling \"Enable on ...\" from your browser's action menu while visiting a site will also add or remove it from this list.",
"description": "An option description on the Preferences screen (option_enabledOn_description)"
},
"option_publicGatewayUrl_title": {
"message": "Default Public Gateway",
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 @@ -49,8 +49,8 @@ module.exports = async function init () {

try {
log('init')
await migrateOptions(browser.storage.local)
await storeMissingOptions(await browser.storage.local.get(), optionDefaults, browser.storage.local)
await migrateOptions(browser.storage.local, debug)
const options = await browser.storage.local.get(optionDefaults)
runtime = await createRuntimeChecks(browser)
state = initState(options)
Expand Down Expand Up @@ -258,7 +258,7 @@ module.exports = async function init () {
openViaWebUI: state.openViaWebUI,
apiURLString: dropSlash(state.apiURLString),
redirect: state.redirect,
noIntegrationsHostnames: state.noIntegrationsHostnames,
disabledOn: state.disabledOn,
currentTab
}
try {
Expand All @@ -285,7 +285,7 @@ module.exports = async function init () {
}
info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(url)
info.currentFqdn = info.currentDnslinkFqdn || new URL(url).hostname
info.currentTabIntegrationsOptOut = info.noIntegrationsHostnames && info.noIntegrationsHostnames.includes(info.currentFqdn)
info.currentTabIntegrationsOptOut = info.disabledOn && info.disabledOn.includes(info.currentFqdn)
info.isRedirectContext = info.currentFqdn && ipfsPathValidator.isRedirectPageActionsContext(url)
}
// Still here?
Expand Down Expand Up @@ -697,7 +697,8 @@ module.exports = async function init () {
case 'preloadAtPublicGateway':
case 'openViaWebUI':
case 'useLatestWebUI':
case 'noIntegrationsHostnames':
case 'enabledOn':
case 'disabledOn':
case 'dnslinkRedirect':
state[key] = change.newValue
break
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
if (fqdn.endsWith(optout) || (parentFqdn && parentFqdn.endsWith(optout))) return true
return false
}
if (state.noIntegrationsHostnames.some(triggerOptOut)) {
if (state.disabledOn.some(triggerOptOut)) {
ignore(request.requestId)
}

Expand Down
33 changes: 31 additions & 2 deletions add-on/src/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ exports.optionDefaults = Object.freeze({
publicSubdomainGatewayUrl: 'https://dweb.link',
useCustomGateway: true,
useSubdomains: true,
noIntegrationsHostnames: [],
enabledOn: [], // hostnames with explicit integration opt-in
disabledOn: [], // hostnames with explicit integration opt-out
automaticMode: true,
linkify: false,
dnslinkPolicy: 'best-effort',
Expand Down Expand Up @@ -145,7 +146,10 @@ function localhostNameUrl (url) {
return url.hostname.toLowerCase() === 'localhost'
}

exports.migrateOptions = async (storage) => {
exports.migrateOptions = async (storage, debug) => {
const log = debug('ipfs-companion:migrations')
log.error = debug('ipfs-companion:migrations:error')

// <= v2.4.4
// DNSLINK: convert old on/off 'dnslink' flag to text-based 'dnslinkPolicy'
const { dnslink } = await storage.get('dnslink')
Expand All @@ -157,6 +161,7 @@ exports.migrateOptions = async (storage) => {
})
await storage.remove('dnslink')
}

// ~ v2.8.x + Brave
// Upgrade js-ipfs to js-ipfs + chrome.sockets
const { ipfsNodeType } = await storage.get('ipfsNodeType')
Expand All @@ -167,13 +172,15 @@ 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')
}

// ~v2.11: subdomain proxy at *.ipfs.localhost
// migrate old default 127.0.0.1 to localhost hostname
const { customGatewayUrl: gwUrl } = await storage.get('customGatewayUrl')
Expand All @@ -184,4 +191,26 @@ exports.migrateOptions = async (storage) => {
await storage.set({ customGatewayUrl: newUrl })
}
}

{ // ~v2.15.x: migrating noIntregrationsHostnames → disabledOn
const { disabledOn, noIntegrationsHostnames } = await storage.get(['disabledOn', 'noIntegrationsHostnames'])
if (noIntegrationsHostnames) {
log('migrating noIntregrationsHostnames → disabledOn')
await storage.set({ disabledOn: disabledOn.concat(noIntegrationsHostnames) })
await storage.remove('noIntegrationsHostnames')
}
}

{ // ~v2.15.x: opt-out some hostnames if user does not have excplicit rule already
const { enabledOn, disabledOn } = await storage.get(['enabledOn', 'disabledOn'])
for (const fqdn of [
'proto.school', // https://github.com/ipfs-shipyard/ipfs-companion/issues/921
'app.fleek.co' // https://github.com/ipfs-shipyard/ipfs-companion/pull/929#pullrequestreview-509501401
]) {
if (enabledOn.includes(fqdn) || disabledOn.includes(fqdn)) continue
log(`adding '${fqdn}' to 'disabledOn' list`)
disabledOn.push(fqdn)
await storage.set({ disabledOn })
}
}
}
2 changes: 1 addition & 1 deletion add-on/src/lib/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function initState (options, overrides) {
if (!state.active) return false
try {
const fqdn = new URL(url).hostname
return !(state.noIntegrationsHostnames.find(host => fqdn.endsWith(host)))
return !(state.disabledOn.find(host => fqdn.endsWith(host)))
} catch (_) {
return false
}
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/options/forms/dnslink-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function dnslinkForm ({
</dd>
</dl>
</label>
<select id="dnslinkPolicy" name='dnslinkPolicy' class="self-center-ns bg-white" onchange=${onDnslinkPolicyChange}>
<select id="dnslinkPolicy" name='dnslinkPolicy' class="self-center-ns bg-white navy" onchange=${onDnslinkPolicyChange}>
<option
value='false'
selected=${String(dnslinkPolicy) === 'false'}>
Expand Down
35 changes: 26 additions & 9 deletions add-on/src/options/forms/gateways-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ function gatewaysForm ({
customGatewayUrl,
useCustomGateway,
useSubdomains,
noIntegrationsHostnames,
disabledOn,
enabledOn,
publicGatewayUrl,
publicSubdomainGatewayUrl,
onOptionChange
Expand All @@ -26,7 +27,8 @@ function gatewaysForm ({
const onUseSubdomainProxyChange = onOptionChange('useSubdomains')
const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', guiURLString)
const onPublicSubdomainGatewayUrlChange = onOptionChange('publicSubdomainGatewayUrl', guiURLString)
const onNoIntegrationsHostnamesChange = onOptionChange('noIntegrationsHostnames', hostTextToArray)
const onDisabledOnChange = onOptionChange('disabledOn', hostTextToArray)
const onEnabledOnChange = onOptionChange('enabledOn', hostTextToArray)
const mixedContentWarning = !secureContextUrl.test(customGatewayUrl)
const supportRedirectToCustomGateway = ipfsNodeType !== 'embedded'
const allowChangeOfCustomGateway = ipfsNodeType !== 'embedded:chromesockets'
Expand Down Expand Up @@ -132,19 +134,34 @@ function gatewaysForm ({
` : null}
${supportRedirectToCustomGateway ? html`
<div class="flex-row-ns pb0-ns">
<label for="noIntegrationsHostnames">
<label for="disabledOn">
<dl>
<dt>${browser.i18n.getMessage('option_noIntegrationsHostnames_title')}</dt>
<dd>${browser.i18n.getMessage('option_noIntegrationsHostnames_description')}</dd>
<dt>${browser.i18n.getMessage('option_disabledOn_title')}</dt>
<dd>${browser.i18n.getMessage('option_disabledOn_description')}</dd>
</dl>
</label>
<textarea
class="bg-white navy self-center-ns"
id="noIntegrationsHostnames"
id="disabledOn"
spellcheck="false"
onchange=${onNoIntegrationsHostnamesChange}
rows="1"
>${hostArrayToText(noIntegrationsHostnames)}</textarea>
onchange=${onDisabledOnChange}
rows="${disabledOn.length + 1}"
>${hostArrayToText(disabledOn)}</textarea>
</div>
<div class="flex-row-ns pb0-ns">
<label for="enabledOn">
<dl>
<dt>${browser.i18n.getMessage('option_enabledOn_title')}</dt>
<dd>${browser.i18n.getMessage('option_enabledOn_description')}</dd>
</dl>
</label>
<textarea
class="bg-white navy self-center-ns"
id="enabledOn"
spellcheck="false"
onchange=${onEnabledOnChange}
rows="${enabledOn.length + 1}"
>${hostArrayToText(enabledOn)}</textarea>
</div>
` : null}

Expand Down
9 changes: 7 additions & 2 deletions add-on/src/options/forms/ipfs-node-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
</dd>
</dl>
</label>
<select id="ipfsNodeType" name='ipfsNodeType' class="self-center-ns bg-white" onchange=${onIpfsNodeTypeChange}>
<select id="ipfsNodeType" name='ipfsNodeType' class="self-center-ns bg-white navy" onchange=${onIpfsNodeTypeChange}>
<option
value='external'
selected=${ipfsNodeType === 'external'}>
Expand Down Expand Up @@ -55,7 +55,12 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
<dd>${browser.i18n.getMessage('option_ipfsNodeConfig_description')}</dd>
</dl>
</label>
<textarea id="ipfsNodeConfig" rows="7" onchange=${onIpfsNodeConfigChange}>${ipfsNodeConfig}</textarea>
<textarea
class="bg-white navy self-center-ns"
spellcheck="false"
id="ipfsNodeConfig"
rows="${(ipfsNodeConfig.match(/\n/g) || []).length + 1}"
onchange=${onIpfsNodeConfigChange}>${ipfsNodeConfig}</textarea>
</div>
` : null}
</fieldset>
Expand Down
3 changes: 2 additions & 1 deletion add-on/src/options/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ module.exports = function optionsPage (state, emit) {
useSubdomains: state.options.useSubdomains,
publicGatewayUrl: state.options.publicGatewayUrl,
publicSubdomainGatewayUrl: state.options.publicSubdomainGatewayUrl,
noIntegrationsHostnames: state.options.noIntegrationsHostnames,
disabledOn: state.options.disabledOn,
enabledOn: state.options.enabledOn,
onOptionChange
})}
${fileImportForm({
Expand Down
25 changes: 14 additions & 11 deletions add-on/src/popup/browser-action/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ module.exports = (state, emitter) => {
currentTab: null,
currentFqdn: null,
currentDnslinkFqdn: null,
noIntegrationsHostnames: []
enabledOn: [],
disabledOn: []
})

let port
Expand Down Expand Up @@ -183,30 +184,32 @@ module.exports = (state, emitter) => {
emitter.emit('render')

try {
let noIntegrationsHostnames = state.noIntegrationsHostnames
let { enabledOn, disabledOn, currentTab, currentDnslinkFqdn, currentFqdn } = state
// if we are on /ipns/fqdn.tld/ then use hostname from DNSLink
const fqdn = state.currentDnslinkFqdn || state.currentFqdn
if (noIntegrationsHostnames.includes(fqdn)) {
noIntegrationsHostnames = noIntegrationsHostnames.filter(host => !host.endsWith(fqdn))
const fqdn = currentDnslinkFqdn || currentFqdn
if (disabledOn.includes(fqdn)) {
disabledOn = disabledOn.filter(host => !host.endsWith(fqdn))
enabledOn.push(fqdn)
} else {
noIntegrationsHostnames.push(fqdn)
enabledOn = enabledOn.filter(host => !host.endsWith(fqdn))
disabledOn.push(fqdn)
}
// console.dir('toggleSiteIntegrations', state)
await browser.storage.local.set({ noIntegrationsHostnames })
await browser.storage.local.set({ disabledOn, enabledOn })

// Reload the current tab to apply updated redirect preference
if (!state.currentDnslinkFqdn || !isIPFS.ipnsUrl(state.currentTab.url)) {
if (!currentDnslinkFqdn || !isIPFS.ipnsUrl(currentTab.url)) {
// No DNSLink, reload URL as-is
await browser.tabs.reload(state.currentTab.id)
await browser.tabs.reload(currentTab.id)
} else {
// DNSLinked websites require URL change
// from http?://gateway.tld/ipns/{fqdn}/some/path OR
// from http?://{fqdn}.ipns.gateway.tld/some/path
// to http://{fqdn}/some/path
// (defaulting to http: https websites will have HSTS or a redirect)
const path = ipfsContentPath(state.currentTab.url, { keepURIParams: true })
const path = ipfsContentPath(currentTab.url, { keepURIParams: true })
const originalUrl = path.replace(/^.*\/ipns\//, 'http://')
await browser.tabs.update(state.currentTab.id, {
await browser.tabs.update(currentTab.id, {
// FF only: loadReplace: true,
url: originalUrl
})
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"precommit": "run-s lint:standard",
"prepush": "run-s clean build lint test",
"chromium": "run-s bundle:chromium && web-ext run --no-reload --target chromium",
"firefox": "run-s bundle:firefox && web-ext run --no-reload --url about:debugging",
"firefox": "run-s bundle:firefox && web-ext run --no-reload --url about:debugging --verbose",
"firefox:nightly": "cross-env PATH=\"./firefox:$PATH\" run-s get-firefox-nightly firefox",
"firefox:beta:add": "faauv --update ci/firefox/update.json ",
"get-firefox-nightly": "shx test -e ./firefox/firefox || get-firefox -b nightly -e",
Expand Down
2 changes: 1 addition & 1 deletion test/functional/lib/ipfs-proxy/enable-command.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('lib/ipfs-proxy/enable-command', () => {
it('should throw if ALL IPFS integrations are disabled for requested scope', async () => {
const getState = () => initState(optionDefaults, {
ipfsProxy: true,
noIntegrationsHostnames: ['foo.tld']
disabledOn: ['foo.tld']
})
const accessControl = new AccessControl(new Storage())
const getScope = () => 'https://1.foo.tld/path/'
Expand Down
2 changes: 1 addition & 1 deletion test/functional/lib/ipfs-proxy/pre-acl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('lib/ipfs-proxy/pre-acl', () => {
it('should throw if ALL IPFS integrations are disabled for requested scope', async () => {
const getState = () => initState(optionDefaults, {
ipfsProxy: true,
noIntegrationsHostnames: ['foo.tld']
disabledOn: ['foo.tld']
})
const accessControl = new AccessControl(new Storage())
const getScope = () => 'https://2.foo.tld/bar/buzz/'
Expand Down
8 changes: 4 additions & 4 deletions test/functional/lib/state.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,23 @@ describe('state.js', 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']
state.disabledOn = ['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.disabledOn = ['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']
state.disabledOn = ['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']
state.disabledOn = ['wikipedia.org']
const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna'
expect(state.activeIntegrations(url)).to.equal(false)
})
Expand Down