Skip to content

Commit

Permalink
fix: mixed-content on HTTP pages
Browse files Browse the repository at this point in the history
Firefox 74 does not mark *.localhost subdomains as Secure Context yet
(https://bugzilla.mozilla.org/show_bug.cgi?id=1220810#c23) so we can't redirect
there when we have IPFS resource embedded on HTTPS page (eg.  image loaded from
a public gateway) because that would cause mixed-content warning and
subresource would fail to load.  Given the fact that localhost/ipfs/* provided
by go-ipfs 0.5+ returns a redirect to *.ipfs.localhost subdomain we need to
check requests for subresources, and manually replace 'localhost' hostname with
'127.0.0.1' (IP is hardcoded as Secure Context in Firefox). The need for this
workaround can be revisited when Firefox closes mentioned bug.

Chromium 80 seems to force HTTPS in the final URL (after all redirects) so
https://*.localhost fails. This needs additional research (could be a bug in
Chromium). For now we reuse the same workaround as Firefox.

To unify use of 127.0.0.1 and localhost in address bar (eg. when user opens an
image in a new tab etc) when Subdomain Proxy is enabled we normalize address
bar requests made to the local gateway and replace raw IP with 'localhost'
hostname to take advantage of subdomain redirect provided by go-ipfs >= 0.5
  • Loading branch information
lidel committed Apr 3, 2020
1 parent 207fd76 commit 3e6708b
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 20 deletions.
1 change: 1 addition & 0 deletions add-on/src/lib/dnslink.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ module.exports = function createDnslinkResolver (getState) {
// in url.hostname OR in url.pathname (/ipns/<fqdn>)
// and return matching FQDN if present
findDNSLinkHostname (url) {
if (!url) return
// Normalize subdomain and path gateways to to /ipns/<fqdn>
const contentPath = ipfsContentPath(url)
if (IsIpfs.ipnsPath(contentPath)) {
Expand Down
4 changes: 2 additions & 2 deletions add-on/src/lib/ipfs-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function sameGateway (url, gwUrl) {
return url.hostname === gwUrl.hostname
}

const gws = [gwUrl.hostname]
const gws = [gwUrl.host]

// localhost gateway has more than one hostname
if (gwUrl.hostname === 'localhost') {
Expand All @@ -106,7 +106,7 @@ function sameGateway (url, gwUrl) {

for (const gwName of gws) {
// match against the end to include subdomain gateways
if (url.hostname.endsWith(gwName)) return true
if (url.host.endsWith(gwName)) return true
}
return false
}
Expand Down
54 changes: 48 additions & 6 deletions add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const LRU = require('lru-cache')
const isIPFS = require('is-ipfs')
const isFQDN = require('is-fqdn')
const { pathAtHttpGateway, sameGateway } = require('./ipfs-path')
const { safeURL } = require('./options')

const redirectOptOutHint = 'x-ipfs-companion-no-redirect'
const recoverableNetworkErrors = new Set([
Expand Down Expand Up @@ -142,6 +143,15 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
onBeforeRequest (request) {
const state = getState()
if (!state.active) return

// When Subdomain Proxy is enabled we normalize address bar requests made
// to the local gateway and replace raw IP with 'localhost' hostname to
// take advantage of subdomain redirect provided by go-ipfs >= 0.5
if (state.redirect && request.type === 'main_frame' && sameGateway(request.url, state.gwURL)) {
const redirectUrl = safeURL(request.url, { useLocalhostName: state.useSubdomainProxy }).toString()
if (redirectUrl !== request.url) return { redirectUrl }
}

// early sanity checks
if (preNormalizationSkip(state, request)) {
return
Expand Down Expand Up @@ -169,7 +179,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
}
// Detect valid /ipfs/ and /ipns/ on any site
if (ipfsPathValidator.publicIpfsOrIpnsResource(request.url) && isSafeToRedirect(request, runtime)) {
return redirectToGateway(request, request.url, state, ipfsPathValidator)
return redirectToGateway(request, request.url, state, ipfsPathValidator, runtime)
}
// Detect dnslink using heuristics enabled in Preferences
if (state.dnslinkPolicy && dnslinkResolver.canLookupURL(request.url)) {
Expand Down Expand Up @@ -358,7 +368,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
return { redirectUrl }
}
}
return redirectToGateway(request, request.url, state, ipfsPathValidator)
return redirectToGateway(request, request.url, state, ipfsPathValidator, runtime)
}

// Detect X-Ipfs-Path Header and upgrade transport to IPFS:
Expand Down Expand Up @@ -406,7 +416,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
// redirect only if local node is around
if (newUrl && state.localGwAvailable) {
log(`onHeadersReceived: normalized ${request.url} to ${newUrl}`)
return redirectToGateway(request, newUrl, state, ipfsPathValidator)
return redirectToGateway(request, newUrl, state, ipfsPathValidator, runtime)
}
}
}
Expand Down Expand Up @@ -505,10 +515,42 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
exports.redirectOptOutHint = redirectOptOutHint
exports.createRequestModifier = createRequestModifier

function redirectToGateway (request, url, state, ipfsPathValidator) {
// Returns a string with URL at the active gateway (local or public)
function redirectToGateway (request, url, state, ipfsPathValidator, runtime) {
const { resolveToPublicUrl, resolveToLocalUrl } = ipfsPathValidator
const redirectUrl = state.localGwAvailable ? resolveToLocalUrl(url) : resolveToPublicUrl(url)
// redirect only if we actually change anything
let redirectUrl = state.localGwAvailable ? resolveToLocalUrl(url) : resolveToPublicUrl(url)

// SUBRESOURCE ON HTTPS PAGE: THE WORKAROUND EXTRAVAGANZA
// ------------------------------------------------------ \o/
//
// Firefox 74 does not mark *.localhost subdomains as Secure Context yet
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1220810#c23) so we can't
// redirect there when we have IPFS resource embedded on HTTPS page (eg.
// image loaded from a public gateway) because that would cause mixed-content
// warning and subresource would fail to load. Given the fact that
// localhost/ipfs/* provided by go-ipfs 0.5+ returns a redirect to
// *.ipfs.localhost subdomain we need to check requests for subresources, and
// manually replace 'localhost' hostname with '127.0.0.1' (IP is hardcoded as
// Secure Context in Firefox). The need for this workaround can be revisited
// when Firefox closes mentioned bug.
//
// Chromium 80 seems to force HTTPS in the final URL (after all redirects) so
// https://*.localhost fails TODO: needs additional research (could be a bug
// in Chromium). For now we reuse the same workaround as Firefox.
//
if (state.localGwAvailable) {
const { type, originUrl, initiator } = request
// match request types for embedded subdresources, but skip ones coming from local gateway
const parentUrl = originUrl || initiator // FF || Chromium
if (type !== 'main_frame' && (parentUrl && !sameGateway(parentUrl, state.gwURL))) {
// use raw IP to ensure subresource will be loaded from the path gateway
// at 127.0.0.1, which is marked as Secure Context in all browsers
const useLocalhostName = false
redirectUrl = safeURL(redirectUrl, { useLocalhostName }).toString()
}
}

// return a redirect only if URL changed
if (redirectUrl && request.url !== redirectUrl) return { redirectUrl }
}

Expand Down
25 changes: 24 additions & 1 deletion test/functional/lib/ipfs-path.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { stub } = require('sinon')
const { describe, it, beforeEach, afterEach } = require('mocha')
const { expect } = require('chai')
const { URL } = require('url')
const { ipfsContentPath, createIpfsPathValidator } = require('../../../add-on/src/lib/ipfs-path')
const { ipfsContentPath, createIpfsPathValidator, sameGateway } = require('../../../add-on/src/lib/ipfs-path')
const { initState } = require('../../../add-on/src/lib/state')
const createDnslinkResolver = require('../../../add-on/src/lib/dnslink')
const { optionDefaults } = require('../../../add-on/src/lib/options')
Expand Down Expand Up @@ -102,6 +102,29 @@ describe('ipfs-path.js', function () {
})
})

describe('sameGateway', function () {
it('should return true on direct host match', function () {
const url = 'https://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
const gw = 'http://127.0.0.1:8080'
expect(sameGateway(url, gw)).to.equal(true)
})
it('should return true on localhost/127.0.0.1 host match', function () {
const url = 'https://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
const gw = 'http://127.0.0.1:8080'
expect(sameGateway(url, gw)).to.equal(true)
})
it('should return true on 127.0.0.1/localhost host match', function () {
const url = 'https://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
const gw = 'http://localhost:8080'
expect(sameGateway(url, gw)).to.equal(true)
})
it('should return false on hostname match but different port', function () {
const url = 'https://localhost:8081/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
const gw = 'http://localhost:8080'
expect(sameGateway(url, gw)).to.equal(false)
})
})

describe('validIpfsOrIpns', function () {
// this is just a smoke test, extensive tests are in is-ipfs package
it('should return true for IPFS NURI', function () {
Expand Down
32 changes: 21 additions & 11 deletions test/functional/lib/ipfs-request-gateway-redirect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,30 +111,30 @@ describe('modifyRequest.onBeforeRequest:', function () {
})
})

describe('XHR request for a path matching /ipfs/{CIDv0}', function () {
describe('XHR request for a path matching /ipfs/{CIDv0} coming from 3rd party Origin', function () {
describe('with external node', function () {
beforeEach(function () {
state.ipfsNodeType = 'external'
})
it('should be served from custom gateway if fetched from the same origin and redirect is enabled in Firefox', function () {
runtime.isFirefox = true
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://google.com/' }
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
})
it('should be served from custom gateway if fetched from the same origin and redirect is enabled in Chromium', function () {
runtime.isFirefox = false
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://google.com/' }
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
})
it('should be served from custom gateway if XHR is cross-origin and redirect is enabled in Chromium', function () {
runtime.isFirefox = false
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
})
it('should be served from custom gateway if XHR is cross-origin and redirect is enabled in Firefox', function () {
runtime.isFirefox = true
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
})
})
describe('with embedded node', function () {
Expand Down Expand Up @@ -170,17 +170,17 @@ describe('modifyRequest.onBeforeRequest:', function () {
it('should be served from custom gateway if fetched from the same origin and redirect is enabled in Firefox', function () {
runtime.isFirefox = true
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://google.com/' }
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
})
it('should be served from custom gateway if fetched from the same origin and redirect is enabled in non-Firefox', function () {
runtime.isFirefox = false
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://google.com/' }
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
})
it('should be served from custom gateway if XHR is cross-origin and redirect is enabled in non-Firefox', function () {
runtime.isFirefox = false
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
})
it('should be served from custom gateway via late redirect in onHeadersReceived if XHR is cross-origin and redirect is enabled in Firefox', function () {
// Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
Expand All @@ -189,7 +189,7 @@ describe('modifyRequest.onBeforeRequest:', function () {
// onBeforeRequest should not change anything, as it will trigger false-positive CORS error
expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
// onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
})
})
})
Expand Down Expand Up @@ -255,7 +255,7 @@ describe('modifyRequest.onBeforeRequest:', function () {
})
})

describe('request to a public subdomain gateway (CID in subdomain)', function () {
describe('request to a subdomain gateway', function () {
const cid = 'bafybeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge'
const peerid = 'bafzbeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge'

Expand Down Expand Up @@ -339,7 +339,7 @@ describe('modifyRequest.onBeforeRequest:', function () {
state.redirect = true
})
describe(`with ${nodeType} node:`, function () {
describe('request for IPFS path at a localhost', function () {
describe('request for IPFS path at the localhost', function () {
// we do not touch local requests, as it may interfere with other nodes running at the same machine
// or could produce false-positives such as redirection from localhost:5001/ipfs/path to localhost:8080/ipfs/path
it('should be left untouched if localhost is used', function () {
Expand All @@ -362,6 +362,16 @@ describe('modifyRequest.onBeforeRequest:', function () {
const request = url2request('http://[::1]:5001/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
expectNoRedirect(modifyRequest, request)
})
it('should be redirected to localhost (subdomain in go-ipfs >0.5) if type=main_frame and 127.0.0.1 (path gw) is used un URL', function () {
state.redirect = true
state.useSubdomainProxy = true
expect(state.gwURL.hostname).to.equal('localhost')
const cid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
const request = url2request(`http://127.0.0.1:8080/ipfs/${cid}?arg=val#hash`)
request.type = 'main_frame' // explicit
expect(modifyRequest.onBeforeRequest(request).redirectUrl)
.to.equal(`http://localhost:8080/ipfs/${cid}?arg=val#hash`)
})
})
})
})
Expand Down
50 changes: 50 additions & 0 deletions test/functional/lib/ipfs-request-workarounds.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,56 @@ describe('modifyRequest processing', function () {
modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
})

// Additional handling is required for redirected IPFS subresources on regular HTTPS pages
// (eg. image embedded from public gateway on HTTPS website)
describe('a subresource request on HTTPS website', function () {
const cid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
it('should be routed to "127.0.0.1" gw in Chromium if type is image', function () {
runtime.isFirefox = false
const request = {
method: 'GET',
type: 'image',
url: `https://ipfs.io/ipfs/${cid}`,
initiator: 'https://some-website.example.com' // Chromium
}
expect(modifyRequest.onBeforeRequest(request).redirectUrl)
.to.equal(`http://127.0.0.1:8080/ipfs/${cid}`)
})
it('should be routed to "localhost" gw in Chromium if not a subresource', function () {
runtime.isFirefox = false
const request = {
method: 'GET',
type: 'main_frame',
url: `https://ipfs.io/ipfs/${cid}`,
initiator: 'https://some-website.example.com' // Chromium
}
expect(modifyRequest.onBeforeRequest(request).redirectUrl)
.to.equal(`http://localhost:8080/ipfs/${cid}`)
})
it('should be routed to "127.0.0.1" gw to avoid mixed content warning in Firefox', function () {
runtime.isFirefox = true
const request = {
method: 'GET',
type: 'image',
url: `https://ipfs.io/ipfs/${cid}`,
originUrl: 'https://some-website.example.com/some/page.html' // FF only
}
expect(modifyRequest.onBeforeRequest(request).redirectUrl)
.to.equal(`http://127.0.0.1:8080/ipfs/${cid}`)
})
it('should be routed to "localhost" gw in Firefox if not a subresource', function () {
runtime.isFirefox = true
const request = {
method: 'GET',
type: 'main_frame',
url: `https://ipfs.io/ipfs/${cid}`,
originUrl: 'https://some-website.example.com/some/page.html' // FF only
}
expect(modifyRequest.onBeforeRequest(request).redirectUrl)
.to.equal(`http://localhost:8080/ipfs/${cid}`)
})
})

describe('a request to <apiURL>/api/v0/add with stream-channels=true', function () {
const expectHeader = { name: 'Expect', value: '100-continue' }
it('should apply the "Expect: 100-continue" fix for https://github.com/ipfs/go-ipfs/issues/5168 ', function () {
Expand Down

0 comments on commit 3e6708b

Please sign in to comment.