Skip to content

Commit fb5958a

Browse files
authored
fix: restore ESR compatibility (#812)
- changes minimal Firefox version to 68 - restores CORS workaround when runtime is Firefox <69. This makes Companion once again compatible with Firefox ESR and Fennec-based Firefox for Android. Closes #784 Closes #779
1 parent 1d6b2a9 commit fb5958a

7 files changed

+130
-22
lines changed

.babelrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"@babel/preset-env",
88
{
99
"targets": {
10-
"firefox": 69,
10+
"firefox": 68,
1111
"chrome": 72
1212
}
1313
}

add-on/manifest.firefox.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"applications": {
99
"gecko": {
1010
11-
"strict_min_version": "69.0"
11+
"strict_min_version": "68.0"
1212
}
1313
},
1414
"page_action": {

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

+35
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const recoverableNetworkErrors = new Set([
2424
])
2525
const recoverableHttpError = (code) => code && code >= 400
2626

27+
// Tracking late redirects for edge cases such as https://github.com/ipfs-shipyard/ipfs-companion/issues/436
28+
const onHeadersReceivedRedirect = new Set()
29+
2730
// Request modifier provides event listeners for the various stages of making an HTTP request
2831
// API Details: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest
2932
function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, runtime) {
@@ -311,6 +314,19 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
311314
}
312315

313316
if (state.redirect) {
317+
// Late redirect as a workaround for edge cases such as:
318+
// - CORS XHR in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
319+
if (runtime.requiresXHRCORSfix && onHeadersReceivedRedirect.has(request.requestId)) {
320+
onHeadersReceivedRedirect.delete(request.requestId)
321+
if (state.dnslinkPolicy) {
322+
const dnslinkRedirect = dnslinkResolver.dnslinkRedirect(request.url)
323+
if (dnslinkRedirect) {
324+
return dnslinkRedirect
325+
}
326+
}
327+
return redirectToGateway(request.url, state, ipfsPathValidator)
328+
}
329+
314330
// Detect X-Ipfs-Path Header and upgrade transport to IPFS:
315331
// 1. Check if DNSLink exists and redirect to it.
316332
// 2. If there is no DNSLink, validate path from the header and redirect
@@ -404,6 +420,11 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
404420
}
405421
}
406422

423+
// Cleanup after https://github.com/ipfs-shipyard/ipfs-companion/issues/436
424+
if (runtime.requiresXHRCORSfix && onHeadersReceivedRedirect.has(request.requestId)) {
425+
onHeadersReceivedRedirect.delete(request.requestId)
426+
}
427+
407428
// Check if error can be recovered by opening same content-addresed path
408429
// using active gateway (public or local, depending on redirect state)
409430
if (isRecoverable(request, state, ipfsPathValidator)) {
@@ -463,6 +484,20 @@ function isSafeToRedirect (request, runtime) {
463484
return false
464485
}
465486

487+
// Ignore XHR requests for which redirect would fail due to CORS bug in Firefox
488+
// See: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
489+
if (runtime.requiresXHRCORSfix && request.type === 'xmlhttprequest' && !request.responseHeaders) {
490+
// Sidenote on XHR Origin: Firefox 60 uses request.originUrl, Chrome 63 uses request.initiator
491+
if (request.originUrl) {
492+
const sourceOrigin = new URL(request.originUrl).origin
493+
const targetOrigin = new URL(request.url).origin
494+
if (sourceOrigin !== targetOrigin) {
495+
log('Delaying redirect of CORS XHR until onHeadersReceived due to https://github.com/ipfs-shipyard/ipfs-companion/issues/436 :', request.url)
496+
onHeadersReceivedRedirect.add(request.requestId)
497+
return false
498+
}
499+
}
500+
}
466501
return true
467502
}
468503

add-on/src/lib/runtime-checks.js

+11-12
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ function getBrowserInfo (browser) {
66
if (browser && browser.runtime && browser.runtime.getBrowserInfo) {
77
return browser.runtime.getBrowserInfo()
88
}
9-
return Promise.resolve()
9+
return Promise.resolve({})
1010
}
1111

1212
function getPlatformInfo (browser) {
@@ -28,21 +28,20 @@ function hasChromeSocketsForTcp () {
2828

2929
async function createRuntimeChecks (browser) {
3030
// browser
31-
const browserInfo = await getBrowserInfo(browser)
32-
const runtimeBrowserName = browserInfo ? browserInfo.name : 'unknown'
33-
const runtimeIsFirefox = !!(runtimeBrowserName.includes('Firefox') || runtimeBrowserName.includes('Fennec'))
34-
const runtimeHasNativeProtocol = !!(browser && browser.protocol && browser.protocol.registerStringProtocol)
31+
const { name, version } = await getBrowserInfo(browser)
32+
const isFirefox = name && (name.includes('Firefox') || name.includes('Fennec'))
33+
const hasNativeProtocolHandler = !!(browser && browser.protocol && browser.protocol.registerStringProtocol)
3534
// platform
3635
const platformInfo = await getPlatformInfo(browser)
37-
const runtimeIsAndroid = platformInfo ? platformInfo.os === 'android' : false
38-
const runtimeHasSocketsForTcp = hasChromeSocketsForTcp()
36+
const isAndroid = platformInfo ? platformInfo.os === 'android' : false
3937
return Object.freeze({
4038
browser,
41-
isFirefox: runtimeIsFirefox,
42-
isAndroid: runtimeIsAndroid,
43-
isBrave: runtimeHasSocketsForTcp, // TODO: make it more robust
44-
hasChromeSocketsForTcp: runtimeHasSocketsForTcp,
45-
hasNativeProtocolHandler: runtimeHasNativeProtocol
39+
isFirefox,
40+
isAndroid,
41+
isBrave: hasChromeSocketsForTcp(), // TODO: make it more robust
42+
requiresXHRCORSfix: !!(isFirefox && version && version.startsWith('68')),
43+
hasChromeSocketsForTcp: hasChromeSocketsForTcp(),
44+
hasNativeProtocolHandler
4645
})
4746
}
4847

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

+27
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,20 @@ describe('modifyRequest processing', function () {
183183
const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
184184
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
185185
})
186+
it('should redirect later in onHeadersReceived if dnslink exists, XHR is cross-origin and runtime is Firefox <69', function () {
187+
// stub existence of a valid DNS record
188+
const fqdn = 'explore.ipld.io'
189+
dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
190+
//
191+
// Context for CORS XHR problems in Firefox <69: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
192+
runtime.requiresXHRCORSfix = true
193+
// Firefox uses 'originUrl' for origin
194+
const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
195+
// onBeforeRequest should not change anything, as it will trigger false-positive CORS error
196+
expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
197+
// onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
198+
expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
199+
})
186200
it('should do nothing if dnslink does not exist and XHR is cross-origin in Firefox', function () {
187201
// stub no dnslink
188202
const fqdn = 'youtube.com'
@@ -286,6 +300,19 @@ describe('modifyRequest processing', function () {
286300
xhrRequest.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
287301
expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
288302
})
303+
it('should redirect later in onHeadersReceived if XHR is cross-origin and runtime is Firefox <69', function () {
304+
// stub existence of a valid DNS record
305+
const fqdn = 'explore.ipld.io'
306+
dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
307+
//
308+
// Context for CORS XHR problems in Firefox <69: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
309+
runtime.requiresXHRCORSfix = true
310+
const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
311+
// onBeforeRequest should not change anything, as it will trigger false-positive CORS error
312+
expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
313+
// onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
314+
expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
315+
})
289316
// Test makes more sense for dnslinkPolicy=enabled, but we keep it here for completeness
290317
it('should do nothing if DNS TXT record is missing and XHR is cross-origin in Firefox', function () {
291318
// stub no dnslink

test/functional/lib/ipfs-request-gateway-redirect.test.js

+30
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,36 @@ describe('modifyRequest.onBeforeRequest:', function () {
160160
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
161161
})
162162
})
163+
describe('with external node when runtime.requiresXHRCORSfix', function () {
164+
beforeEach(function () {
165+
state.ipfsNodeType = 'external'
166+
browser.runtime.getBrowserInfo = () => Promise.resolve({ name: 'Firefox', version: '68.0.0' })
167+
})
168+
it('should be served from custom gateway if fetched from the same origin and redirect is enabled in Firefox', function () {
169+
runtime.isFirefox = true
170+
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://google.com/' }
171+
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
172+
})
173+
it('should be served from custom gateway if fetched from the same origin and redirect is enabled in non-Firefox', function () {
174+
runtime.isFirefox = false
175+
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://google.com/' }
176+
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
177+
})
178+
it('should be served from custom gateway if XHR is cross-origin and redirect is enabled in non-Firefox', function () {
179+
runtime.isFirefox = false
180+
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
181+
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
182+
})
183+
it('should be served from custom gateway via late redirect in onHeadersReceived if XHR is cross-origin and redirect is enabled in Firefox', function () {
184+
// Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
185+
runtime.isFirefox = true
186+
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
187+
// onBeforeRequest should not change anything, as it will trigger false-positive CORS error
188+
expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
189+
// onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
190+
expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
191+
})
192+
})
163193
})
164194

165195
describe('request for a path matching /ipns/{path}', function () {

test/functional/lib/runtime-checks.test.js

+25-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { describe, it, before, beforeEach, after } = require('mocha')
55
const { expect } = require('chai')
66
const browser = require('sinon-chrome')
77
const { createRuntimeChecks, hasChromeSocketsForTcp } = require('../../../add-on/src/lib/runtime-checks')
8+
const promiseStub = (result) => () => Promise.resolve(result)
89

910
describe('runtime-checks.js', function () {
1011
before(() => {
@@ -21,14 +22,6 @@ describe('runtime-checks.js', function () {
2122
browser.flush()
2223
})
2324

24-
// current version of sinon-chrome is missing stubs for some APIs,
25-
// this is a workaround until fix is provided upstream
26-
function promiseStub (result) {
27-
return () => {
28-
return Promise.resolve(result)
29-
}
30-
}
31-
3225
it('should return true when in Firefox runtime', async function () {
3326
browser.runtime.getBrowserInfo = promiseStub({ name: 'Firefox' })
3427
const runtime = await createRuntimeChecks(browser)
@@ -42,6 +35,30 @@ describe('runtime-checks.js', function () {
4235
})
4336
})
4437

38+
describe('requiresXHRCORSfix', function () {
39+
beforeEach(function () {
40+
browser.flush()
41+
})
42+
43+
it('should return true when in Firefox runtime < 69', async function () {
44+
browser.runtime.getBrowserInfo = promiseStub({ name: 'Firefox', version: '68.0.0' })
45+
const runtime = await createRuntimeChecks(browser)
46+
expect(runtime.requiresXHRCORSfix).to.equal(true)
47+
})
48+
49+
it('should return false when in Firefox runtime >= 69', async function () {
50+
browser.runtime.getBrowserInfo = promiseStub({ name: 'Firefox', version: '69.0.0' })
51+
const runtime = await createRuntimeChecks(browser)
52+
expect(runtime.requiresXHRCORSfix).to.equal(false)
53+
})
54+
55+
it('should return false when if getBrowserInfo is not present', async function () {
56+
browser.runtime.getBrowserInfo = undefined
57+
const runtime = await createRuntimeChecks(browser)
58+
expect(runtime.requiresXHRCORSfix).to.equal(false)
59+
})
60+
})
61+
4562
describe('isAndroid', function () {
4663
beforeEach(function () {
4764
browser.flush()

0 commit comments

Comments
 (0)