From 8486e8e05669eccbe73fce224dd4c79c3fdfa74e Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Sat, 19 Feb 2022 03:37:07 +0100 Subject: [PATCH] refactor: necromancy to restore build - removed yarn and all the hackery around transitive dependency overrides - bumped all dependencies - switched to ipfs-core for less deps - removed unused deps - Firefox manifest fix to pass latest webext lint - removed remaining window.ipfs code that was pulling dead dependencies (https://github.com/ipfs/in-web-browsers/issues/172) TODO: - fix tests - docker builds - browserAction icon --- .gitignore | 2 +- Dockerfile | 2 +- add-on/_locales/en/messages.json | 12 - add-on/manifest.firefox.json | 2 +- .../src/contentScripts/ipfs-proxy/content.js | 51 - .../ipfs-proxy/inject-script.js | 15 - add-on/src/contentScripts/ipfs-proxy/page.js | 48 - add-on/src/lib/ipfs-client/embedded.js | 2 +- add-on/src/lib/ipfs-client/external.js | 4 +- add-on/src/lib/ipfs-companion.js | 61 - add-on/src/lib/ipfs-proxy/access-control.js | 207 - .../src/lib/ipfs-proxy/command-whitelist.json | 23 - add-on/src/lib/ipfs-proxy/enable-command.js | 57 - add-on/src/lib/ipfs-proxy/index.js | 83 - add-on/src/lib/ipfs-proxy/pre-acl.js | 61 - add-on/src/lib/ipfs-proxy/pre-command.js | 62 - add-on/src/lib/ipfs-proxy/pre-mfs-scope.js | 152 - add-on/src/lib/ipfs-proxy/request-access.js | 117 - add-on/src/lib/options.js | 1 - add-on/src/options/forms/experiments-form.js | 26 - add-on/src/options/page.js | 1 - add-on/src/pages/proxy-acl/index.html | 18 - add-on/src/pages/proxy-acl/index.js | 15 - add-on/src/pages/proxy-acl/page.js | 119 - add-on/src/pages/proxy-acl/proxy-acl.css | 14 - add-on/src/pages/proxy-acl/store.js | 40 - package-lock.json | 35942 ++++++++++++++++ package.json | 97 +- .../lib/ipfs-proxy/access-control.test.js | 796 - .../lib/ipfs-proxy/enable-command.test.js | 271 - .../functional/lib/ipfs-proxy/pre-acl.test.js | 170 - .../lib/ipfs-proxy/pre-command.test.js | 68 - .../lib/ipfs-proxy/pre-mfs-scope.test.js | 293 - test/functional/pages/proxy-acl/page.test.js | 105 - test/functional/pages/proxy-acl/store.test.js | 324 - webpack.config.js | 20 +- yarn.lock | 14023 ------ 37 files changed, 36008 insertions(+), 17296 deletions(-) delete mode 100644 add-on/src/contentScripts/ipfs-proxy/content.js delete mode 100644 add-on/src/contentScripts/ipfs-proxy/inject-script.js delete mode 100644 add-on/src/contentScripts/ipfs-proxy/page.js delete mode 100644 add-on/src/lib/ipfs-proxy/access-control.js delete mode 100644 add-on/src/lib/ipfs-proxy/command-whitelist.json delete mode 100644 add-on/src/lib/ipfs-proxy/enable-command.js delete mode 100644 add-on/src/lib/ipfs-proxy/index.js delete mode 100644 add-on/src/lib/ipfs-proxy/pre-acl.js delete mode 100644 add-on/src/lib/ipfs-proxy/pre-command.js delete mode 100644 add-on/src/lib/ipfs-proxy/pre-mfs-scope.js delete mode 100644 add-on/src/lib/ipfs-proxy/request-access.js delete mode 100644 add-on/src/pages/proxy-acl/index.html delete mode 100644 add-on/src/pages/proxy-acl/index.js delete mode 100644 add-on/src/pages/proxy-acl/page.js delete mode 100644 add-on/src/pages/proxy-acl/proxy-acl.css delete mode 100644 add-on/src/pages/proxy-acl/store.js create mode 100644 package-lock.json delete mode 100644 test/functional/lib/ipfs-proxy/access-control.test.js delete mode 100644 test/functional/lib/ipfs-proxy/enable-command.test.js delete mode 100644 test/functional/lib/ipfs-proxy/pre-acl.test.js delete mode 100644 test/functional/lib/ipfs-proxy/pre-command.test.js delete mode 100644 test/functional/lib/ipfs-proxy/pre-mfs-scope.test.js delete mode 100644 test/functional/pages/proxy-acl/page.test.js delete mode 100644 test/functional/pages/proxy-acl/store.test.js delete mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index 5a8067eff..895dd6ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ /vendor /node_modules -/package-lock.json +/yarn.lock /firefox /cache /build diff --git a/Dockerfile b/Dockerfile index 9de285a7a..91b50d1c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:14.15.4 +FROM node:16.13.1 ARG USER_ID ARG GROUP_ID diff --git a/add-on/_locales/en/messages.json b/add-on/_locales/en/messages.json index fe5c26c76..25be0113c 100644 --- a/add-on/_locales/en/messages.json +++ b/add-on/_locales/en/messages.json @@ -515,18 +515,6 @@ "message": "Load items with x-ipfs-path headers over IPFS (instead of HTTP) if the header value is an IPFS path. To redirect IPNS paths as well, enable DNSLink support.", "description": "An option description on the Preferences screen (option_detectIpfsPathHeader_description)" }, - "option_ipfsProxy_title": { - "message": "Support for window.ipfs", - "description": "An option title for enabling/disabling the IPFS proxy (option_ipfsProxy_title)" - }, - "option_ipfsProxy_description": { - "message": "IPFS is added to the window object on every page. Enable/disable access to the functions it exposes.", - "description": "An option description for the IPFS proxy (option_ipfsProxy_description)" - }, - "option_ipfsProxy_link_manage_permissions": { - "message": "Manage permissions", - "description": "Link text for managing permissions" - }, "option_openViaWebUI_title": { "message": "Open Imported Files in the Files Screen", "description": "An option title on the Preferences screen (option_openViaWebUI_title)" diff --git a/add-on/manifest.firefox.json b/add-on/manifest.firefox.json index 2cdaa8473..5e0b44a01 100644 --- a/add-on/manifest.firefox.json +++ b/add-on/manifest.firefox.json @@ -8,7 +8,7 @@ "applications": { "gecko": { "id": "ipfs-firefox-addon@lidel.org", - "strict_min_version": "68.0" + "strict_min_version": "91.1.0" } }, "permissions": [ diff --git a/add-on/src/contentScripts/ipfs-proxy/content.js b/add-on/src/contentScripts/ipfs-proxy/content.js deleted file mode 100644 index d43f0b5ee..000000000 --- a/add-on/src/contentScripts/ipfs-proxy/content.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict' - -import rawCode from './../../../dist/bundles/ipfsProxyContentScriptPayload.bundle.js' - -const browser = require('webextension-polyfill') -const injectScript = require('./inject-script') - -function init () { - const port = browser.runtime.connect({ name: 'ipfs-proxy' }) - // Forward on messages from background to the page and vice versa - port.onMessage.addListener((data) => { - if (data && data.sender && data.sender.startsWith('postmsg-rpc/')) { - window.postMessage(data, '*') - } - }) - window.addEventListener('message', (msg) => { - if (msg.data && msg.data.sender && msg.data.sender.startsWith('postmsg-rpc/')) { - port.postMessage(msg.data) - } - }) - - injectScript(rawCode) -} - -function injectIpfsProxy () { - // Skip if proxy is already present - if (window.ipfs) { - return false - } - // Restricting window.ipfs to Secure Context - // See: https://github.com/ipfs-shipyard/ipfs-companion/issues/476 - if (!window.isSecureContext) { - return false - } - // Skip if not in HTML context - // Check 1/2 - const doctype = window.document.doctype - if (doctype && doctype.name !== 'html') { - return false - } - // Check 2/2 - if (document.documentElement.nodeName !== 'HTML') { - return false - } - // Should be ok by now - return true -} - -if (injectIpfsProxy()) { - init() -} diff --git a/add-on/src/contentScripts/ipfs-proxy/inject-script.js b/add-on/src/contentScripts/ipfs-proxy/inject-script.js deleted file mode 100644 index 8c1a45a63..000000000 --- a/add-on/src/contentScripts/ipfs-proxy/inject-script.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict' - -function injectScript (code, opts) { - opts = opts || {} - const doc = opts.document || document - - const scriptTag = document.createElement('script') - scriptTag.innerHTML = code - - const target = opts.target || doc.head || doc.documentElement - target.appendChild(scriptTag) - scriptTag.parentNode.removeChild(scriptTag) -} - -module.exports = injectScript diff --git a/add-on/src/contentScripts/ipfs-proxy/page.js b/add-on/src/contentScripts/ipfs-proxy/page.js deleted file mode 100644 index 89fd9733e..000000000 --- a/add-on/src/contentScripts/ipfs-proxy/page.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict' - -const { assign, freeze } = Object - -// TODO: (wip) this should not be injected by default into every page, -// instead should be lazy-loaded when .enable() method is called for the first time -const { createProxyClient } = require('ipfs-postmsg-proxy') - -function createEnableCommand (proxyClient) { - return { - enable: async (opts) => { - // Send message to proxy server for additional validation - // eg. trigger user prompt if a list of requested capabilities is not empty - // or fail fast and throw if IPFS Proxy is disabled globally - await require('postmsg-rpc').call('proxy.enable', opts) - // Create client - const proxyClient = createProxyClient() - // Additional client-side features - if (opts && opts.experiments) { - if (opts.experiments.ipfsx) { - // Old experiment where we wrapped API with https://github.com/alanshaw/ipfsx - throw new Error('ipfsx no longer supported, use modern JS instead: https://blog.ipfs.io/2020-02-01-async-await-refactor/') - } - } - return freeze(proxyClient) - } - } -} - -function createWindowIpfs () { - const proxyClient = createProxyClient() - - // Add deprecation warning to window.ipfs. - for (const cmd in proxyClient) { - const fn = proxyClient[cmd] - proxyClient[cmd] = function () { - console.warn('Calling commands directly on window.ipfs is deprecated and will be removed in the future. To future-proof your app use API instance returned by window.ipfs.enable() instead. Current best practices can be found at: https://github.com/ipfs-shipyard/ipfs-companion/blob/master/docs/window.ipfs.md') - return fn.apply(this, arguments) - } - } - - // TODO: return thin object with lazy-init inside of window.ipfs.enable - assign(proxyClient, createEnableCommand()) - - return freeze(proxyClient) -} - -window.ipfs = window.ipfs || createWindowIpfs() diff --git a/add-on/src/lib/ipfs-client/embedded.js b/add-on/src/lib/ipfs-client/embedded.js index b8b45caf3..b85403c98 100644 --- a/add-on/src/lib/ipfs-client/embedded.js +++ b/add-on/src/lib/ipfs-client/embedded.js @@ -5,7 +5,7 @@ const log = debug('ipfs-companion:client:embedded') log.error = debug('ipfs-companion:client:embedded:error') const mergeOptions = require('merge-options') -const Ipfs = require('ipfs') +const Ipfs = require('ipfs-core') const { optionDefaults } = require('../options') let node = null diff --git a/add-on/src/lib/ipfs-client/external.js b/add-on/src/lib/ipfs-client/external.js index 7571cba34..75baf5355 100644 --- a/add-on/src/lib/ipfs-client/external.js +++ b/add-on/src/lib/ipfs-client/external.js @@ -5,13 +5,13 @@ const debug = require('debug') const log = debug('ipfs-companion:client:external') log.error = debug('ipfs-companion:client:external:error') -const httpClient = require('ipfs-http-client') +const { create } = require('ipfs-http-client') exports.init = async function (browser, opts) { log(`init with IPFS API at ${opts.apiURLString}`) const clientConfig = opts.apiURLString // https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-http-client#importing-the-module-and-usage - const api = httpClient(clientConfig) + const api = await create(clientConfig) return api } diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index cf7a9db9e..ea0a999b1 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -23,7 +23,6 @@ const createCopier = require('./copier') const createInspector = require('./inspector') const { createRuntimeChecks } = require('./runtime-checks') const { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuViewOnGateway, contextMenuCopyPermalink, contextMenuCopyCidAddress } = require('./context-menus') -const createIpfsProxy = require('./ipfs-proxy') const { registerSubdomainProxy } = require('./http-proxy') const { runPendingOnInstallTasks } = require('./on-installed') @@ -44,8 +43,6 @@ module.exports = async function init () { let runtime let contextMenus let apiStatusUpdateInterval - let ipfsProxy - // TODO: window.ipfs var ipfsProxyContentScript let ipfsImportHandler const idleInSecs = 5 * 60 const browserActionPortName = 'browser-action-port' @@ -84,8 +81,6 @@ module.exports = async function init () { onCopyAddressAtPublicGw: copier.copyAddressAtPublicGw }) modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime) - ipfsProxy = createIpfsProxy(getIpfs, getState) - // TODO(window.ipfs) ipfsProxyContentScript = await registerIpfsProxyContentScript() log('register all listeners') registerListeners() await registerSubdomainProxy(getState, runtime, notify) @@ -137,43 +132,6 @@ module.exports = async function init () { } } - // Register Content Script responsible for loading window.ipfs (ipfsProxy) - // - // The key difference between tabs.executeScript and contentScripts API - // is the latter provides guarantee to execute before anything else. - // https://github.com/ipfs-shipyard/ipfs-companion/issues/451#issuecomment-382669093 - /* TODO(window.ipfs) - async function registerIpfsProxyContentScript (previousHandle) { - previousHandle = previousHandle || ipfsProxyContentScript - if (previousHandle) { - previousHandle.unregister() - } - // TODO: - // No window.ipfs for now. - // We will restore when Migration to JS API with Async Await and Async Iterables - // is done: - // https://github.com/ipfs-shipyard/ipfs-companion/pull/777 - // https://github.com/ipfs-shipyard/ipfs-companion/issues/843 - // https://github.com/ipfs-shipyard/ipfs-companion/issues/852#issuecomment-594510819 - const forceOff = true - if (forceOff || !state.active || !state.ipfsProxy || !browser.contentScripts) { - // no-op if global toggle is off, window.ipfs is disabled in Preferences - // or if runtime has no contentScript API - // (Chrome loads content script via manifest) - return - } - const newHandle = await browser.contentScripts.register({ - matches: [''], - js: [ - { file: '/dist/bundles/ipfsProxyContentScript.bundle.js' } - ], - allFrames: true, - runAt: 'document_start' - }) - return newHandle - } - */ - // HTTP Request Hooks // =================================================================== @@ -616,7 +574,6 @@ module.exports = async function init () { switch (key) { case 'active': state[key] = change.newValue - // TODO(window.ipfs) ipfsProxyContentScript = await registerIpfsProxyContentScript() await registerSubdomainProxy(getState, runtime) shouldRestartIpfsClient = true shouldStopIpfsClient = !state.active @@ -671,12 +628,6 @@ module.exports = async function init () { // Finally, update proxy settings based on the state await registerSubdomainProxy(getState, runtime) break - /* TODO(window.ipfs) - case 'ipfsProxy': - state[key] = change.newValue - // This is window.ipfs proxy, requires update of the content script: - ipfsProxyContentScript = await registerIpfsProxyContentScript() - break */ case 'dnslinkPolicy': state.dnslinkPolicy = String(change.newValue) === 'false' ? false : change.newValue if (state.dnslinkPolicy === 'best-effort' && !state.detectIpfsPathHeader) { @@ -770,18 +721,6 @@ module.exports = async function init () { apiStatusUpdateInterval = null } - /* TODO(window.ipfs) - if (ipfsProxyContentScript) { - ipfsProxyContentScript.unregister() - ipfsProxyContentScript = null - } - */ - - if (ipfsProxy) { - await ipfsProxy.destroy() - ipfsProxy = null - } - if (ipfs) { await destroyIpfsClient(browser) ipfs = null diff --git a/add-on/src/lib/ipfs-proxy/access-control.js b/add-on/src/lib/ipfs-proxy/access-control.js deleted file mode 100644 index ea536906d..000000000 --- a/add-on/src/lib/ipfs-proxy/access-control.js +++ /dev/null @@ -1,207 +0,0 @@ -'use strict' -/* eslint-env browser */ - -const EventEmitter = require('events') -const { default: PQueue } = require('p-queue') - -class AccessControl extends EventEmitter { - constructor (storage, storageKeyPrefix = 'ipfsProxyAcl') { - super() - this._storage = storage - this._storageKeyPrefix = storageKeyPrefix - this._onStorageChange = this._onStorageChange.bind(this) - storage.onChanged.addListener(this._onStorageChange) - this._writeQ = new PQueue({ concurrency: 1 }) - } - - async _onStorageChange (changes) { - const prefix = this._storageKeyPrefix - const scopesKey = this._getScopesKey() - const aclChangeKeys = Object.keys(changes).filter((key) => { - return key !== scopesKey && key.startsWith(prefix) - }) - - if (!aclChangeKeys.length) return - - // Map { scope => Map { permission => allow } } - this.emit('change', aclChangeKeys.reduce((aclChanges, key) => { - return aclChanges.set( - key.slice(prefix.length + ('.access'.length) + 1), - new Map(JSON.parse(changes[key].newValue)) - ) - }, new Map())) - } - - _getScopesKey () { - return `${this._storageKeyPrefix}.scopes` - } - - // Get the list of scopes stored in the acl - async _getScopes () { - const key = this._getScopesKey() - return new Set( - JSON.parse((await this._storage.local.get({ [key]: '[]' }))[key]) - ) - } - - async _addScope (scope) { - const scopes = await this._getScopes() - scopes.add(scope) - - const key = this._getScopesKey() - await this._storage.local.set({ [key]: JSON.stringify(Array.from(scopes)) }) - } - - // ordered by longest first - async _getMatchingScopes (scope) { - const scopes = await this._getScopes() - const origin = new URL(scope).origin - - return Array.from(scopes) - .filter(s => { - if (origin !== new URL(s).origin) return false - return scope.startsWith(s) - }) - .sort((a, b) => b.length - a.length) - } - - _getAccessKey (scope) { - return `${this._storageKeyPrefix}.access.${scope}` - } - - // Get a Map of granted permissions for a given scope - // e.g. Map { 'add' => true, 'object.new' => false } - async _getAllAccess (scope) { - const key = this._getAccessKey(scope) - return new Map( - JSON.parse((await this._storage.local.get({ [key]: '[]' }))[key]) - ) - } - - // Return current access rights to given permission. - async getAccess (scope, permission) { - if (!isScope(scope)) throw new TypeError('Invalid scope') - if (!isString(permission)) throw new TypeError('Invalid permission') - - const matchingScopes = await this._getMatchingScopes(scope) - - let allow = null - let matchingScope - - for (matchingScope of matchingScopes) { - const allAccess = await this._getAllAccess(matchingScope) - - if (allAccess.has('*')) { - allow = allAccess.get('*') - break - } - - if (allAccess.has(permission)) { - allow = allAccess.get(permission) - break - } - } - - return allow == null ? null : { scope: matchingScope, permissions: [permission], allow } - } - - // Set access rights to given permissions. - // 'permissions' can be an array of strings or a single string - async setAccess (scope, permissions, allow) { - permissions = Array.isArray(permissions) ? permissions : [permissions] - if (!isScope(scope)) throw new TypeError('Invalid scope') - if (!isStringArray(permissions)) throw new TypeError('Invalid permissions') - if (!isBoolean(allow)) throw new TypeError('Invalid allow') - - return this._writeQ.add(async () => { - const allAccess = await this._getAllAccess(scope) - - // Trying to set access for non-wildcard permission, when wildcard - // permission is already granted? - if (allAccess.has('*') && !permissions.includes('*')) { - if (allAccess.get('*') === allow) { - // Noop if requested access is the same as access for wildcard grant - return { scope, permissions, allow } - } else { - // Fail if requested access is the different to access for wildcard grant - throw new Error(`Illegal set access for '${permissions}' when wildcard exists`) - } - } - - // If setting a wildcard permission, remove existing grants - if (permissions.includes('*')) { - allAccess.clear() - } - - permissions.forEach(permission => allAccess.set(permission, allow)) - - const accessKey = this._getAccessKey(scope) - await this._storage.local.set({ [accessKey]: JSON.stringify(Array.from(allAccess)) }) - - await this._addScope(scope) - - return { scope, permissions, allow } - }) - } - - // Map { scope => Map { permission => allow } } - async getAcl () { - const scopes = await this._getScopes() - const acl = new Map() - - await Promise.all(Array.from(scopes).map(scope => { - return (async () => { - const allAccess = await this._getAllAccess(scope) - acl.set(scope, allAccess) - })() - })) - - return acl - } - - // Revoke access to the given permission - // if permission is null, revoke all access - async revokeAccess (scope, permission = null) { - if (!isScope(scope)) throw new TypeError('Invalid scope') - if (permission && !isString(permission)) throw new TypeError('Invalid permission') - - return this._writeQ.add(async () => { - let allAccess - - if (permission) { - allAccess = await this._getAllAccess(scope) - if (!allAccess.has(permission)) return - allAccess.delete(permission) - } else { - allAccess = new Map() - } - - const key = this._getAccessKey(scope) - await this._storage.local.set({ [key]: JSON.stringify(Array.from(allAccess)) }) - }) - } - - destroy () { - this._storage.onChanged.removeListener(this._onStorageChange) - } -} - -module.exports = AccessControl - -const isScope = (value) => { - if (!isString(value)) return false - - let url - - try { - url = new URL(value) - } catch (_) { - return false - } - - return url.origin + url.pathname === value -} - -const isString = (value) => Object.prototype.toString.call(value) === '[object String]' -const isStringArray = (value) => Array.isArray(value) && value.length && value.every(isString) -const isBoolean = (value) => value === true || value === false diff --git a/add-on/src/lib/ipfs-proxy/command-whitelist.json b/add-on/src/lib/ipfs-proxy/command-whitelist.json deleted file mode 100644 index 9e37d2150..000000000 --- a/add-on/src/lib/ipfs-proxy/command-whitelist.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - "add", - "block", - "cat", - "dag", - "dht", - "dns", - "files", - "get", - "id", - "ls", - "name.resolve", - "object", - "pin", - "ping", - "pingPullStream", - "pingReadableStream", - "pubsub", - "repo", - "stats", - "swarm", - "version" -] diff --git a/add-on/src/lib/ipfs-proxy/enable-command.js b/add-on/src/lib/ipfs-proxy/enable-command.js deleted file mode 100644 index ff87c0074..000000000 --- a/add-on/src/lib/ipfs-proxy/enable-command.js +++ /dev/null @@ -1,57 +0,0 @@ -const debug = require('debug') -const log = debug('ipfs-companion:proxy') -log.error = debug('ipfs-companion:proxy:error') - -const { inCommandWhitelist, createCommandWhitelistError } = require('./pre-command') -const { createProxyAclError } = require('./pre-acl') - -// Artificial API command responsible for backend orchestration -// during window.ipfs.enable() -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 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 - - // Validate and prompt for any missing permissions in bulk - // if a list of needed commands is announced up front - if (opts.commands) { - const missingAcls = [] - const deniedAcls = [] - for (const command of opts.commands) { - // Fail fast if command is not allowed to be proxied at all - if (!inCommandWhitelist(command)) { - throw createCommandWhitelistError(command) - } - // Get the current access flag to decide if it should be added - // to the list of permissions to be prompted about in the next step - const access = await accessControl.getAccess(scope, command) - if (!access) { - missingAcls.push(command) - } else if (access.allow !== true) { - deniedAcls.push(command) - } - } - // Fail fast if user already denied any of requested permissions - if (deniedAcls.length) { - throw createProxyAclError(scope, deniedAcls) - } - // Display a single prompt with all missing permissions - if (missingAcls.length) { - const { allow, wildcard } = await requestAccess(scope, missingAcls) - const access = await accessControl.setAccess(scope, wildcard ? '*' : missingAcls, allow) - if (!access.allow) { - throw createProxyAclError(scope, missingAcls) - } - } - } - } -} - -module.exports = createEnableCommand diff --git a/add-on/src/lib/ipfs-proxy/index.js b/add-on/src/lib/ipfs-proxy/index.js deleted file mode 100644 index f895ae1b9..000000000 --- a/add-on/src/lib/ipfs-proxy/index.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict' -/* eslint-env browser */ - -const browser = require('webextension-polyfill') -const { createProxyServer, closeProxyServer } = require('ipfs-postmsg-proxy') -const { expose } = require('postmsg-rpc') -const AccessControl = require('./access-control') -const createEnableCommand = require('./enable-command') -const { createPreCommand } = require('./pre-command') -const { createPreAcl } = require('./pre-acl') -const createPreMfsScope = require('./pre-mfs-scope') -const createRequestAccess = require('./request-access') - -// Creates an object that manages the "server side" of the IPFS proxy -function createIpfsProxy (getIpfs, getState) { - let connections = [] - const accessControl = new AccessControl(browser.storage) - const requestAccess = createRequestAccess(browser, screen) - - // Port connection events are emitted when a content script attempts to - // communicate with us. Each new URL visited by the user will open a port. - // When a port is opened, we create a new IPFS proxy server to listen to the - // messages. - const onPortConnect = (port) => { - if (port.name !== 'ipfs-proxy') return - - const getScope = async () => { - const tab = await browser.tabs.get(port.sender.tab.id) - const { origin, pathname } = new URL(tab.url) - return origin + pathname - } - - // https://github.com/ipfs-shipyard/ipfs-postmsg-proxy#api - const proxyCfg = { - addListener: (_, handler) => port.onMessage.addListener(handler), - removeListener: (_, handler) => port.onMessage.removeListener(handler), - postMessage: (data) => port.postMessage(data), - getMessageData: (d) => d, - pre: (fnName) => [ - createPreCommand(fnName), - createPreAcl(fnName, getState, getScope, accessControl, requestAccess), - createPreMfsScope(fnName, getScope, getIpfs) - ] - } - - const proxy = createProxyServer(getIpfs, proxyCfg) - - // Extend proxy with Companion-specific commands: - const enableCommand = createEnableCommand(getIpfs, getState, getScope, accessControl, requestAccess) - Object.assign(proxy, { - // window.ipfs.enable(opts) - 'proxy.enable': expose('proxy.enable', enableCommand, proxyCfg) - }) - - const close = () => { - port.onDisconnect.removeListener(onDisconnect) - connections = connections.filter(c => c.close !== close) - return closeProxyServer(proxy) - } - - const onDisconnect = () => close() - - // If the port disconnects, clean up the resources for this connection - port.onDisconnect.addListener(onDisconnect) - - // Keep track of the open connections so that if destroy is called we can - // clean up properly. - connections.push({ close }) - } - - browser.runtime.onConnect.addListener(onPortConnect) - - const handle = { - destroy () { - browser.runtime.onConnect.removeListener(onPortConnect) - return Promise.all(connections.map(c => c.close())) - } - } - - return handle -} - -module.exports = createIpfsProxy diff --git a/add-on/src/lib/ipfs-proxy/pre-acl.js b/add-on/src/lib/ipfs-proxy/pre-acl.js deleted file mode 100644 index d99d14628..000000000 --- a/add-on/src/lib/ipfs-proxy/pre-acl.js +++ /dev/null @@ -1,61 +0,0 @@ -// Creates a "pre" function that is called prior to calling a real function -// on the IPFS instance. It will throw if access is denied, and ask the user if -// no access decision has been made yet. -function createPreAcl (permission, getState, getScope, accessControl, requestAccess) { - return async (...args) => { - 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 access = await getAccessWithPrompt(accessControl, requestAccess, scope, permission) - - if (!access.allow) { - throw createProxyAclError(scope, permission) - } - - return args - } -} - -async function getAccessWithPrompt (accessControl, requestAccess, scope, permission) { - let access = await accessControl.getAccess(scope, permission) - if (!access) { - const { allow, wildcard } = await requestAccess(scope, permission) - access = await accessControl.setAccess(scope, wildcard ? '*' : permission, allow) - } - return access -} - -// Standardized error thrown when a command access is denied -// TODO: return errors following conventions from https://github.com/ipfs/js-ipfs/pull/1746 -function createProxyAclError (scope, permission, message) { - const err = new Error(message || `User denied access to selected commands over IPFS proxy: ${permission}`) - const permissions = Array.isArray(permission) ? permission : [permission] - err.output = { - payload: { - // Error follows convention from https://github.com/ipfs/js-ipfs/pull/1746/files - code: 'ERR_IPFS_PROXY_ACCESS_DENIED', - permissions, - scope, - // TODO: remove below after deprecation period ends with Q1 - get isIpfsProxyAclError () { - console.warn("[ipfs-companion] reading .isIpfsProxyAclError from Ipfs Proxy errors is deprecated, use '.code' instead") - return true - }, - get permission () { - if (!this.permissions || !this.permissions.length) return undefined - console.warn("[ipfs-companion] reading .permission from Ipfs Proxy errors is deprecated, use '.permissions' instead") - return this.permissions[0] - } - } - } - return err -} - -module.exports = { - createPreAcl, - createProxyAclError -} diff --git a/add-on/src/lib/ipfs-proxy/pre-command.js b/add-on/src/lib/ipfs-proxy/pre-command.js deleted file mode 100644 index 5b64f8a4e..000000000 --- a/add-on/src/lib/ipfs-proxy/pre-command.js +++ /dev/null @@ -1,62 +0,0 @@ -// Some API commands are too sensitive to be exposed to dapps on every website -// We follow a safe security practice of denying everything and allowing access -// to a pre-approved list of known APIs. This way if a new API is added -// it will be blocked by default, until it is explicitly enabled below. -const COMMAND_WHITELIST = Object.freeze(require('./command-whitelist.json')) - -// Creates a "pre" function that is called prior to calling a real function -// on the IPFS instance. It will throw if access is denied -// due to API not being whitelisted of arguments not being supported -function createPreCommand (permission) { - return async (...args) => { - if (!inCommandWhitelist(permission)) { - throw createCommandWhitelistError(permission) - } - if (['add', 'files.add'].includes(permission)) { - // Fail fast: nocopy does not work over proxy - if (args.some(arg => typeof arg === 'object' && arg.nocopy)) { - throw new Error(`ipfs.${permission} with 'nocopy' flag is not supported by IPFS Proxy`) - } - } - return args - } -} - -function inCommandWhitelist (permission) { - // Fail fast if API or namespace is not explicitly whitelisted - const permRoot = permission.split('.')[0] - return COMMAND_WHITELIST.includes(permRoot) || COMMAND_WHITELIST.includes(permission) -} - -// Standardized error thrown when a command is not on the COMMAND_WHITELIST -// TODO: return errors following conventions from https://github.com/ipfs/js-ipfs/pull/1746 -function createCommandWhitelistError (permission) { - const permissions = Array.isArray(permission) ? permission : [permission] - console.warn(`[ipfs-companion] Access to '${permission}' commands over window.ipfs is blocked. If you feel it should be allowed, open an issue at https://github.com/ipfs-shipyard/ipfs-companion/issues/new`) - const err = new Error(`Access to '${permission}' commands over IPFS Proxy is globally blocked`) - err.output = { - payload: { - // Error follows convention from https://github.com/ipfs/js-ipfs/pull/1746/files - code: 'ERR_IPFS_PROXY_ACCESS_DENIED', - permissions, - // TODO: remove below after deprecation period ends with Q1 - get isIpfsProxyWhitelistError () { - console.warn("[ipfs-companion] reading .isIpfsProxyWhitelistError from Ipfs Proxy errors is deprecated, use '.code' instead") - return true - }, - get permission () { - if (!this.permissions || !this.permissions.length) return undefined - console.warn("[ipfs-companion] reading .permission from Ipfs Proxy errors is deprecated, use '.permissions' instead") - return this.permissions[0] - } - } - } - return err -} - -module.exports = { - createPreCommand, - createCommandWhitelistError, - inCommandWhitelist, - COMMAND_WHITELIST -} diff --git a/add-on/src/lib/ipfs-proxy/pre-mfs-scope.js b/add-on/src/lib/ipfs-proxy/pre-mfs-scope.js deleted file mode 100644 index 7643b82ce..000000000 --- a/add-on/src/lib/ipfs-proxy/pre-mfs-scope.js +++ /dev/null @@ -1,152 +0,0 @@ -// Use path-browserify for consistent behavior between browser and tests on Windows -const Path = require('path-browserify') -const IsIpfs = require('is-ipfs') -const DEFAULT_ROOT_PATH = '/dapps' - -// Creates a "pre" function that is called prior to calling a real function -// on the IPFS instance. It modifies the arguments to MFS functions to scope -// file access to a directory designated to the web page -function createPreMfsScope (fnName, getScope, getIpfs, rootPath = DEFAULT_ROOT_PATH) { - return MfsPre[fnName] ? MfsPre[fnName](getScope, getIpfs, rootPath) : null -} - -module.exports = createPreMfsScope - -const MfsPre = { - 'files.cp': createSrcDestPre, - 'files.mkdir': createSrcPre, - 'files.stat': createSrcPre, - 'files.rm' (getScope, getIpfs, rootPath) { - const srcPre = createSrcPre(getScope, getIpfs, rootPath) - // Do not allow rm app root - // Need to explicitly deny because it's ok to rm -rf /a/path that's not / - return (...args) => { - if (isRoot(args[0])) throw new Error('cannot delete root') - return srcPre(...args) - } - }, - 'files.read': createSrcPre, - 'files.write' (getScope, getIpfs, rootPath) { - const srcPre = createSrcPre(getScope, getIpfs, rootPath) - // Do not allow write to app root - // Need to explicitly deny because app path might not exist yet - return (...args) => { - if (isRoot(args[0])) throw new Error('/ was not a file') - return srcPre(...args) - } - }, - 'files.mv': createSrcDestPre, - 'files.flush': createOptionalSrcPre, - 'files.ls': createOptionalSrcPre -} - -// Scope a src/dest tuple to the app path -function createSrcDestPre (getScope, getIpfs, rootPath) { - return async (...args) => { - // console.log('createSrcDestPre.args.before: ' + JSON.stringify(args)) - const appPath = await getAppPath(getScope, getIpfs, rootPath) - // console.log('createSrcDestPre.appPath: ', appPath) - args = prefixSrcDestArgs(appPath, args) - // console.log('createSrcDestPre.args.after: ' + JSON.stringify(args)) - return args - } -} - -// Prefix src and dest args where applicable -function prefixSrcDestArgs (prefix, args) { - const prefixedArgs = [] - const destPosition = destinationPosition(args) - for (let i = 0; i < args.length; i++) { - const item = args[i] - if (typeof item === 'string') { - const isDestination = (i === destPosition) - prefixedArgs[i] = safePathPrefix(prefix, item, isDestination) - } else if (Array.isArray(item)) { - // The syntax recently changed to remove passing an array, - // but we allow for both versions until js-ipfs-http-client is updated to remove - // support for it - console.warn('[ipfs-companion] use of array in ipfs.files.cp|mv is deprecated, see https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#filescp') - prefixedArgs[i] = prefixSrcDestArgs(prefix, item) - } else { - // {options} or callback, passing as-is - prefixedArgs[i] = item - } - } - return prefixedArgs -} - -// Find the last string argument and save as the position of destination path -function destinationPosition (args) { - let destPosition - for (let i = 0; i < args.length; i++) { - if (typeof args[i] === 'string') { - destPosition = i - } - } - return destPosition -} - -// Add a prefix to a path in a safe way -function safePathPrefix (prefix, path, isDestination) { - const realPath = safePath(path) - if (!isDestination && IsIpfs.ipfsPath(realPath)) { - // we don't prefix valid /ipfs/ paths in source paths - // (those are public and immutable, so safe as-is) - return realPath - } - return Path.join(prefix, realPath) -} - -// Scope a src path to the app path -function createSrcPre (getScope, getIpfs, rootPath) { - return async (...args) => { - const appPath = await getAppPath(getScope, getIpfs, rootPath) - args[0] = Path.join(appPath, safePath(args[0])) - return args - } -} - -// Scope an optional src path to the app path -function createOptionalSrcPre (getScope, getIpfs, rootPath) { - return async (...args) => { - const appPath = await getAppPath(getScope, getIpfs, rootPath) - - if (Object.prototype.toString.call(args[0]) === '[object String]') { - args[0] = Path.join(appPath, safePath(args[0])) - } else { - switch (args.length) { - case 0: return [appPath] // e.g. ipfs.files.ls() - case 1: return [appPath, args[0]] // e.g. ipfs.files.ls(options) - case 2: return [appPath, args[1]] // e.g. ipfs.files.ls(null, options) - default: throw new Error('Unexpected number of arguments') - } - } - return args - } -} - -// Get the app path (create if not exists) for a scope, prefixed with rootPath -const getAppPath = async (getScope, getIpfs, rootPath) => { - const appPath = rootPath + scopeToPath(await getScope()) - await getIpfs().files.mkdir(appPath, { parents: true }) - return appPath -} - -// Turn http://ipfs.io/ipfs/QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn -// into /http/ipfs.io/ipfs/QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn -const scopeToPath = (scope) => { - return ('/' + scope) - .replace(/\/\//g, '/') - .split('/') - // Special case for protocol in scope, remove : from the end - .map((seg, i) => i === 1 && seg.endsWith(':') ? seg.slice(0, -1) : seg) - .map(encodeURIComponent) - .join('/') -} - -// Make a path "safe" by resolving any directory traversal segments relative to -// '/'. Allows us to then prefix the app path without worrying about the user -// breaking out of their jail. -const safePath = (path) => Path.resolve('/', path) - -const isRoot = (path) => Path.resolve('/', path) === '/' diff --git a/add-on/src/lib/ipfs-proxy/request-access.js b/add-on/src/lib/ipfs-proxy/request-access.js deleted file mode 100644 index b85bc50a3..000000000 --- a/add-on/src/lib/ipfs-proxy/request-access.js +++ /dev/null @@ -1,117 +0,0 @@ -'use strict' - -const { piggyback } = require('piggybacker') - -const DIALOG_WIDTH = 540 -const DIALOG_HEIGHT = 220 -const DIALOG_PATH = 'dist/pages/proxy-access-dialog/index.html' -const DIALOG_PORT_NAME = 'proxy-access-dialog' - -function createRequestAccess (browser, screen) { - // piggybacker allows multiple requests for access to the same permissions to - // receive the same response i.e. don't popup multiple dialogs for the - // same permissions request. - return piggyback(requestAccess, (scope, permissions) => `${scope}/${permissions}`) - - async function requestAccess (scope, permissions, opts) { - opts = opts || {} - - // TODO: cleanup so below stub is not needed - permissions = Array.isArray(permissions) ? permissions : [permissions] - - const url = browser.runtime.getURL(opts.dialogPath || DIALOG_PATH) - - let dialogTabId - - if (browser.windows && browser.windows.create) { - // display modal dialog in a centered popup window - const currentWin = await browser.windows.getCurrent() - const width = opts.dialogWidth || DIALOG_WIDTH - const height = opts.dialogHeight || DIALOG_HEIGHT - const top = Math.round(((screen.width / 2) - (width / 2)) + currentWin.left) - const left = Math.round(((screen.height / 2) - (height / 2)) + currentWin.top) - - const dialogWindow = await browser.windows.create({ url, width, height, top, left, type: 'popup' }) - dialogTabId = dialogWindow.tabs[0].id - } else { - // fallback: opening dialog as a new active tab - // (runtimes without browser.windows.create, eg. Andorid) - dialogTabId = (await browser.tabs.create({ active: true, url: url })).id - } - - // Resolves with { allow, wildcard } - const userResponse = getUserResponse(dialogTabId, scope, permissions, opts) - // Never resolves, might reject if user closes the tab - const userTabRemoved = getUserTabRemoved(dialogTabId, scope, permissions) - - let response - - try { - // Will the user respond to or close the dialog? - response = await Promise.race([userTabRemoved, userResponse]) - } finally { - userTabRemoved.destroy() - userResponse.destroy() - } - - await browser.tabs.remove(dialogTabId) - - return response - } - - function getUserResponse (tabId, scope, permissions, opts) { - opts = opts || {} - - const dialogPortName = opts.dialogPortName || DIALOG_PORT_NAME - let onPortConnect - - const userResponse = new Promise((resolve, reject) => { - onPortConnect = port => { - if (port.name !== dialogPortName) return - if (!port.sender || !port.sender.tab || port.sender.tab.id !== tabId) return - - browser.runtime.onConnect.removeListener(onPortConnect) - - // Tell the dialog what scope/permissions it is about - port.postMessage({ scope, permissions }) - - // Wait for the user response - const onMessage = ({ allow, wildcard }) => { - port.onMessage.removeListener(onMessage) - resolve({ allow, wildcard }) - } - - port.onMessage.addListener(onMessage) - } - - browser.runtime.onConnect.addListener(onPortConnect) - }) - - userResponse.destroy = () => browser.runtime.onConnect.removeListener(onPortConnect) - - return userResponse - } - - // Since the dialog is a tab not a real dialog it can be closed by the user - // with no response, this function creates a promise that will reject if the tab - // is removed. - function getUserTabRemoved (tabId, scope, permissions) { - let onTabRemoved - - const userTabRemoved = new Promise((resolve, reject) => { - onTabRemoved = (id) => { - if (id !== tabId) return - const err = new Error(`IPFS Proxy failed to obtain access response for '${permissions}' at ${scope}`) - err.output = { payload: { isIpfsProxyError: true, isIpfsProxyAclError: true, scope, permissions } } - reject(err) - } - browser.tabs.onRemoved.addListener(onTabRemoved) - }) - - userTabRemoved.destroy = () => browser.tabs.onRemoved.removeListener(onTabRemoved) - - return userTabRemoved - } -} - -module.exports = createRequestAccess diff --git a/add-on/src/lib/options.js b/add-on/src/lib/options.js index 91655ae7a..c82cd941a 100644 --- a/add-on/src/lib/options.js +++ b/add-on/src/lib/options.js @@ -27,7 +27,6 @@ exports.optionDefaults = Object.freeze({ customGatewayUrl: 'http://localhost:8080', ipfsApiUrl: 'http://127.0.0.1:5001', ipfsApiPollMs: 3000, - ipfsProxy: true, // window.ipfs logNamespaces: 'jsipfs*,ipfs*,libp2p:mdns*,libp2p-delegated*,-*:ipns*,-ipfs:preload*,-ipfs-http-client:request*,-ipfs:http-api*', importDir: '/ipfs-companion-imports/%Y-%M-%D_%h%m%s/', useLatestWebUI: false, diff --git a/add-on/src/options/forms/experiments-form.js b/add-on/src/options/forms/experiments-form.js index 521d66898..24b2db160 100644 --- a/add-on/src/options/forms/experiments-form.js +++ b/add-on/src/options/forms/experiments-form.js @@ -13,7 +13,6 @@ function experimentsForm ({ linkify, recoverFailedHttpRequests, detectIpfsPathHeader, - ipfsProxy, logNamespaces, onOptionChange }) { @@ -24,7 +23,6 @@ function experimentsForm ({ const onLinkifyChange = onOptionChange('linkify') const onrecoverFailedHttpRequestsChange = onOptionChange('recoverFailedHttpRequests') const onDetectIpfsPathHeaderChange = onOptionChange('detectIpfsPathHeader') - const onIpfsProxyChange = onOptionChange('ipfsProxy') return html`
@@ -98,30 +96,6 @@ function experimentsForm ({
${switchToggle({ id: 'detectIpfsPathHeader', checked: detectIpfsPathHeader, onchange: onDetectIpfsPathHeaderChange })}
-
- -
${switchToggle({ id: 'ipfsProxy', checked: ipfsProxy, disabled: true, onchange: onIpfsProxyChange })}
-