diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index 4346e1c44..c3ddd6766 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -2,6 +2,7 @@ /* eslint-env browser, webextensions */ const browser = require('webextension-polyfill') +const toMultiaddr = require('uri-to-multiaddr') const { optionDefaults, storeMissingOptions, migrateOptions } = require('./options') const { initState, offlinePeerCount } = require('./state') const { createIpfsPathValidator, pathAtHttpGateway } = require('./ipfs-path') @@ -92,13 +93,18 @@ module.exports = async function init () { } function registerListeners () { - browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, { urls: [''] }, ['blocking', 'requestHeaders']) + let onBeforeSendInfoSpec = ['blocking', 'requestHeaders'] + if (!runtime.isFirefox) { + // Chrome 72+ requires 'extraHeaders' for access to Referer header (used in cors whitelisting of webui) + onBeforeSendInfoSpec.push('extraHeaders') + } + browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, { urls: [''] }, onBeforeSendInfoSpec) browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, { urls: [''] }, ['blocking']) browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, { urls: [''] }, ['blocking', 'responseHeaders']) browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, { urls: [''] }) browser.storage.onChanged.addListener(onStorageChange) browser.webNavigation.onCommitted.addListener(onNavigationCommitted) - browser.tabs.onUpdated.addListener(onUpdatedTab) + browser.webNavigation.onDOMContentLoaded.addListener(onDOMContentLoaded) browser.tabs.onActivated.addListener(onActivatedTab) if (browser.windows) { browser.windows.onFocusChanged.addListener(onWindowFocusChanged) @@ -213,6 +219,7 @@ module.exports = async function init () { peerCount: state.peerCount, gwURLString: state.gwURLString, pubGwURLString: state.pubGwURLString, + webuiRootUrl: state.webuiRootUrl, currentTab: await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0]) } try { @@ -378,45 +385,57 @@ module.exports = async function init () { } } - async function onUpdatedTab (tabId, changeInfo, tab) { + async function onDOMContentLoaded (details) { if (!state.active) return // skip content script injection when off - if (changeInfo.status && changeInfo.status === 'complete' && tab.url && tab.url.startsWith('http')) { - if (state.linkify) { - console.info(`[ipfs-companion] Running linkfyDOM for ${tab.url}`) - try { - await browser.tabs.executeScript(tabId, { - file: '/dist/bundles/linkifyContentScript.bundle.js', - matchAboutBlank: false, - allFrames: true, - runAt: 'document_idle' - }) - } catch (error) { - console.error(`Unable to linkify DOM at '${tab.url}' due to`, error) - } + if (!details.url.startsWith('http')) return // skip special pages + // console.info(`[ipfs-companion] onDOMContentLoaded`, details) + if (state.linkify) { + console.info(`[ipfs-companion] Running linkfy experiment for ${details.url}`) + try { + await browser.tabs.executeScript(details.tabId, { + file: '/dist/bundles/linkifyContentScript.bundle.js', + matchAboutBlank: false, + allFrames: true, + runAt: 'document_idle' + }) + } catch (error) { + console.error(`Unable to linkify DOM at '${details.url}' due to`, error) } - if (state.catchUnhandledProtocols) { - // console.log(`[ipfs-companion] Normalizing links with unhandled protocols at ${tab.url}`) - // See: https://github.com/ipfs/ipfs-companion/issues/286 - try { - // pass the URL of user-preffered public gateway - await browser.tabs.executeScript(tabId, { - code: `window.ipfsCompanionPubGwURL = '${state.pubGwURLString}'`, - matchAboutBlank: false, - allFrames: true, - runAt: 'document_start' - }) - // inject script that normalizes `href` and `src` containing unhandled protocols - await browser.tabs.executeScript(tabId, { - file: '/dist/bundles/normalizeLinksContentScript.bundle.js', - matchAboutBlank: false, - allFrames: true, - runAt: 'document_end' - }) - } catch (error) { - console.error(`Unable to normalize links at '${tab.url}' due to`, error) - } + } + if (state.catchUnhandledProtocols) { + // console.log(`[ipfs-companion] Normalizing links with unhandled protocols at ${tab.url}`) + // See: https://github.com/ipfs/ipfs-companion/issues/286 + try { + // pass the URL of user-preffered public gateway + await browser.tabs.executeScript(details.tabId, { + code: `window.ipfsCompanionPubGwURL = '${state.pubGwURLString}'`, + matchAboutBlank: false, + allFrames: true, + runAt: 'document_start' + }) + // inject script that normalizes `href` and `src` containing unhandled protocols + await browser.tabs.executeScript(details.tabId, { + file: '/dist/bundles/normalizeLinksContentScript.bundle.js', + matchAboutBlank: false, + allFrames: true, + runAt: 'document_end' + }) + } catch (error) { + console.error(`Unable to normalize links at '${details.url}' due to`, error) } } + if (details.url.startsWith(state.webuiRootUrl)) { + // Ensure API backend points at one from IPFS Companion + const apiMultiaddr = toMultiaddr(state.apiURLString) + await browser.tabs.executeScript(details.tabId, { + runAt: 'document_start', + code: `if (!localStorage.getItem('ipfsApi')) { + console.log('[ipfs-companion] Setting API to ${apiMultiaddr}'); + localStorage.setItem('ipfsApi', '${apiMultiaddr}'); + window.location.reload(); + }` + }) + } } // API STATUS UPDATES @@ -597,6 +616,7 @@ module.exports = async function init () { case 'customGatewayUrl': state.gwURL = new URL(change.newValue) state.gwURLString = state.gwURL.toString() + state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/` break case 'publicGatewayUrl': state.pubGwURL = new URL(change.newValue) diff --git a/add-on/src/lib/ipfs-request.js b/add-on/src/lib/ipfs-request.js index 9253a44bb..e3fd40bbb 100644 --- a/add-on/src/lib/ipfs-request.js +++ b/add-on/src/lib/ipfs-request.js @@ -25,10 +25,33 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru const runtimeRoot = browser.runtime.getURL('/') const webExtensionOrigin = runtimeRoot ? new URL(runtimeRoot).origin : 'null' - // Ignored requests are identified once and cached across all browser.webRequest hooks - const ignoredRequests = new LRU({ max: 128, maxAge: 1000 * 30 }) + // Various types of requests are identified once and cached across all browser.webRequest hooks + const requestCacheCfg = { max: 128, maxAge: 1000 * 30 } + const ignoredRequests = new LRU(requestCacheCfg) const ignore = (id) => ignoredRequests.set(id, true) const isIgnored = (id) => ignoredRequests.get(id) !== undefined + + const acrhHeaders = new LRU(requestCacheCfg) // webui cors fix in Chrome + const originUrls = new LRU(requestCacheCfg) // request.originUrl workaround for Chrome + const originUrl = (request) => { + // Firefox and Chrome provide relevant value in different fields: + // (Firefox) request object includes full URL of origin document, return as-is + if (request.originUrl) return request.originUrl + // (Chrome) is lacking: `request.initiator` is just the origin (protocol+hostname+port) + // To reconstruct originUrl we read full URL from Referer header in onBeforeSendHeaders + // and cache it for short time + // TODO: when request.originUrl is available in Chrome the `originUrls` cache can be removed + let cachedUrl = originUrls.get(request.requestId) + if (cachedUrl) return cachedUrl + if (request.requestHeaders) { + const referer = request.requestHeaders.find(h => h.name === 'Referer') + if (referer) { + originUrls.set(request.requestId, referer.value) + return referer.value + } + } + } + const preNormalizationSkip = (state, request) => { // skip requests to the custom gateway or API (otherwise we have too much recursion) if (request.url.startsWith(state.gwURLString) || request.url.startsWith(state.apiURLString)) { @@ -44,6 +67,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru } return isIgnored(request.requestId) } + const postNormalizationSkip = (state, request) => { // skip requests to the public gateway if embedded node is running (otherwise we have too much recursion) if (state.ipfsNodeType === 'embedded' && request.url.startsWith(state.pubGwURLString)) { @@ -115,12 +139,40 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru return } + // Special handling of requests made to API if (request.url.startsWith(state.apiURLString)) { + // Requests made by 'blessed' Web UI + // -------------------------------------------- + // Goal: Web UI works without setting CORS at go-ipfs + // (Without this snippet go-ipfs will return HTTP 403 due to additional origin check on the backend) + const origin = originUrl(request) + if (origin && origin.startsWith(state.webuiRootUrl)) { + // console.log('onBeforeSendHeaders', request) + // console.log('onBeforeSendHeaders.origin', origin) + // Swap Origin to pass server-side check + // (go-ipfs returns HTTP 403 on origin mismatch if there are no CORS headers) + const swapOrigin = (at) => { + request.requestHeaders[at].value = request.requestHeaders[at].value.replace(state.gwURL.origin, state.apiURL.origin) + } + let foundAt = request.requestHeaders.findIndex(h => h.name === 'Origin') + if (foundAt > -1) swapOrigin(foundAt) + foundAt = request.requestHeaders.findIndex(h => h.name === 'Referer') + if (foundAt > -1) swapOrigin(foundAt) + + // Save access-control-request-headers from preflight + foundAt = request.requestHeaders.findIndex(h => h.name && h.name.toLowerCase() === 'access-control-request-headers') + if (foundAt > -1) { + acrhHeaders.set(request.requestId, request.requestHeaders[foundAt].value) + // console.log('onBeforeSendHeaders FOUND access-control-request-headers', acrhHeaders.get(request.requestId)) + } + // console.log('onBeforeSendHeaders fixed headers', request.requestHeaders) + } + // '403 - Forbidden' fix for Chrome and Firefox // -------------------------------------------- - // We remove Origin header from requests made to API URL + // We remove Origin header from requests made to API URL and WebUI // by js-ipfs-http-client running in WebExtension context to remove need - // for manual whitelisting Access-Control-Allow-Origin at go-ipfs + // for manual CORS whitelisting via Access-Control-Allow-Origin at go-ipfs // More info: // Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/622 // Chromium 71: https://github.com/ipfs-shipyard/ipfs-companion/pull/616 @@ -142,13 +194,10 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru } return false } - for (let i = 0; i < request.requestHeaders.length; i++) { - let header = request.requestHeaders[i] - if (header.name === 'Origin' && isWebExtensionOrigin(header.value)) { - request.requestHeaders.splice(i, 1) - break - } - } + + // Remove Origin header matching webExtensionOrigin + const foundAt = request.requestHeaders.findIndex(h => h.name === 'Origin' && isWebExtensionOrigin(h.value)) + if (foundAt > -1) request.requestHeaders.splice(foundAt, 1) // Fix "http: invalid Read on closed Body" // ---------------------------------- @@ -200,8 +249,48 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru onHeadersReceived (request) { const state = getState() - // Skip if IPFS integrations are inactive or request is marked as ignored - if (!state.active || isIgnored(request.requestId)) { + // Skip if IPFS integrations are inactive + if (!state.active) { + return + } + + // Special handling of requests made to API + if (request.url.startsWith(state.apiURLString)) { + // Special handling of requests made by 'blessed' Web UI from local Gateway + // Goal: Web UI works without setting CORS at go-ipfs + // (This includes 'ignored' requests: CORS needs to be fixed even if no redirect is done) + const origin = originUrl(request) + if (origin && origin.startsWith(state.webuiRootUrl) && request.responseHeaders) { + // console.log('onHeadersReceived', request) + const acaOriginHeader = { name: 'Access-Control-Allow-Origin', value: state.gwURL.origin } + const foundAt = findHeaderIndex(acaOriginHeader.name, request.responseHeaders) + if (foundAt > -1) { + request.responseHeaders[foundAt].value = acaOriginHeader.value + } else { + request.responseHeaders.push(acaOriginHeader) + } + + // Restore access-control-request-headers from preflight + const acrhValue = acrhHeaders.get(request.requestId) + if (acrhValue) { + const acahHeader = { name: 'Access-Control-Allow-Headers', value: acrhValue } + const foundAt = findHeaderIndex(acahHeader.name, request.responseHeaders) + if (foundAt > -1) { + request.responseHeaders[foundAt].value = acahHeader.value + } else { + request.responseHeaders.push(acahHeader) + } + acrhHeaders.del(request.requestId) + // console.log('onHeadersReceived SET Access-Control-Allow-Headers', header) + } + + // console.log('onHeadersReceived fixed headers', request.responseHeaders) + return { responseHeaders: request.responseHeaders } + } + } + + // Skip if request is marked as ignored + if (isIgnored(request.requestId)) { return } @@ -419,3 +508,7 @@ function normalizedUnhandledIpfsProtocol (request, pubGwUrl) { return { redirectUrl: pathAtHttpGateway(path, pubGwUrl) } } } + +function findHeaderIndex (name, headers) { + return headers.findIndex(x => x.name && x.name.toLowerCase() === name.toLowerCase()) +} diff --git a/add-on/src/lib/state.js b/add-on/src/lib/state.js index 9f0988997..320d87573 100644 --- a/add-on/src/lib/state.js +++ b/add-on/src/lib/state.js @@ -22,6 +22,10 @@ function initState (options) { state.gwURLString = state.gwURL.toString() delete state.customGatewayUrl state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy + // store info about 'blessed' release of Web UI + // which should work without setting CORS headers + state.webuiCid = 'QmXc9raDM1M5G5fpBnVyQ71vR4gbnskwnB9iMEzBuLgvoZ' // v2.3.3 + state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/` return state } diff --git a/add-on/src/popup/browser-action/operations.js b/add-on/src/popup/browser-action/operations.js index 40277b264..dea80e5c2 100644 --- a/add-on/src/popup/browser-action/operations.js +++ b/add-on/src/popup/browser-action/operations.js @@ -16,7 +16,7 @@ module.exports = function operations ({ onToggleRedirect }) { const activeQuickUpload = active && isIpfsOnline && isApiAvailable - const activeWebUI = active && isIpfsOnline // (js-ipfs >=0.34.0-rc.0 is ok) && ipfsNodeType === 'external' + const activeWebUI = active && isIpfsOnline && ipfsNodeType === 'external' const activeGatewaySwitch = active && ipfsNodeType === 'external' return html` diff --git a/add-on/src/popup/browser-action/store.js b/add-on/src/popup/browser-action/store.js index bccb60066..3271665f3 100644 --- a/add-on/src/popup/browser-action/store.js +++ b/add-on/src/popup/browser-action/store.js @@ -125,8 +125,7 @@ module.exports = (state, emitter) => { emitter.on('openWebUi', async () => { try { - // Open bundled version of WebUI - await browser.tabs.create({ url: '/webui/index.html' }) + browser.tabs.create({ url: state.webuiRootUrl }) window.close() } catch (error) { console.error(`Unable Open Web UI due to ${error}`) @@ -236,6 +235,7 @@ module.exports = (state, emitter) => { state.isIpfsOnline = state.active && status.peerCount > -1 state.gatewayVersion = state.active && status.gatewayVersion ? status.gatewayVersion : null state.ipfsApiUrl = state.active ? options.ipfsApiUrl : null + state.webuiRootUrl = status.webuiRootUrl } else { state.ipfsNodeType = 'external' state.swarmPeers = null diff --git a/package.json b/package.json index ceaf04594..660acce23 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,12 @@ "build:copy:ui-kit:ipfs-css:fonts": "shx mkdir -p add-on/ui-kit/fonts && shx cp node_modules/ipfs-css/fonts/* add-on/ui-kit/fonts", "build:copy:ui-kit:ipfs-css:icons": "shx mkdir -p add-on/ui-kit/icons && shx cp node_modules/ipfs-css/icons/* add-on/ui-kit/icons", "build:copy:ui-kit:tachyons": "shx mkdir -p add-on/ui-kit && shx cp node_modules/tachyons/css/tachyons.css add-on/ui-kit", - "build:webui": "cross-env CID=QmXc9raDM1M5G5fpBnVyQ71vR4gbnskwnB9iMEzBuLgvoZ npm run build:webui:with-cid", - "build:webui:with-cid": "cross-env-shell \"shx test -d add-on/webui/ || (npm run build:webui:dir && (npm run build:webui:fetch-ipfs || npm run build:webui:fetch-http) && npm run build:webui:minimize)\"", - "build:webui:dir": "shx mkdir -p add-on/webui", - "build:webui:fetch-ipfs": "cross-env-shell \"ipfs get $CID -o add-on/webui/\"", - "build:webui:fetch-http": "cross-env-shell \"node scripts/fetch-webui-from-gateway.js $CID add-on/webui/\"", - "build:webui:minimize": "shx rm -rf add-on/webui/static/js/*.map && shx rm -rf add-on/webui/static/css/*.map && shx rm -rf add-on/webui/manifest.json", + "DISABLED:issue679:build:webui": "cross-env CID=QmXc9raDM1M5G5fpBnVyQ71vR4gbnskwnB9iMEzBuLgvoZ npm run build:webui:with-cid", + "DISABLED:issue679:build:webui:with-cid": "cross-env-shell \"shx test -d add-on/webui/ || (npm run build:webui:dir && (npm run build:webui:fetch-ipfs || npm run build:webui:fetch-http) && npm run build:webui:minimize)\"", + "DISABLED:issue679:build:webui:dir": "shx mkdir -p add-on/webui", + "DISABLED:issue679:build:webui:fetch-ipfs": "cross-env-shell \"ipfs get $CID -o add-on/webui/\"", + "DISABLED:issue679:build:webui:fetch-http": "cross-env-shell \"node scripts/fetch-webui-from-gateway.js $CID add-on/webui/\"", + "DISABLED:issue679:build:webui:minimize": "shx rm -rf add-on/webui/static/js/*.map && shx rm -rf add-on/webui/static/css/*.map && shx rm -rf add-on/webui/manifest.json", "build:js": "run-s build:js:*", "build:js:webpack": "webpack -p", "build:minimize-dist": "shx rm -rf add-on/dist/lib add-on/dist/contentScripts/ add-on/dist/bundles/ipfsProxyContentScriptPayload.bundle.js", @@ -127,6 +127,7 @@ "piggybacker": "2.0.0", "pull-file-reader": "1.0.2", "tachyons": "4.11.1", + "uri-to-multiaddr": "3.0.1", "webextension-polyfill": "0.3.1" } } diff --git a/scripts/fetch-webui-from-gateway.js b/scripts/fetch-webui-from-gateway.js index a9d6f0138..143d21b3a 100755 --- a/scripts/fetch-webui-from-gateway.js +++ b/scripts/fetch-webui-from-gateway.js @@ -1,5 +1,6 @@ /* This is a fallback script used when ipfs cli fails or is not available * More details: https://github.com/ipfs-shipyard/ipfs-webui/issues/843 + * See also why this is not used: https://github.com/ipfs-shipyard/ipfs-companion/issues/679 */ const tar = require('tar') const request = require('request') @@ -8,10 +9,6 @@ const progress = require('request-progress') const cid = process.argv[2] const destination = process.argv[3] -// pick random preloader -// const no = Math.round(Math.random()) // 0 or 1 -// const url = 'https://node' + no + '.preload.ipfs.io/api/v0/get?arg=' + cid + '&archive=true&compress=true' - // use public gw const url = 'https://ipfs.io/api/v0/get?arg=' + cid + '&archive=true&compress=true' diff --git a/test/functional/lib/ipfs-request-workarounds.test.js b/test/functional/lib/ipfs-request-workarounds.test.js index 477450778..a51e6ab72 100644 --- a/test/functional/lib/ipfs-request-workarounds.test.js +++ b/test/functional/lib/ipfs-request-workarounds.test.js @@ -114,6 +114,50 @@ describe('modifyRequest processing', function () { }) }) + // Web UI is loaded from hardcoded 'blessed' CID, which enables us to remove + // CORS limitation. This makes Web UI opened from browser action work without + // the need for any additional configuration of go-ipfs daemon + describe('a request to API from blessed webuiRootUrl', function () { + it('should pass without CORS limitations ', async function () { + // ensure clean modifyRequest + runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests + modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime) + // test + const webuiOriginHeader = { name: 'Origin', value: state.webuiRootUrl } + const webuiRefererHeader = { name: 'Referer', value: state.webuiRootUrl } + // CORS whitelisting does not worh in Chrome 72 without passing/restoring ACRH preflight header + const acrhHeader = { name: 'Access-Control-Request-Headers', value: 'X-Test' } // preflight to store + + // Test request + let request = { + requestHeaders: [ webuiOriginHeader, webuiRefererHeader, acrhHeader ], + type: 'xmlhttprequest', + originUrl: state.webuiRootUrl, + url: `${state.apiURLString}api/v0/id` + } + request = modifyRequest.onBeforeRequest(request) || request // executes before onBeforeSendHeaders, may mutate state + const requestHeaders = modifyRequest.onBeforeSendHeaders(request).requestHeaders + + // "originUrl" should be swapped to look like it came from the same origin as HTTP API + const expectedOriginUrl = state.webuiRootUrl.replace(state.gwURLString, state.apiURLString) + expect(requestHeaders).to.deep.include({ name: 'Origin', value: expectedOriginUrl }) + expect(requestHeaders).to.deep.include({ name: 'Referer', value: expectedOriginUrl }) + expect(requestHeaders).to.deep.include(acrhHeader) + + // Test response + const response = Object.assign({}, request) + delete response.requestHeaders + response.responseHeaders = [] + const responseHeaders = modifyRequest.onHeadersReceived(response).responseHeaders + const corsHeader = { name: 'Access-Control-Allow-Origin', value: state.gwURL.origin } + const acahHeader = { name: 'Access-Control-Allow-Headers', value: acrhHeader.value } // expect value restored from preflight + expect(responseHeaders).to.deep.include(corsHeader) + expect(responseHeaders).to.deep.include(acahHeader) + + browser.runtime.getURL.flush() + }) + }) + after(function () { delete global.URL delete global.browser diff --git a/webpack.config.js b/webpack.config.js index d57c1c17e..82e49e111 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -55,6 +55,9 @@ const commonConfig = { net: 'empty', tls: 'empty' }, + watchOptions: { + ignored: ['add-on/dist/**/*', 'node_modules'] + }, performance: { maxEntrypointSize: Infinity, maxAssetSize: 4194304 // https://github.com/mozilla/addons-linter/pull/892 diff --git a/yarn.lock b/yarn.lock index e68616ef7..e26276af7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13367,6 +13367,14 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +uri-to-multiaddr@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/uri-to-multiaddr/-/uri-to-multiaddr-3.0.1.tgz#460bd5d78074002c47b60fdc456efd009e7168ae" + integrity sha512-77slJiNB/IxM35zgflBEgp8T8ywpyYAbEh8Ezdnq7kAuA6TRg6wfvNTi4Uixfh6CsPv9K2fAkI5+E4C2dw3tXA== + dependencies: + is-ip "^2.0.0" + multiaddr "^6.0.3" + urijs@^1.18.2: version "1.19.1" resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a"