Skip to content

Commit

Permalink
tests: updates related to subdomain gateways
Browse files Browse the repository at this point in the history
  • Loading branch information
lidel committed Mar 27, 2020
1 parent e9a34a7 commit f948443
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 53 deletions.
4 changes: 2 additions & 2 deletions add-on/src/lib/dnslink.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ module.exports = function createDnslinkResolver (getState) {
// to load the correct path from IPFS
// - https://github.com/ipfs/ipfs-companion/issues/298
const ipnsPath = dnslinkResolver.convertToIpnsPath(url)
const gateway = state.ipfsNodeType === 'embedded' ? state.pubGwURLString : state.gwURLString
const gateway = state.localGwAvailable ? state.gwURLString : state.pubGwURLString
return pathAtHttpGateway(ipnsPath, gateway)
}
},
Expand Down Expand Up @@ -110,7 +110,7 @@ module.exports = function createDnslinkResolver (getState) {
preloadUrlCache.set(url, true)
const dnslink = await dnslinkResolver.resolve(url)
if (!dnslink) return
if (state.ipfsNodeType === 'embedded') return
if (!state.localGwAvailable) return
if (state.peerCount < 1) return
return preloadQueue.add(async () => {
const { pathname } = new URL(url)
Expand Down
33 changes: 22 additions & 11 deletions add-on/src/lib/http-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,31 @@ log.error = debug('ipfs-companion:http-proxy:error')
// registerSubdomainProxy is necessary wourkaround for supporting subdomains
// under 'localhost' (*.ipfs.localhost) because some operating systems do not
// resolve them to local IP and return NX error not found instead
async function registerSubdomainProxy (getState, runtime) {
const { useSubdomainProxy: enable, gwURLString } = getState()
async function registerSubdomainProxy (getState, runtime, notify) {
try {
const { useSubdomainProxy: enable, gwURLString } = getState()

// HTTP Proxy feature is exposed on the gateway port
// Just ensure we use localhost IP to remove any dependency on DNS
const proxy = safeURL(gwURLString, { useLocalhostName: false })
// HTTP Proxy feature is exposed on the gateway port
// Just ensure we use localhost IP to remove any dependency on DNS
const proxy = safeURL(gwURLString, { useLocalhostName: false })

// Firefox uses own APIs for selective proxying
if (runtime.isFirefox) {
return registerSubdomainProxyFirefox(enable, proxy.hostname, proxy.port)
}
// Firefox uses own APIs for selective proxying
if (runtime.isFirefox) {
return await registerSubdomainProxyFirefox(enable, proxy.hostname, proxy.port)
}

// at this point we asume Chromium
return registerSubdomainProxyChromium(enable, proxy.host)
// at this point we asume Chromium
return await registerSubdomainProxyChromium(enable, proxy.host)
} catch (err) {
// registerSubdomainProxy is just a failsafe, not necessary in most cases,
// so we should not break init when it fails.
// For now we just log error and exit as NOOP
log.error('registerSubdomainProxy failed', err)
// Show pop-up only the first time, during init() when notify is passed
try {
if (notify) notify('notify_addonIssueTitle', 'notify_addonIssueMsg')
} catch (_) {}
}
}

// storing listener for later
Expand Down
6 changes: 3 additions & 3 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ module.exports = async function init () {
ipfsProxyContentScript = await registerIpfsProxyContentScript()
log('register all listeners')
registerListeners()
await registerSubdomainProxy(getState, runtime)
await setApiStatusUpdateInterval(options.ipfsApiPollMs)
await registerSubdomainProxy(getState, runtime, notify)
log('init done')
await showPendingLandingPages()
} catch (error) {
Expand Down Expand Up @@ -324,7 +324,7 @@ module.exports = async function init () {
}
ipfsImportHandler.copyShareLink(result)
ipfsImportHandler.preloadFilesAtPublicGateway(result)
if (state.ipfsNodeType === 'embedded' || !state.openViaWebUI) {
if (!state.localGwAvailable || !state.openViaWebUI) {
return ipfsImportHandler.openFilesAtGateway({ result, openRootInNewTab: true })
} else {
return ipfsImportHandler.openFilesAtWebUI(importDir)
Expand Down Expand Up @@ -557,7 +557,7 @@ module.exports = async function init () {
// enable/disable gw redirect based on API going online or offline
// newPeerCount === -1 currently implies node is offline.
// TODO: use `node.isOnline()` if available (js-ipfs)
if (state.automaticMode && state.ipfsNodeType !== 'embedded') {
if (state.automaticMode && state.localGwAvailable) {
if (oldPeerCount === offlinePeerCount && newPeerCount > offlinePeerCount && !state.redirect) {
browser.storage.local.set({ useCustomGateway: true })
.then(() => notify('notify_apiOnlineTitle', 'notify_apiOnlineAutomaticModeMsg'))
Expand Down
54 changes: 32 additions & 22 deletions add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
}

const postNormalizationSkip = (state, request) => {
// skip requests to the public gateway if embedded node is running (otherwise we have too much recursion)
if (state.ipfsNodeType === 'embedded' && sameGateway(request.url, state.pubGwURL)) {
// skip requests to the public gateway if we can't reedirect them to local
// node is running (otherwise we have too much recursion)
if (!state.localGwAvailable && sameGateway(request.url, state.pubGwURL)) {
ignore(request.requestId)
// TODO: do not skip and redirect to `ipfs://` and `ipns://` if hasNativeProtocolHandler === true
}
Expand Down Expand Up @@ -351,7 +352,9 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
// All the following requests will be upgraded to IPNS
const cachedDnslink = dnslinkResolver.readAndCacheDnslink(new URL(request.url).hostname)
const redirectUrl = dnslinkResolver.dnslinkAtGateway(request.url, cachedDnslink)
if (redirectUrl) {
// redirect only if local node is around, as we can't guarantee DNSLink support
// at a public subdomain gateway (requires more than 1 level of wildcard TLS certs)
if (redirectUrl && state.localGwAvailable) {
log(`onHeadersReceived: dnslinkRedirect from ${request.url} to ${redirectUrl}`)
return { redirectUrl }
}
Expand All @@ -371,8 +374,8 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
const url = new URL(request.url)
const pathWithArgs = `${xIpfsPath}${url.search}${url.hash}`
const newUrl = pathAtHttpGateway(pathWithArgs, state.pubGwURLString)
// redirect only if anything changed
if (newUrl !== request.url) {
// redirect only if local node is around
if (newUrl && state.localGwAvailable) {
log(`onHeadersReceived: normalized ${request.url} to ${newUrl}`)
return redirectToGateway(request, newUrl, state, ipfsPathValidator)
}
Expand Down Expand Up @@ -407,9 +410,10 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
if (isRecoverableViaEthDNS(request, state)) {
const url = new URL(request.url)
url.hostname = `${url.hostname}.link`
const redirect = { redirectUrl: url.toString() }
log(`onErrorOccurred: attempting to recover from DNS error (${request.error}) using EthDNS for ${request.url}${redirect.redirectUrl}`, request)
return createTabWithURL(redirect, browser, recoveredTabs)
const redirectUrl = url.toString()
log(`onErrorOccurred: attempting to recover from DNS error (${request.error}) using EthDNS for ${request.url}${redirectUrl}`, request)
// TODO: update existing tab
return createTabWithURL(request, redirectUrl, browser, recoveredTabs)
}

// Check if error can be recovered via DNSLink
Expand All @@ -419,7 +423,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
if (dnslink) {
const redirectUrl = dnslinkResolver.dnslinkAtGateway(request.url, dnslink)
log(`onErrorOccurred: attempting to recover from network error (${request.error}) using dnslink for ${request.url}${redirectUrl}`, request)
return createTabWithURL({ redirectUrl }, browser, recoveredTabs)
return createTabWithURL(request, redirectUrl, browser, recoveredTabs)
}
}

Expand All @@ -433,7 +437,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
if (isRecoverable(request, state, ipfsPathValidator)) {
const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url)
log(`onErrorOccurred: attempting to recover from network error (${request.error}) for ${request.url}${redirectUrl}`, request)
return createTabWithURL({ redirectUrl }, browser, recoveredTabs)
return createTabWithURL(request, redirectUrl, browser, recoveredTabs)
}
},

Expand Down Expand Up @@ -463,7 +467,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
if (isRecoverable(request, state, ipfsPathValidator)) {
const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url)
log(`onCompleted: attempting to recover from HTTP Error ${request.statusCode} for ${request.url}${redirectUrl}`, request)
return createTabWithURL({ redirectUrl }, browser, recoveredTabs)
return createTabWithURL(request, redirectUrl, browser, recoveredTabs)
}
}
}
Expand All @@ -474,11 +478,8 @@ exports.createRequestModifier = createRequestModifier

function redirectToGateway (request, url, state, ipfsPathValidator) {
const { resolveToPublicUrl, resolveToLocalUrl } = ipfsPathValidator
const useLocal = state.ipfsNodeType !== 'embedded'
const redirectUrl = useLocal ? resolveToLocalUrl(url) : resolveToPublicUrl(url)
const redirectUrl = state.localGwAvailable ? resolveToLocalUrl(url) : resolveToPublicUrl(url)
// redirect only if we actually change anything
console.log('request.url', request.url)
console.log('redirectUrl', redirectUrl)
if (redirectUrl && request.url !== redirectUrl) return { redirectUrl }
}

Expand Down Expand Up @@ -588,11 +589,17 @@ function findHeaderIndex (name, headers) {

// Recovery check for onErrorOccurred (request.error) and onCompleted (request.statusCode)
function isRecoverable (request, state, ipfsPathValidator) {
return state.recoverFailedHttpRequests &&
// Note: we are unable to recover default public gateways without a local one
const { error, statusCode, url } = request
const { redirect, localGwAvailable, pubGwURL, pubSubdomainGwURL } = state
return (state.recoverFailedHttpRequests &&
request.type === 'main_frame' &&
(recoverableNetworkErrors.has(request.error) || recoverableHttpError(request.statusCode)) &&
(ipfsPathValidator.publicIpfsOrIpnsResource(request.url) || isIPFS.subdomain(request.url)) &&
!sameGateway(request.url, state.pubGwURL) && !sameGateway(request.url, state.pubSubdomainGwURL)
(recoverableNetworkErrors.has(error) ||
recoverableHttpError(statusCode)) &&
ipfsPathValidator.publicIpfsOrIpnsResource(url) &&
((redirect && localGwAvailable) ||
(!sameGateway(url, pubGwURL) &&
!sameGateway(url, pubSubdomainGwURL))))
}

// Recovery check for onErrorOccurred (request.error)
Expand All @@ -616,8 +623,11 @@ function isRecoverableViaEthDNS (request, state) {
// We can't redirect in onErrorOccurred/onCompleted
// Indead, we recover by opening URL in a new tab that replaces the failed one
// TODO: display an user-friendly prompt when the very first recovery is done
async function createTabWithURL (redirect, browser, recoveredTabs) {
const tabKey = redirect.redirectUrl
async function createTabWithURL (request, redirectUrl, browser, recoveredTabs) {
// Do nothing if the URL remains the same
if (request.url === redirectUrl) return

const tabKey = redirectUrl
// reuse existing tab, if exists
// (this avoids duplicated tabs - https://github.com/ipfs-shipyard/ipfs-companion/issues/805)
try {
Expand All @@ -635,7 +645,7 @@ async function createTabWithURL (redirect, browser, recoveredTabs) {
const newTab = await browser.tabs.create({
active: true,
openerTabId,
url: redirect.redirectUrl
url: redirectUrl
})
if (newTab) recoveredTabs.set(tabKey, newTab.id)
}
8 changes: 6 additions & 2 deletions add-on/src/lib/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ function initState (options, overrides) {
return false
}
}
state.isNodeConnected = () => state.peerCount > 0
state.isNodeActive = () => state.peerCount > offlinePeerCount
// TODO state.connected ~= state.peerCount > 0
// TODO state.nodeActive ~= API is online,eg. state.peerCount > offlinePeerCount
Object.defineProperty(state, 'localGwAvailable', {
// TODO: make quick fetch to confirm it works?
get: function () { return this.ipfsNodeType !== 'embedded' }
})
// apply optional overrides
if (overrides) Object.assign(state, overrides)
return state
Expand Down
8 changes: 4 additions & 4 deletions test/functional/lib/dnslink.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ describe('dnslinkResolver (dnslinkPolicy=detectIpfsPathHeader)', function () {
dnslinkPolicy: 'detectIpfsPathHeader',
peerCount: 1
})
const getExternalNodeState = () => Object.assign({}, getState(), { ipfsNodeType: 'external' })
const getEmbeddedNodeState = () => Object.assign({}, getState(), { ipfsNodeType: 'embedded' })
const getExternalNodeState = () => Object.assign(getState(), { ipfsNodeType: 'external' })
const getEmbeddedNodeState = () => Object.assign(getState(), { ipfsNodeType: 'embedded' })

describe('dnslinkAtGateway(url)', function () {
['/api/v0/foo', '/ipfs/foo', '/ipns/foo'].forEach(path => {
Expand Down Expand Up @@ -149,8 +149,8 @@ describe('dnslinkResolver (dnslinkPolicy=enabled)', function () {
dnslinkPolicy: 'enabled',
peerCount: 1
})
const getExternalNodeState = () => Object.assign({}, getState(), { ipfsNodeType: 'external' })
const getEmbeddedNodeState = () => Object.assign({}, getState(), { ipfsNodeType: 'embedded' })
const getExternalNodeState = () => Object.assign(getState(), { ipfsNodeType: 'external' })
const getEmbeddedNodeState = () => Object.assign(getState(), { ipfsNodeType: 'embedded' })

describe('dnslinkAtGateway(url)', function () {
['/api/v0/foo', '/ipfs/foo', '/ipns/foo'].forEach(path => {
Expand Down
16 changes: 11 additions & 5 deletions test/functional/lib/ipfs-request-gateway-recover.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,21 @@ describe('requestHandler.onCompleted:', function () { // HTTP-level errors
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
})
it('should do nothing if broken request is a non-public IPFS request to 127.0.0.1', async function () {
it('should do nothing if broken request is a local request to 127.0.0.1/ipfs', async function () {
const request = urlRequestWithStatus('http://127.0.0.1:8080/ipfs/QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
})
it('should do nothing if broken request is a non-public IPFS request to localhost', async function () {
it('should do nothing if broken request is a local request to localhost/ipfs', async function () {
const request = urlRequestWithStatus('http://localhost:8080/ipfs/QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
})
it('should do nothing if broken request is a local request to *.ipfs.localhost', async function () {
const request = urlRequestWithStatus('http://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhw.ipfs.localhost:8080/', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
})
it('should do nothing if broken request is to the default public gateway', async function () {
const request = urlRequestWithStatus('https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
await requestHandler.onCompleted(request)
Expand Down Expand Up @@ -176,13 +181,14 @@ describe('requestHandler.onErrorOccurred:', function () { // network errors
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.withArgs({ url: 'https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', active: true, openerTabId: 20 }).calledOnce, 'tabs.create should be called with IPFS default public gateway URL')
})
it('should recover from unreachable HTTP server by reopening DNSLink on the public gateway', async function () {
it('should recover from unreachable HTTP server by reopening DNSLink on the active gateway', async function () {
state.dnslinkPolicy = 'best-effort'
dnslinkResolver.setDnslink('en.wikipedia-on-ipfs.org', '/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco')
const expectedUrl = 'http://127.0.0.1:8080/ipns/en.wikipedia-on-ipfs.org/'
// avoid DNS failures when recovering to local gateweay (if available)
const expectedUrl = 'http://localhost:8080/ipns/en.wikipedia-on-ipfs.org/'
const request = urlRequestWithNetworkError('https://en.wikipedia-on-ipfs.org/')
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.withArgs({ url: expectedUrl, active: true, openerTabId: 20 }).calledOnce, 'tabs.create should be called with ENS resource on local gateway URL')
assert.ok(browser.tabs.create.withArgs({ url: expectedUrl, active: true, openerTabId: 20 }).calledOnce, 'tabs.create should be called with DNSLink on local gateway URL')
dnslinkResolver.clearCache()
})
it('should recover from failed DNS for .eth opening it on EthDNS gateway at .eth.link', async function () {
Expand Down
Loading

0 comments on commit f948443

Please sign in to comment.