Skip to content

Commit

Permalink
refactor: remove code for "blessing" custom webui
Browse files Browse the repository at this point in the history
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
  • Loading branch information
lidel committed Apr 5, 2020
1 parent eb66dfa commit a94e46b
Show file tree
Hide file tree
Showing 8 changed files with 59 additions and 210 deletions.
8 changes: 7 additions & 1 deletion add-on/src/lib/dnslink.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,13 @@ module.exports = function createDnslinkResolver (getState) {
let apiProvider
// TODO: fix DNS resolver for ipfsNodeType='embedded:chromesockets', for now use ipfs.io
if (!state.ipfsNodeType.startsWith('embedded') && state.peerCount !== offlinePeerCount) {
apiProvider = state.apiURLString
// Use gw port so it can be a GET:
// Chromium does not execute onBeforeSendHeaders for synchronous calls
// made from the same extension context as onBeforeSendHeaders
// which means we are unable to fixup Origin on the fly for this
// This will no longer be needed when we switch
// to async lookup via ipfs.dns everywhere
apiProvider = state.gwURLString
} else {
// fallback to resolver at public gateway
apiProvider = 'https://ipfs.io/'
Expand Down
19 changes: 3 additions & 16 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 pMemoize = require('p-memoize')
const { optionDefaults, storeMissingOptions, migrateOptions, guiURLString } = require('./options')
const { initState, offlinePeerCount } = require('./state')
Expand Down Expand Up @@ -107,7 +106,8 @@ module.exports = async function init () {
function registerListeners () {
const onBeforeSendInfoSpec = ['blocking', 'requestHeaders']
if (browser.webRequest.OnBeforeSendHeadersOptions && 'EXTRA_HEADERS' in browser.webRequest.OnBeforeSendHeadersOptions) {
// 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 @@ -385,18 +385,6 @@ module.exports = async function init () {
log.error(`Unable to linkify DOM 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 @@ -615,6 +603,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 @@ -623,8 +612,6 @@ module.exports = async function init () {
case 'customGatewayUrl':
state.gwURL = new URL(change.newValue)
state.gwURLString = state.gwURL.toString()
// TODO: for now we load webui from API port, should we remove this?
// state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/`
break
case 'publicGatewayUrl':
state.pubGwURL = new URL(change.newValue)
Expand Down
137 changes: 23 additions & 114 deletions add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,6 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
const isIgnored = (id) => ignoredRequests.get(id) !== undefined
const errorInFlight = new LRU({ max: 3, maxAge: 1000 })

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
}
}
}

// Returns a canonical hostname representing the site from url
// Main reason for this is unwrapping DNSLink from local subdomain
// <fqdn>.ipns.localhost → <fqdn>
Expand Down Expand Up @@ -208,63 +187,34 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru

// Special handling of requests made to API
if (sameGateway(request.url, state.apiURL)) {
// 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)
}

const { requestHeaders } = request
// '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 update "Origin: *-extension://" HTTP headers in requests made to API
// by js-ipfs-http-client running in the background page of browser
// extension. Without this, some users would need to do manual CORS
// whitelisting by adding "..extension://<UUID>" to
// API.HTTPHeaders.Access-Control-Allow-Origin in go-ipfs config.
// With this, API calls made by browser extension look like ones made
// by webui loaded from the API port.
// 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
// https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin
if (origin == null || origin === 'null') {
return true
}
// Firefox Nightly 65 sets moz-extension://{extension-installation-id}
// Chromium Beta 72 sets chrome-extension://{uid}
if (origin &&

// Firefox Nightly 65 sets moz-extension://{extension-installation-id}
// Chromium Beta 72 sets chrome-extension://{uid}
const isWebExtensionOrigin = (origin) =>
origin &&
(origin.startsWith('moz-extension://') ||
origin.startsWith('chrome-extension://')) &&
new URL(origin).origin === webExtensionOrigin) {
return true
}
return false
}
origin.startsWith('chrome-extension://')) &&
new URL(origin).origin === webExtensionOrigin

// 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)
// Replace Origin header matching webExtensionOrigin with API one
const foundAt = requestHeaders.findIndex(h => h.name === 'Origin' && isWebExtensionOrigin(h.value))
if (foundAt > -1) {
requestHeaders[foundAt].value = state.apiURL.origin
}

// Fix "http: invalid Read on closed Body"
// ----------------------------------
Expand All @@ -277,7 +227,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
let addExpectHeader = true
const expectHeader = { name: 'Expect', value: '100-continue' }
const warningMsg = 'Executing "Expect: 100-continue" workaround for ipfs.add due to https://github.com/ipfs/go-ipfs/issues/5168'
for (const header of request.requestHeaders) {
for (const header of requestHeaders) {
// Workaround A: https://github.com/ipfs/go-ipfs/issues/5168#issuecomment-401417420
// (works in Firefox, but Chromium does not expose Connection header)
/* (disabled so we use the workaround B in all browsers)
Expand All @@ -301,12 +251,10 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
}
if (addExpectHeader) {
log(warningMsg)
request.requestHeaders.push(expectHeader)
requestHeaders.push(expectHeader)
}
}
}
return {
requestHeaders: request.requestHeaders
return { requestHeaders }
}
},

Expand All @@ -317,41 +265,6 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
const state = getState()
if (!state.active) return

// Special handling of requests made to API
if (sameGateway(request.url, state.apiURL)) {
// 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 Down Expand Up @@ -651,10 +564,6 @@ function normalizedUnhandledIpfsProtocol (request, pubGwUrl) {
}
}

function findHeaderIndex (name, headers) {
return headers.findIndex(x => x.name && x.name.toLowerCase() === name.toLowerCase())
}

// RECOVERY OF FAILED REQUESTS
// ===================================================================

Expand Down
5 changes: 4 additions & 1 deletion add-on/src/lib/precache.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ const drain = require('pull-stream/sinks/drain')
const toStream = require('it-to-stream')
const tar = require('tar-stream')
const CID = require('cids')
const { webuiCid } = require('./state')

const debug = require('debug')
const log = debug('ipfs-companion:precache')
log.error = debug('ipfs-companion:precache:error')

// Web UI release that should be precached
// WARNING: do not remove this constant, as its used in package.json
const webuiCid = 'Qmexhq2sBHnXQbvyP2GfUdbnY7HCagH2Mw5vUNSBn2nxip' // v2.7.2

const PRECACHE_ARCHIVES = [
{ tarPath: '/dist/precache/webui.tar', cid: webuiCid }
]
Expand Down
20 changes: 0 additions & 20 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 = 'Qmexhq2sBHnXQbvyP2GfUdbnY7HCagH2Mw5vUNSBn2nxip' // v2.7.2

function initState (options, overrides) {
// we store options and some pregenerated values to avoid async storage
// reads and minimize performance impact on overall browsing experience
Expand All @@ -29,21 +25,6 @@ function initState (options, overrides) {
state.gwURLString = state.gwURL.toString()
delete state.customGatewayUrl
state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy
state.webuiCid = webuiCid

// TODO: unify the way webui is opened
// - https://github.com/ipfs-shipyard/ipfs-companion/pull/737
// - https://github.com/ipfs-shipyard/ipfs-companion/pull/738
// Context: previously, we loaded webui from gateway port
// (`${state.gwURLString}ipfs/${state.webuiCid}/`) because API port
// has hardcoded list of whitelisted webui versions.
// To enable API access from webui loaded from Gateway port Companion
// removed Origin header to avoid CORS, now we move away from that
// complexity and for now just load version whitelisted on API port.
// In the future, we want to load webui from $webuiCid.ipfs.localhost
// and whitelist API access from that specific hostname
// by appending it to API.HTTPHeaders.Access-Control-Allow-Origin list
// When that is possible, we can remove Origin manipulation (see PR #737 for PoC)
state.webuiRootUrl = `${state.apiURLString}webui/`

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

exports.initState = initState
exports.offlinePeerCount = offlinePeerCount
exports.webuiCid = webuiCid
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"build:bundle-all": "cross-env RELEASE_CHANNEL=${RELEASE_CHANNEL:=dev} run-s bundle:chromium bundle:brave:$RELEASE_CHANNEL bundle:firefox:$RELEASE_CHANNEL",
"build:rename-artifacts": "./scripts/rename-artifacts.js",
"precache:clean": "shx rm -rf add-on/dist/precache",
"precache:webui:cid": "shx grep 'const webuiCid' add-on/src/lib/state.js | shx sed \"s/^const webuiCid = '//\" | shx sed \"s/'.*$//\"",
"precache:webui:cid": "shx grep 'const webuiCid' add-on/src/lib/precache.js | shx sed \"s/^const webuiCid = '//\" | shx sed \"s/'.*$//\"",
"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",
"bundle": "run-s bundle:*",
"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",
Expand Down Expand Up @@ -158,7 +158,6 @@
"tachyons": "4.11.1",
"tar-stream": "2.1.2",
"timers-browserify-full": "0.0.1",
"uri-to-multiaddr": "3.0.1",
"webextension-polyfill": "0.6.0",
"webrtc-ips": "0.1.4"
},
Expand Down
Loading

0 comments on commit a94e46b

Please sign in to comment.