Skip to content

Commit 707fa29

Browse files
committed
refactor: use webui provided by IPFS node
This changes the way "Open Web UI" menu option works. Before: we kept CID of latest version and opened it from gateway port + executed a lot of magic to ensure it can talk to api port in secure way. After: we removed all the magic and just open the version provided on api port. Details: #736
1 parent 346d9d0 commit 707fa29

File tree

5 files changed

+62
-202
lines changed

5 files changed

+62
-202
lines changed

add-on/src/lib/ipfs-client/index.js

+19-36
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ const debug = require('debug')
66
const log = debug('ipfs-companion:client')
77
log.error = debug('ipfs-companion:client:error')
88

9-
const browser = require('webextension-polyfill')
109
const external = require('./external')
1110
const embedded = require('./embedded')
1211
const embeddedWithChromeSockets = require('./embedded-chromesockets')
13-
const { webuiCid } = require('../state')
1412

1513
let client
1614

@@ -33,7 +31,7 @@ async function initIpfsClient (opts) {
3331

3432
const instance = await client.init(opts)
3533
easeApiChanges(instance)
36-
_reloadIpfsClientDependents(instance) // async (API is present)
34+
preloadWebui(instance, opts)
3735
return instance
3836
}
3937

@@ -43,43 +41,28 @@ async function destroyIpfsClient () {
4341
await client.destroy()
4442
} finally {
4543
client = null
46-
await _reloadIpfsClientDependents() // sync (API stopped working)
4744
}
4845
}
4946
}
5047

51-
function _isWebuiTab (url) {
52-
const bundled = !url.startsWith('http') && url.includes('/webui/index.html#/')
53-
const ipns = url.includes('/webui.ipfs.io/#/')
54-
return bundled || ipns
55-
}
56-
57-
async function _reloadIpfsClientDependents (instance, opts) {
58-
// online || offline
59-
if (browser.tabs && browser.tabs.query) {
60-
const tabs = await browser.tabs.query({})
61-
if (tabs) {
62-
tabs.forEach((tab) => {
63-
// detect bundled webui in any of open tabs
64-
if (_isWebuiTab(tab.url)) {
65-
browser.tabs.reload(tab.id)
66-
log('reloading bundled webui')
67-
}
68-
})
69-
}
70-
}
71-
// online only
72-
if (client && instance) {
73-
if (webuiCid && instance.refs) {
74-
// Optimization: preload the root CID to speed up the first time
75-
// Web UI is opened. If embedded js-ipfs is used it will trigger
76-
// remote (always recursive) preload of entire DAG to one of preload nodes.
77-
// This way when embedded node wants to load resource related to webui
78-
// it will get it fast from preload nodes.
79-
log(`preloading webui root at ${webuiCid}`)
80-
instance.refs(webuiCid, { recursive: false })
81-
}
82-
}
48+
function preloadWebui (instance, opts) {
49+
// run only when client still exists and async fetch is possible
50+
if (!(client && instance && opts.webuiRootUrl && typeof fetch === 'function')) return
51+
// Optimization: preload the root CID to speed up the first time
52+
// Web UI is opened. If embedded js-ipfs is used it will trigger
53+
// remote (always recursive) preload of entire DAG to one of preload nodes.
54+
// This way when embedded node wants to load resource related to webui
55+
// it will get it fast from preload nodes.
56+
const webuiUrl = opts.webuiRootUrl
57+
log(`preloading webui root at ${webuiUrl}`)
58+
return fetch(webuiUrl, { redirect: 'follow' })
59+
.then(response => {
60+
const webuiPath = new URL(response.url).pathname
61+
log(`preloaded webui root at ${webuiPath}`)
62+
// trigger recursive remote preload in js-ipfs
63+
instance.refs(webuiPath, { recursive: false })
64+
})
65+
.catch(err => log.error(`failed to preload webui root`, err))
8366
}
8467

8568
const movedFilesApis = ['add', 'addPullStream', 'addReadableStream', 'cat', 'catPullStream', 'catReadableStream', 'get', 'getPullStream', 'getReadableStream']

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

+5-15
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 { optionDefaults, storeMissingOptions, migrateOptions } = require('./options')
1110
const { initState, offlinePeerCount } = require('./state')
1211
const { createIpfsPathValidator } = require('./ipfs-path')
@@ -98,7 +97,8 @@ module.exports = async function init () {
9897
function registerListeners () {
9998
const onBeforeSendInfoSpec = ['blocking', 'requestHeaders']
10099
if (!runtime.isFirefox) {
101-
// Chrome 72+ requires 'extraHeaders' for access to Referer header (used in cors whitelisting of webui)
100+
// Chrome 72+ requires 'extraHeaders' for accessing all headers
101+
// Note: we need this for code ensuring ipfs-http-client can talk to API without setting CORS
102102
onBeforeSendInfoSpec.push('extraHeaders')
103103
}
104104
browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, { urls: ['<all_urls>'] }, onBeforeSendInfoSpec)
@@ -419,13 +419,15 @@ module.exports = async function init () {
419419
// See: https://github.com/ipfs/ipfs-companion/issues/286
420420
try {
421421
// pass the URL of user-preffered public gateway
422+
// TOOD: plan to remove this
422423
await browser.tabs.executeScript(details.tabId, {
423424
code: `window.ipfsCompanionPubGwURL = '${state.pubGwURLString}'`,
424425
matchAboutBlank: false,
425426
allFrames: true,
426427
runAt: 'document_start'
427428
})
428429
// inject script that normalizes `href` and `src` containing unhandled protocols
430+
// TOOD: add deprecation warning and plan to remove this
429431
await browser.tabs.executeScript(details.tabId, {
430432
file: '/dist/bundles/normalizeLinksContentScript.bundle.js',
431433
matchAboutBlank: false,
@@ -436,18 +438,6 @@ module.exports = async function init () {
436438
console.error(`Unable to normalize links at '${details.url}' due to`, error)
437439
}
438440
}
439-
if (details.url.startsWith(state.webuiRootUrl)) {
440-
// Ensure API backend points at one from IPFS Companion
441-
const apiMultiaddr = toMultiaddr(state.apiURLString)
442-
await browser.tabs.executeScript(details.tabId, {
443-
runAt: 'document_start',
444-
code: `if (!localStorage.getItem('ipfsApi')) {
445-
console.log('[ipfs-companion] Setting API to ${apiMultiaddr}');
446-
localStorage.setItem('ipfsApi', '${apiMultiaddr}');
447-
window.location.reload();
448-
}`
449-
})
450-
}
451441
}
452442

453443
// API STATUS UPDATES
@@ -643,6 +633,7 @@ module.exports = async function init () {
643633
case 'ipfsApiUrl':
644634
state.apiURL = new URL(change.newValue)
645635
state.apiURLString = state.apiURL.toString()
636+
state.webuiRootUrl = `${state.apiURLString}webui`
646637
shouldRestartIpfsClient = true
647638
break
648639
case 'ipfsApiPollMs':
@@ -651,7 +642,6 @@ module.exports = async function init () {
651642
case 'customGatewayUrl':
652643
state.gwURL = new URL(change.newValue)
653644
state.gwURLString = state.gwURL.toString()
654-
state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/`
655645
break
656646
case 'publicGatewayUrl':
657647
state.pubGwURL = new URL(change.newValue)

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

+30-90
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,6 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
3434
const ignoredRequests = new LRU(requestCacheCfg)
3535
const ignore = (id) => ignoredRequests.set(id, true)
3636
const isIgnored = (id) => ignoredRequests.get(id) !== undefined
37-
38-
const acrhHeaders = new LRU(requestCacheCfg) // webui cors fix in Chrome
39-
const originUrls = new LRU(requestCacheCfg) // request.originUrl workaround for Chrome
40-
const originUrl = (request) => {
41-
// Firefox and Chrome provide relevant value in different fields:
42-
// (Firefox) request object includes full URL of origin document, return as-is
43-
if (request.originUrl) return request.originUrl
44-
// (Chrome) is lacking: `request.initiator` is just the origin (protocol+hostname+port)
45-
// To reconstruct originUrl we read full URL from Referer header in onBeforeSendHeaders
46-
// and cache it for short time
47-
// TODO: when request.originUrl is available in Chrome the `originUrls` cache can be removed
48-
const cachedUrl = originUrls.get(request.requestId)
49-
if (cachedUrl) return cachedUrl
50-
if (request.requestHeaders) {
51-
const referer = request.requestHeaders.find(h => h.name === 'Referer')
52-
if (referer) {
53-
originUrls.set(request.requestId, referer.value)
54-
return referer.value
55-
}
56-
}
57-
}
58-
5937
const preNormalizationSkip = (state, request) => {
6038
// skip requests to the custom gateway or API (otherwise we have too much recursion)
6139
if (request.url.startsWith(state.gwURLString) || request.url.startsWith(state.apiURLString)) {
@@ -161,46 +139,23 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
161139

162140
// Special handling of requests made to API
163141
if (request.url.startsWith(state.apiURLString)) {
164-
// Requests made by 'blessed' Web UI
165-
// --------------------------------------------
166-
// Goal: Web UI works without setting CORS at go-ipfs
167-
// (Without this snippet go-ipfs will return HTTP 403 due to additional origin check on the backend)
168-
const origin = originUrl(request)
169-
if (origin && origin.startsWith(state.webuiRootUrl)) {
170-
// console.log('onBeforeSendHeaders', request)
171-
// console.log('onBeforeSendHeaders.origin', origin)
172-
// Swap Origin to pass server-side check
173-
// (go-ipfs returns HTTP 403 on origin mismatch if there are no CORS headers)
174-
const swapOrigin = (at) => {
175-
request.requestHeaders[at].value = request.requestHeaders[at].value.replace(state.gwURL.origin, state.apiURL.origin)
176-
}
177-
let foundAt = request.requestHeaders.findIndex(h => h.name === 'Origin')
178-
if (foundAt > -1) swapOrigin(foundAt)
179-
foundAt = request.requestHeaders.findIndex(h => h.name === 'Referer')
180-
if (foundAt > -1) swapOrigin(foundAt)
181-
182-
// Save access-control-request-headers from preflight
183-
foundAt = request.requestHeaders.findIndex(h => h.name && h.name.toLowerCase() === 'access-control-request-headers')
184-
if (foundAt > -1) {
185-
acrhHeaders.set(request.requestId, request.requestHeaders[foundAt].value)
186-
// console.log('onBeforeSendHeaders FOUND access-control-request-headers', acrhHeaders.get(request.requestId))
187-
}
188-
// console.log('onBeforeSendHeaders fixed headers', request.requestHeaders)
189-
}
190-
191142
// '403 - Forbidden' fix for Chrome and Firefox
192143
// --------------------------------------------
193-
// We remove Origin header from requests made to API URL and WebUI
194-
// by js-ipfs-http-client running in WebExtension context to remove need
195-
// for manual CORS whitelisting via Access-Control-Allow-Origin at go-ipfs
144+
// We remove Origin header from requests made to API URL
145+
// by js-ipfs-http-client running in WebExtension context.
146+
// This act as unification of CORS behavior across all vendors,
147+
// where behavior was non-deterministic and changed between releases.
148+
// Without this, some users would need to do manual CORS whitelisting
149+
// by adding webExtensionOrigin to API.Access-Control-Allow-Origin at their IPFS node.
196150
// More info:
197151
// Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/622
198152
// Chromium 71: https://github.com/ipfs-shipyard/ipfs-companion/pull/616
199153
// Chromium 72: https://github.com/ipfs-shipyard/ipfs-companion/issues/630
200154
const isWebExtensionOrigin = (origin) => {
201155
// console.log(`origin=${origin}, webExtensionOrigin=${webExtensionOrigin}`)
202-
// Chromium <= 71 returns opaque Origin as defined in
156+
// Chromium <72 returns opaque Origin as defined in
203157
// https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin
158+
// TODO: remove this when <72 is not used by users
204159
if (origin == null || origin === 'null') {
205160
return true
206161
}
@@ -274,41 +229,6 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
274229
return
275230
}
276231

277-
// Special handling of requests made to API
278-
if (request.url.startsWith(state.apiURLString)) {
279-
// Special handling of requests made by 'blessed' Web UI from local Gateway
280-
// Goal: Web UI works without setting CORS at go-ipfs
281-
// (This includes 'ignored' requests: CORS needs to be fixed even if no redirect is done)
282-
const origin = originUrl(request)
283-
if (origin && origin.startsWith(state.webuiRootUrl) && request.responseHeaders) {
284-
// console.log('onHeadersReceived', request)
285-
const acaOriginHeader = { name: 'Access-Control-Allow-Origin', value: state.gwURL.origin }
286-
const foundAt = findHeaderIndex(acaOriginHeader.name, request.responseHeaders)
287-
if (foundAt > -1) {
288-
request.responseHeaders[foundAt].value = acaOriginHeader.value
289-
} else {
290-
request.responseHeaders.push(acaOriginHeader)
291-
}
292-
293-
// Restore access-control-request-headers from preflight
294-
const acrhValue = acrhHeaders.get(request.requestId)
295-
if (acrhValue) {
296-
const acahHeader = { name: 'Access-Control-Allow-Headers', value: acrhValue }
297-
const foundAt = findHeaderIndex(acahHeader.name, request.responseHeaders)
298-
if (foundAt > -1) {
299-
request.responseHeaders[foundAt].value = acahHeader.value
300-
} else {
301-
request.responseHeaders.push(acahHeader)
302-
}
303-
acrhHeaders.del(request.requestId)
304-
// console.log('onHeadersReceived SET Access-Control-Allow-Headers', header)
305-
}
306-
307-
// console.log('onHeadersReceived fixed headers', request.responseHeaders)
308-
return { responseHeaders: request.responseHeaders }
309-
}
310-
}
311-
312232
// Skip if request is marked as ignored
313233
if (isIgnored(request.requestId)) {
314234
return
@@ -317,6 +237,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
317237
if (state.redirect) {
318238
// Late redirect as a workaround for edge cases such as:
319239
// - CORS XHR in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
240+
// TODO: remove when Firefox with a fix landed in Stable channel
320241
if (onHeadersReceivedRedirect.has(request.requestId)) {
321242
onHeadersReceivedRedirect.delete(request.requestId)
322243
if (state.dnslinkPolicy) {
@@ -529,6 +450,25 @@ function normalizedUnhandledIpfsProtocol (request, pubGwUrl) {
529450
}
530451
}
531452

532-
function findHeaderIndex (name, headers) {
533-
return headers.findIndex(x => x.name && x.name.toLowerCase() === name.toLowerCase())
453+
/* not used at the moment, but this heuristic may be useful in the future
454+
// Note: Chrome 72+ requires 'extraHeaders' for access to Referer header
455+
const originUrls = new LRU(requestCacheCfg) // request.originUrl workaround for Chrome
456+
const originUrl = (request) => {
457+
// Firefox and Chrome provide relevant value in different fields:
458+
// (Firefox) request object includes full URL of origin document, return as-is
459+
if (request.originUrl) return request.originUrl
460+
// (Chrome) is lacking: `request.initiator` is just the origin (protocol+hostname+port)
461+
// To reconstruct originUrl we read full URL from Referer header in onBeforeSendHeaders
462+
// and cache it for short time
463+
// TODO: when request.originUrl is available in Chrome the `originUrls` cache can be removed
464+
const cachedUrl = originUrls.get(request.requestId)
465+
if (cachedUrl) return cachedUrl
466+
if (request.requestHeaders) {
467+
const referer = request.requestHeaders.find(h => h.name === 'Referer')
468+
if (referer) {
469+
originUrls.set(request.requestId, referer.value)
470+
return referer.value
471+
}
472+
}
534473
}
474+
*/

add-on/src/lib/state.js

+1-7
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 = 'QmYTRvKFGhxgBiUreiw7ihn8g95tfJTWDt7aXqDsvAcJse' // v2.4.7
10-
117
function initState (options) {
128
// we store options and some pregenerated values to avoid async storage
139
// reads and minimize performance impact on overall browsing experience
@@ -26,11 +22,9 @@ function initState (options) {
2622
state.gwURLString = state.gwURL.toString()
2723
delete state.customGatewayUrl
2824
state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy
29-
state.webuiCid = webuiCid
30-
state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/`
25+
state.webuiRootUrl = `${state.apiURLString}webui`
3126
return state
3227
}
3328

3429
exports.initState = initState
3530
exports.offlinePeerCount = offlinePeerCount
36-
exports.webuiCid = webuiCid

0 commit comments

Comments
 (0)