Skip to content

Commit aec856f

Browse files
committed
refactor: remove code for "blessing" custom webui
This restores the proper way of opening webui in version provided by IPFS node. Context: Before subdomain gateway support was added, we loaded webui from gateway port. Why? API port has a hardcoded list of whitelisted webui versions and it is not possible to load non-whitelisted CID when new webui was released. To enable API access from webui loaded via Gateway port, the Companion extension removed Origin header for requests coming from its background page. This let us avoid the need for manual CORS setup, but was seen in the diff, was pretty complex process. Webui is stable now, so to decrease maintenance burden we move away from that complexity and just load version whitelisted on API port. What if someone wants to run newest webui? They can now load it from webui.ipfs.io.ipns.localhost:8080 (whitelist API access from that specific Origin by appending it to API.HTTPHeaders.Access-Control-Allow-Origin in go-ipfs config) Closes #736
1 parent eb66dfa commit aec856f

File tree

7 files changed

+33
-198
lines changed

7 files changed

+33
-198
lines changed

add-on/src/lib/ipfs-companion.js

+3-16
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const log = debug('ipfs-companion:main')
66
log.error = debug('ipfs-companion:main:error')
77

88
const browser = require('webextension-polyfill')
9-
const toMultiaddr = require('uri-to-multiaddr')
109
const pMemoize = require('p-memoize')
1110
const { optionDefaults, storeMissingOptions, migrateOptions, guiURLString } = require('./options')
1211
const { initState, offlinePeerCount } = require('./state')
@@ -107,7 +106,8 @@ module.exports = async function init () {
107106
function registerListeners () {
108107
const onBeforeSendInfoSpec = ['blocking', 'requestHeaders']
109108
if (browser.webRequest.OnBeforeSendHeadersOptions && 'EXTRA_HEADERS' in browser.webRequest.OnBeforeSendHeadersOptions) {
110-
// Chrome 72+ requires 'extraHeaders' for access to Referer header (used in cors whitelisting of webui)
109+
// Chrome 72+ requires 'extraHeaders' for accessing all headers
110+
// Note: we need this for code ensuring ipfs-http-client can talk to API without setting CORS
111111
onBeforeSendInfoSpec.push('extraHeaders')
112112
}
113113
browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, { urls: ['<all_urls>'] }, onBeforeSendInfoSpec)
@@ -385,18 +385,6 @@ module.exports = async function init () {
385385
log.error(`Unable to linkify DOM at '${details.url}' due to`, error)
386386
}
387387
}
388-
if (details.url.startsWith(state.webuiRootUrl)) {
389-
// Ensure API backend points at one from IPFS Companion
390-
const apiMultiaddr = toMultiaddr(state.apiURLString)
391-
await browser.tabs.executeScript(details.tabId, {
392-
runAt: 'document_start',
393-
code: `if (!localStorage.getItem('ipfsApi')) {
394-
console.log('[ipfs-companion] Setting API to ${apiMultiaddr}');
395-
localStorage.setItem('ipfsApi', '${apiMultiaddr}');
396-
window.location.reload();
397-
}`
398-
})
399-
}
400388
}
401389

402390
// API STATUS UPDATES
@@ -615,6 +603,7 @@ module.exports = async function init () {
615603
case 'ipfsApiUrl':
616604
state.apiURL = new URL(change.newValue)
617605
state.apiURLString = state.apiURL.toString()
606+
state.webuiRootUrl = `${state.apiURLString}webui/`
618607
shouldRestartIpfsClient = true
619608
break
620609
case 'ipfsApiPollMs':
@@ -623,8 +612,6 @@ module.exports = async function init () {
623612
case 'customGatewayUrl':
624613
state.gwURL = new URL(change.newValue)
625614
state.gwURLString = state.gwURL.toString()
626-
// TODO: for now we load webui from API port, should we remove this?
627-
// state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/`
628615
break
629616
case 'publicGatewayUrl':
630617
state.pubGwURL = new URL(change.newValue)

add-on/src/lib/ipfs-request.js

+12-106
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,6 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
4343
const isIgnored = (id) => ignoredRequests.get(id) !== undefined
4444
const errorInFlight = new LRU({ max: 3, maxAge: 1000 })
4545

46-
const acrhHeaders = new LRU(requestCacheCfg) // webui cors fix in Chrome
47-
const originUrls = new LRU(requestCacheCfg) // request.originUrl workaround for Chrome
48-
const originUrl = (request) => {
49-
// Firefox and Chrome provide relevant value in different fields:
50-
// (Firefox) request object includes full URL of origin document, return as-is
51-
if (request.originUrl) return request.originUrl
52-
// (Chrome) is lacking: `request.initiator` is just the origin (protocol+hostname+port)
53-
// To reconstruct originUrl we read full URL from Referer header in onBeforeSendHeaders
54-
// and cache it for short time
55-
// TODO: when request.originUrl is available in Chrome the `originUrls` cache can be removed
56-
const cachedUrl = originUrls.get(request.requestId)
57-
if (cachedUrl) return cachedUrl
58-
if (request.requestHeaders) {
59-
const referer = request.requestHeaders.find(h => h.name === 'Referer')
60-
if (referer) {
61-
originUrls.set(request.requestId, referer.value)
62-
return referer.value
63-
}
64-
}
65-
}
66-
6746
// Returns a canonical hostname representing the site from url
6847
// Main reason for this is unwrapping DNSLink from local subdomain
6948
// <fqdn>.ipns.localhost → <fqdn>
@@ -208,59 +187,25 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
208187

209188
// Special handling of requests made to API
210189
if (sameGateway(request.url, state.apiURL)) {
211-
// Requests made by 'blessed' Web UI
212-
// --------------------------------------------
213-
// Goal: Web UI works without setting CORS at go-ipfs
214-
// (Without this snippet go-ipfs will return HTTP 403 due to additional origin check on the backend)
215-
const origin = originUrl(request)
216-
if (origin && origin.startsWith(state.webuiRootUrl)) {
217-
// console.log('onBeforeSendHeaders', request)
218-
// console.log('onBeforeSendHeaders.origin', origin)
219-
// Swap Origin to pass server-side check
220-
// (go-ipfs returns HTTP 403 on origin mismatch if there are no CORS headers)
221-
const swapOrigin = (at) => {
222-
request.requestHeaders[at].value = request.requestHeaders[at].value.replace(state.gwURL.origin, state.apiURL.origin)
223-
}
224-
let foundAt = request.requestHeaders.findIndex(h => h.name === 'Origin')
225-
if (foundAt > -1) swapOrigin(foundAt)
226-
foundAt = request.requestHeaders.findIndex(h => h.name === 'Referer')
227-
if (foundAt > -1) swapOrigin(foundAt)
228-
229-
// Save access-control-request-headers from preflight
230-
foundAt = request.requestHeaders.findIndex(h => h.name && h.name.toLowerCase() === 'access-control-request-headers')
231-
if (foundAt > -1) {
232-
acrhHeaders.set(request.requestId, request.requestHeaders[foundAt].value)
233-
// console.log('onBeforeSendHeaders FOUND access-control-request-headers', acrhHeaders.get(request.requestId))
234-
}
235-
// console.log('onBeforeSendHeaders fixed headers', request.requestHeaders)
236-
}
237-
238190
// '403 - Forbidden' fix for Chrome and Firefox
239191
// --------------------------------------------
240-
// We remove Origin header from requests made to API URL and WebUI
241-
// by js-ipfs-http-client running in WebExtension context to remove need
242-
// for manual CORS whitelisting via Access-Control-Allow-Origin at go-ipfs
192+
// We remove "Origin: *-extension://" header from requests made to API
193+
// by js-ipfs-http-client running in the background page of browser
194+
// extension. Without this, some users would need to do manual CORS
195+
// whitelisting by adding "..extension://<UUID>" to
196+
// API.HTTPHeaders.Access-Control-Allow-Origin in go-ipfs config.
243197
// More info:
244198
// Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/622
245199
// Chromium 71: https://github.com/ipfs-shipyard/ipfs-companion/pull/616
246200
// Chromium 72: https://github.com/ipfs-shipyard/ipfs-companion/issues/630
247-
const isWebExtensionOrigin = (origin) => {
248-
// console.log(`origin=${origin}, webExtensionOrigin=${webExtensionOrigin}`)
249-
// Chromium <= 71 returns opaque Origin as defined in
250-
// https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin
251-
if (origin == null || origin === 'null') {
252-
return true
253-
}
254-
// Firefox Nightly 65 sets moz-extension://{extension-installation-id}
255-
// Chromium Beta 72 sets chrome-extension://{uid}
256-
if (origin &&
201+
202+
// Firefox Nightly 65 sets moz-extension://{extension-installation-id}
203+
// Chromium Beta 72 sets chrome-extension://{uid}
204+
const isWebExtensionOrigin = (origin) =>
205+
origin &&
257206
(origin.startsWith('moz-extension://') ||
258-
origin.startsWith('chrome-extension://')) &&
259-
new URL(origin).origin === webExtensionOrigin) {
260-
return true
261-
}
262-
return false
263-
}
207+
origin.startsWith('chrome-extension://')) &&
208+
new URL(origin).origin === webExtensionOrigin
264209

265210
// Remove Origin header matching webExtensionOrigin
266211
const foundAt = request.requestHeaders.findIndex(h => h.name === 'Origin' && isWebExtensionOrigin(h.value))
@@ -317,41 +262,6 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
317262
const state = getState()
318263
if (!state.active) return
319264

320-
// Special handling of requests made to API
321-
if (sameGateway(request.url, state.apiURL)) {
322-
// Special handling of requests made by 'blessed' Web UI from local Gateway
323-
// Goal: Web UI works without setting CORS at go-ipfs
324-
// (This includes 'ignored' requests: CORS needs to be fixed even if no redirect is done)
325-
const origin = originUrl(request)
326-
if (origin && origin.startsWith(state.webuiRootUrl) && request.responseHeaders) {
327-
// console.log('onHeadersReceived', request)
328-
const acaOriginHeader = { name: 'Access-Control-Allow-Origin', value: state.gwURL.origin }
329-
const foundAt = findHeaderIndex(acaOriginHeader.name, request.responseHeaders)
330-
if (foundAt > -1) {
331-
request.responseHeaders[foundAt].value = acaOriginHeader.value
332-
} else {
333-
request.responseHeaders.push(acaOriginHeader)
334-
}
335-
336-
// Restore access-control-request-headers from preflight
337-
const acrhValue = acrhHeaders.get(request.requestId)
338-
if (acrhValue) {
339-
const acahHeader = { name: 'Access-Control-Allow-Headers', value: acrhValue }
340-
const foundAt = findHeaderIndex(acahHeader.name, request.responseHeaders)
341-
if (foundAt > -1) {
342-
request.responseHeaders[foundAt].value = acahHeader.value
343-
} else {
344-
request.responseHeaders.push(acahHeader)
345-
}
346-
acrhHeaders.del(request.requestId)
347-
// console.log('onHeadersReceived SET Access-Control-Allow-Headers', header)
348-
}
349-
350-
// console.log('onHeadersReceived fixed headers', request.responseHeaders)
351-
return { responseHeaders: request.responseHeaders }
352-
}
353-
}
354-
355265
// Skip if request is marked as ignored
356266
if (isIgnored(request.requestId)) {
357267
return
@@ -651,10 +561,6 @@ function normalizedUnhandledIpfsProtocol (request, pubGwUrl) {
651561
}
652562
}
653563

654-
function findHeaderIndex (name, headers) {
655-
return headers.findIndex(x => x.name && x.name.toLowerCase() === name.toLowerCase())
656-
}
657-
658564
// RECOVERY OF FAILED REQUESTS
659565
// ===================================================================
660566

add-on/src/lib/precache.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ const drain = require('pull-stream/sinks/drain')
55
const toStream = require('it-to-stream')
66
const tar = require('tar-stream')
77
const CID = require('cids')
8-
const { webuiCid } = require('./state')
98

109
const debug = require('debug')
1110
const log = debug('ipfs-companion:precache')
1211
log.error = debug('ipfs-companion:precache:error')
1312

13+
// Web UI release that should be precached
14+
// WARNING: do not remove this constant, as its used in package.json
15+
const webuiCid = 'Qmexhq2sBHnXQbvyP2GfUdbnY7HCagH2Mw5vUNSBn2nxip' // v2.7.2
16+
1417
const PRECACHE_ARCHIVES = [
1518
{ tarPath: '/dist/precache/webui.tar', cid: webuiCid }
1619
]

add-on/src/lib/state.js

-20
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44
const { safeURL } = require('./options')
55
const offlinePeerCount = -1
66

7-
// CID of a 'blessed' Web UI release
8-
// which should work without setting CORS headers
9-
const webuiCid = 'Qmexhq2sBHnXQbvyP2GfUdbnY7HCagH2Mw5vUNSBn2nxip' // v2.7.2
10-
117
function initState (options, overrides) {
128
// we store options and some pregenerated values to avoid async storage
139
// reads and minimize performance impact on overall browsing experience
@@ -29,21 +25,6 @@ function initState (options, overrides) {
2925
state.gwURLString = state.gwURL.toString()
3026
delete state.customGatewayUrl
3127
state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy
32-
state.webuiCid = webuiCid
33-
34-
// TODO: unify the way webui is opened
35-
// - https://github.com/ipfs-shipyard/ipfs-companion/pull/737
36-
// - https://github.com/ipfs-shipyard/ipfs-companion/pull/738
37-
// Context: previously, we loaded webui from gateway port
38-
// (`${state.gwURLString}ipfs/${state.webuiCid}/`) because API port
39-
// has hardcoded list of whitelisted webui versions.
40-
// To enable API access from webui loaded from Gateway port Companion
41-
// removed Origin header to avoid CORS, now we move away from that
42-
// complexity and for now just load version whitelisted on API port.
43-
// In the future, we want to load webui from $webuiCid.ipfs.localhost
44-
// and whitelist API access from that specific hostname
45-
// by appending it to API.HTTPHeaders.Access-Control-Allow-Origin list
46-
// When that is possible, we can remove Origin manipulation (see PR #737 for PoC)
4728
state.webuiRootUrl = `${state.apiURLString}webui/`
4829

4930
// attach helper functions
@@ -69,4 +50,3 @@ function initState (options, overrides) {
6950

7051
exports.initState = initState
7152
exports.offlinePeerCount = offlinePeerCount
72-
exports.webuiCid = webuiCid

package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"build:bundle-all": "cross-env RELEASE_CHANNEL=${RELEASE_CHANNEL:=dev} run-s bundle:chromium bundle:brave:$RELEASE_CHANNEL bundle:firefox:$RELEASE_CHANNEL",
3030
"build:rename-artifacts": "./scripts/rename-artifacts.js",
3131
"precache:clean": "shx rm -rf add-on/dist/precache",
32-
"precache:webui:cid": "shx grep 'const webuiCid' add-on/src/lib/state.js | shx sed \"s/^const webuiCid = '//\" | shx sed \"s/'.*$//\"",
32+
"precache:webui:cid": "shx grep 'const webuiCid' add-on/src/lib/precache.js | shx sed \"s/^const webuiCid = '//\" | shx sed \"s/'.*$//\"",
3333
"precache:webui": "shx mkdir -p add-on/dist/precache && ipfs-or-gateway -c $(npm run -s precache:webui:cid) -p add-on/dist/precache/webui.tar --archive",
3434
"bundle": "run-s bundle:*",
3535
"bundle:chromium": "run-s precache:webui && shx cat add-on/manifest.common.json add-on/manifest.chromium.json | json --deep-merge > add-on/manifest.json && web-ext build -a build/chromium && run-s build:rename-artifacts",
@@ -158,7 +158,6 @@
158158
"tachyons": "4.11.1",
159159
"tar-stream": "2.1.2",
160160
"timers-browserify-full": "0.0.1",
161-
"uri-to-multiaddr": "3.0.1",
162161
"webextension-polyfill": "0.6.0",
163162
"webrtc-ips": "0.1.4"
164163
},

test/functional/lib/ipfs-request-workarounds.test.js

+12-52
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,9 @@ describe('modifyRequest processing', function () {
105105
})
106106
})
107107

108-
describe('a request to <apiURL>/api/v0/ with Origin=moz-extension://{extension-installation-id}', function () {
109-
it('should remove Origin header with moz-extension://', async function () {
108+
describe('a request to <apiURL>/api/v0/ made with extension:// Origin', function () {
109+
it('should have it removed if Origin: moz-extension://{extension-installation-id}', async function () {
110+
// Context: Firefox 65 started setting this header
110111
// set vendor-specific Origin for WebExtension context
111112
browser.runtime.getURL.withArgs('/').returns('moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/')
112113
// ensure clean modifyRequest
@@ -125,8 +126,9 @@ describe('modifyRequest processing', function () {
125126
})
126127
})
127128

128-
describe('a request to <apiURL>/api/v0/ with Origin=chrome-extension://{extension-installation-id}', function () {
129+
describe('should have it removed if Origin: chrome-extension://{extension-installation-id}', function () {
129130
it('should remove Origin header with chrome-extension://', async function () {
131+
// Context: Chromium 72 started setting this header
130132
// set vendor-specific Origin for WebExtension context
131133
browser.runtime.getURL.withArgs('/').returns('chrome-extension://trolrorlrorlrol/')
132134
// ensure clean modifyRequest
@@ -146,65 +148,23 @@ describe('modifyRequest processing', function () {
146148
})
147149

148150
describe('a request to <apiURL>/api/v0/ with Origin=null', function () {
149-
it('should remove Origin header ', async function () {
150-
// set vendor-specific Origin for WebExtension context
151+
it('should keep the "Origin: null" header ', async function () {
152+
// Presence of Origin header is important as it protects API from XSS via sandboxed iframe
153+
// NOTE: Chromium <72 was setting this header in requests sent by browser extension,
154+
// but they fixed it since then.
151155
browser.runtime.getURL.withArgs('/').returns(undefined)
152156
// ensure clean modifyRequest
153157
runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
154158
modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
155159
// test
156-
const bogusOriginHeader = { name: 'Origin', value: 'null' }
160+
const nullOriginHeader = { name: 'Origin', value: 'null' }
157161
const request = {
158-
requestHeaders: [bogusOriginHeader],
162+
requestHeaders: [nullOriginHeader],
159163
type: 'xmlhttprequest',
160164
url: `${state.apiURLString}api/v0/id`
161165
}
162166
modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
163-
expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders).to.not.include(bogusOriginHeader)
164-
browser.runtime.getURL.flush()
165-
})
166-
})
167-
168-
// Web UI is loaded from hardcoded 'blessed' CID, which enables us to remove
169-
// CORS limitation. This makes Web UI opened from browser action work without
170-
// the need for any additional configuration of go-ipfs daemon
171-
describe('a request to API from blessed webuiRootUrl', function () {
172-
it('should pass without CORS limitations ', async function () {
173-
// ensure clean modifyRequest
174-
runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
175-
modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
176-
// test
177-
const webuiOriginHeader = { name: 'Origin', value: state.webuiRootUrl }
178-
const webuiRefererHeader = { name: 'Referer', value: state.webuiRootUrl }
179-
// CORS whitelisting does not worh in Chrome 72 without passing/restoring ACRH preflight header
180-
const acrhHeader = { name: 'Access-Control-Request-Headers', value: 'X-Test' } // preflight to store
181-
182-
// Test request
183-
let request = {
184-
requestHeaders: [webuiOriginHeader, webuiRefererHeader, acrhHeader],
185-
type: 'xmlhttprequest',
186-
originUrl: state.webuiRootUrl,
187-
url: `${state.apiURLString}api/v0/id`
188-
}
189-
request = modifyRequest.onBeforeRequest(request) || request // executes before onBeforeSendHeaders, may mutate state
190-
const requestHeaders = modifyRequest.onBeforeSendHeaders(request).requestHeaders
191-
192-
// "originUrl" should be swapped to look like it came from the same origin as HTTP API
193-
const expectedOriginUrl = state.webuiRootUrl.replace(state.gwURLString, state.apiURLString)
194-
expect(requestHeaders).to.deep.include({ name: 'Origin', value: expectedOriginUrl })
195-
expect(requestHeaders).to.deep.include({ name: 'Referer', value: expectedOriginUrl })
196-
expect(requestHeaders).to.deep.include(acrhHeader)
197-
198-
// Test response
199-
const response = Object.assign({}, request)
200-
delete response.requestHeaders
201-
response.responseHeaders = []
202-
const responseHeaders = modifyRequest.onHeadersReceived(response).responseHeaders
203-
const corsHeader = { name: 'Access-Control-Allow-Origin', value: state.gwURL.origin }
204-
const acahHeader = { name: 'Access-Control-Allow-Headers', value: acrhHeader.value } // expect value restored from preflight
205-
expect(responseHeaders).to.deep.include(corsHeader)
206-
expect(responseHeaders).to.deep.include(acahHeader)
207-
167+
expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders).to.include(nullOriginHeader)
208168
browser.runtime.getURL.flush()
209169
})
210170
})

yarn.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -14965,7 +14965,7 @@ uri-js@^4.2.2:
1496514965
dependencies:
1496614966
punycode "^2.1.0"
1496714967

14968-
uri-to-multiaddr@3.0.1, uri-to-multiaddr@^3.0.1:
14968+
uri-to-multiaddr@^3.0.1:
1496914969
version "3.0.1"
1497014970
resolved "https://registry.yarnpkg.com/uri-to-multiaddr/-/uri-to-multiaddr-3.0.1.tgz#460bd5d78074002c47b60fdc456efd009e7168ae"
1497114971
integrity sha512-77slJiNB/IxM35zgflBEgp8T8ywpyYAbEh8Ezdnq7kAuA6TRg6wfvNTi4Uixfh6CsPv9K2fAkI5+E4C2dw3tXA==

0 commit comments

Comments
 (0)