Skip to content

Commit

Permalink
refactor: use webui provided by IPFS node
Browse files Browse the repository at this point in the history
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
  • Loading branch information
lidel committed Jul 18, 2019
1 parent 346d9d0 commit 707fa29
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 202 deletions.
55 changes: 19 additions & 36 deletions add-on/src/lib/ipfs-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ const debug = require('debug')
const log = debug('ipfs-companion:client')
log.error = debug('ipfs-companion:client:error')

const browser = require('webextension-polyfill')
const external = require('./external')
const embedded = require('./embedded')
const embeddedWithChromeSockets = require('./embedded-chromesockets')
const { webuiCid } = require('../state')

let client

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

const instance = await client.init(opts)
easeApiChanges(instance)
_reloadIpfsClientDependents(instance) // async (API is present)
preloadWebui(instance, opts)
return instance
}

Expand All @@ -43,43 +41,28 @@ async function destroyIpfsClient () {
await client.destroy()
} finally {
client = null
await _reloadIpfsClientDependents() // sync (API stopped working)
}
}
}

function _isWebuiTab (url) {
const bundled = !url.startsWith('http') && url.includes('/webui/index.html#/')
const ipns = url.includes('/webui.ipfs.io/#/')
return bundled || ipns
}

async function _reloadIpfsClientDependents (instance, opts) {
// online || offline
if (browser.tabs && browser.tabs.query) {
const tabs = await browser.tabs.query({})
if (tabs) {
tabs.forEach((tab) => {
// detect bundled webui in any of open tabs
if (_isWebuiTab(tab.url)) {
browser.tabs.reload(tab.id)
log('reloading bundled webui')
}
})
}
}
// online only
if (client && instance) {
if (webuiCid && instance.refs) {
// Optimization: preload the root CID to speed up the first time
// Web UI is opened. If embedded js-ipfs is used it will trigger
// remote (always recursive) preload of entire DAG to one of preload nodes.
// This way when embedded node wants to load resource related to webui
// it will get it fast from preload nodes.
log(`preloading webui root at ${webuiCid}`)
instance.refs(webuiCid, { recursive: false })
}
}
function preloadWebui (instance, opts) {
// run only when client still exists and async fetch is possible
if (!(client && instance && opts.webuiRootUrl && typeof fetch === 'function')) return
// Optimization: preload the root CID to speed up the first time
// Web UI is opened. If embedded js-ipfs is used it will trigger
// remote (always recursive) preload of entire DAG to one of preload nodes.
// This way when embedded node wants to load resource related to webui
// it will get it fast from preload nodes.
const webuiUrl = opts.webuiRootUrl
log(`preloading webui root at ${webuiUrl}`)
return fetch(webuiUrl, { redirect: 'follow' })
.then(response => {
const webuiPath = new URL(response.url).pathname
log(`preloaded webui root at ${webuiPath}`)
// trigger recursive remote preload in js-ipfs
instance.refs(webuiPath, { recursive: false })
})
.catch(err => log.error(`failed to preload webui root`, err))
}

const movedFilesApis = ['add', 'addPullStream', 'addReadableStream', 'cat', 'catPullStream', 'catReadableStream', 'get', 'getPullStream', 'getReadableStream']
Expand Down
20 changes: 5 additions & 15 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const log = debug('ipfs-companion:main')
log.error = debug('ipfs-companion:main:error')

const browser = require('webextension-polyfill')
const toMultiaddr = require('uri-to-multiaddr')
const { optionDefaults, storeMissingOptions, migrateOptions } = require('./options')
const { initState, offlinePeerCount } = require('./state')
const { createIpfsPathValidator } = require('./ipfs-path')
Expand Down Expand Up @@ -98,7 +97,8 @@ module.exports = async function init () {
function registerListeners () {
const onBeforeSendInfoSpec = ['blocking', 'requestHeaders']
if (!runtime.isFirefox) {
// Chrome 72+ requires 'extraHeaders' for access to Referer header (used in cors whitelisting of webui)
// Chrome 72+ requires 'extraHeaders' for accessing all headers
// Note: we need this for code ensuring ipfs-http-client can talk to API without setting CORS
onBeforeSendInfoSpec.push('extraHeaders')
}
browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, { urls: ['<all_urls>'] }, onBeforeSendInfoSpec)
Expand Down Expand Up @@ -419,13 +419,15 @@ module.exports = async function init () {
// See: https://github.com/ipfs/ipfs-companion/issues/286
try {
// pass the URL of user-preffered public gateway
// TOOD: plan to remove this
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
// TOOD: add deprecation warning and plan to remove this
await browser.tabs.executeScript(details.tabId, {
file: '/dist/bundles/normalizeLinksContentScript.bundle.js',
matchAboutBlank: false,
Expand All @@ -436,18 +438,6 @@ module.exports = async function init () {
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
Expand Down Expand Up @@ -643,6 +633,7 @@ module.exports = async function init () {
case 'ipfsApiUrl':
state.apiURL = new URL(change.newValue)
state.apiURLString = state.apiURL.toString()
state.webuiRootUrl = `${state.apiURLString}webui`
shouldRestartIpfsClient = true
break
case 'ipfsApiPollMs':
Expand All @@ -651,7 +642,6 @@ 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)
Expand Down
120 changes: 30 additions & 90 deletions add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,6 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
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
const 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)) {
Expand Down Expand Up @@ -161,46 +139,23 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru

// 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 and WebUI
// by js-ipfs-http-client running in WebExtension context to remove need
// for manual CORS whitelisting via Access-Control-Allow-Origin at go-ipfs
// We remove Origin header from requests made to API URL
// by js-ipfs-http-client running in WebExtension context.
// This act as unification of CORS behavior across all vendors,
// where behavior was non-deterministic and changed between releases.
// Without this, some users would need to do manual CORS whitelisting
// by adding webExtensionOrigin to API.Access-Control-Allow-Origin at their IPFS node.
// More info:
// Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/622
// Chromium 71: https://github.com/ipfs-shipyard/ipfs-companion/pull/616
// Chromium 72: https://github.com/ipfs-shipyard/ipfs-companion/issues/630
const isWebExtensionOrigin = (origin) => {
// console.log(`origin=${origin}, webExtensionOrigin=${webExtensionOrigin}`)
// Chromium <= 71 returns opaque Origin as defined in
// Chromium <72 returns opaque Origin as defined in
// https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin
// TODO: remove this when <72 is not used by users
if (origin == null || origin === 'null') {
return true
}
Expand Down Expand Up @@ -274,41 +229,6 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
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
Expand All @@ -317,6 +237,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
if (state.redirect) {
// Late redirect as a workaround for edge cases such as:
// - CORS XHR in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
// TODO: remove when Firefox with a fix landed in Stable channel
if (onHeadersReceivedRedirect.has(request.requestId)) {
onHeadersReceivedRedirect.delete(request.requestId)
if (state.dnslinkPolicy) {
Expand Down Expand Up @@ -529,6 +450,25 @@ function normalizedUnhandledIpfsProtocol (request, pubGwUrl) {
}
}

function findHeaderIndex (name, headers) {
return headers.findIndex(x => x.name && x.name.toLowerCase() === name.toLowerCase())
/* not used at the moment, but this heuristic may be useful in the future
// Note: Chrome 72+ requires 'extraHeaders' for access to Referer header
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
const 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
}
}
}
*/
8 changes: 1 addition & 7 deletions add-on/src/lib/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
const { safeURL } = require('./options')
const offlinePeerCount = -1

// CID of a 'blessed' Web UI release
// which should work without setting CORS headers
const webuiCid = 'QmYTRvKFGhxgBiUreiw7ihn8g95tfJTWDt7aXqDsvAcJse' // v2.4.7

function initState (options) {
// we store options and some pregenerated values to avoid async storage
// reads and minimize performance impact on overall browsing experience
Expand All @@ -26,11 +22,9 @@ function initState (options) {
state.gwURLString = state.gwURL.toString()
delete state.customGatewayUrl
state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy
state.webuiCid = webuiCid
state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/`
state.webuiRootUrl = `${state.apiURLString}webui`
return state
}

exports.initState = initState
exports.offlinePeerCount = offlinePeerCount
exports.webuiCid = webuiCid
Loading

0 comments on commit 707fa29

Please sign in to comment.