Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: recover from failed HTTP requests to third party gateways #783

Merged
merged 9 commits into from
Oct 17, 2019
8 changes: 8 additions & 0 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,14 @@
"message": "Check before HTTP request",
"description": "A select field option description on the Preferences screen (option_dnslinkPolicy_enabled)"
},
"option_recoverViaPublicGateway_title": {
"message": "Recover links via public gateway",
"description": "An option title on the Preferences screen (option_recoverViaPublicGateway_title)"
},
"option_recoverViaPublicGateway_description": {
"message": "Redirect broken public gateway requests to the default public gateway",
"description": "An option description on the Preferences screen (option_recoverViaPublicGateway_description)"
},
"option_detectIpfsPathHeader_title": {
"message": "Detect X-Ipfs-Path Header",
"description": "An option title on the Preferences screen (option_detectIpfsPathHeader_title)"
Expand Down
8 changes: 8 additions & 0 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ module.exports = async function init () {
browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, { urls: ['<all_urls>'] }, ['blocking'])
browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, { urls: ['<all_urls>'] }, ['blocking', 'responseHeaders'])
browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, { urls: ['<all_urls>'] })
browser.webRequest.onCompleted.addListener(onCompleted, { urls: ['<all_urls>'] })
browser.storage.onChanged.addListener(onStorageChange)
browser.webNavigation.onCommitted.addListener(onNavigationCommitted)
browser.webNavigation.onDOMContentLoaded.addListener(onDOMContentLoaded)
Expand Down Expand Up @@ -169,6 +170,10 @@ module.exports = async function init () {
return modifyRequest.onErrorOccurred(request)
}

function onCompleted (request) {
return modifyRequest.onCompleted(request)
}

// RUNTIME MESSAGES (one-off messaging)
// ===================================================================
// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/sendMessage
Expand Down Expand Up @@ -670,6 +675,9 @@ module.exports = async function init () {
await browser.storage.local.set({ detectIpfsPathHeader: true })
}
break
case 'recoverViaPublicGateway':
state[key] = change.newValue
break
case 'logNamespaces':
shouldReloadExtension = true
state[key] = localStorage.debug = change.newValue
Expand Down
83 changes: 70 additions & 13 deletions add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ const recoverableErrors = new Set([
'net::ERR_INTERNET_DISCONNECTED' // no network
])

const recoverableErrorCodes = new Set([
404,
408,
410,
415,
451,
500,
502,
503,
504,
509,
520,
521,
522,
523,
524,
525,
526
])

// Request modifier provides event listeners for the various stages of making an HTTP request
// API Details: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest
function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, runtime) {
Expand Down Expand Up @@ -380,29 +400,48 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
// console.log('onErrorOccurred:' + request.error)
// console.log('onErrorOccurred', request)
// Check if error is final and can be recovered via DNSLink
let redirect
const recoverableViaDnslink =
state.dnslinkPolicy &&
request.type === 'main_frame' &&
recoverableErrors.has(request.error)
if (recoverableViaDnslink && dnslinkResolver.canLookupURL(request.url)) {
// Explicit call to ignore global DNSLink policy and force DNS TXT lookup
const cachedDnslink = dnslinkResolver.readAndCacheDnslink(new URL(request.url).hostname)
const dnslinkRedirect = dnslinkResolver.dnslinkRedirect(request.url, cachedDnslink)
// We can't redirect in onErrorOccurred, so if DNSLink is present
// recover by opening IPNS version in a new tab
// TODO: add tests and demo
if (dnslinkRedirect) {
log(`onErrorOccurred: recovering using dnslink for ${request.url}`, dnslinkRedirect)
const currentTabId = await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0].id)
await browser.tabs.create({
active: true,
openerTabId: currentTabId,
url: dnslinkRedirect.redirectUrl
})
redirect = dnslinkResolver.dnslinkRedirect(request.url, cachedDnslink)
log(`onErrorOccurred: attempting to recover using dnslink for ${request.url}`, redirect)
}
// if error cannot be recovered via DNSLink
// direct the request to the public gateway
const recoverableViaPubGw = isRecoverableViaPubGw(request, state, ipfsPathValidator)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including the redirect functionality was necessary in both onCompleted and onErrorOccurred, because of the different types of gateway failures. Many requests result in failure, others return 4xx or 5xx error codes.

if (!redirect && recoverableViaPubGw) {
const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url, state.pubGwURLString)
redirect = { redirectUrl }
log(`onErrorOccurred: attempting to recover using public gateway for ${request.url}`, redirect)
}
// We can't redirect in onErrorOccurred, so if DNSLink is present
// recover by opening IPNS version in a new tab
// TODO: add tests and demo
if (redirect) {
createTabWithURL(redirect, browser)
}
},

async onCompleted (request) {
const state = getState()

const recoverableViaPubGw =
isRecoverableViaPubGw(request, state, ipfsPathValidator) &&
recoverableErrorCodes.has(request.statusCode)
if (recoverableViaPubGw) {
const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url, state.pubGwURLString)
const redirect = { redirectUrl }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we drop redirect and use redirectUrl directly?

Copy link
Contributor Author

@colinfruit colinfruit Oct 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is the cleanest way to do this. Without it we would have to worry about the dnslink redirect, and do something like const redirectUrl = redirect ? redirect.redirectUrl : null to be able to use this with the same createTabWithURL function.

if (redirect) {
log(`onErrorOccurred: attempting to recover using public gateway for ${request.url}`, redirect)
createTabWithURL(redirect, browser)
}
}
}

}
}

Expand Down Expand Up @@ -508,3 +547,21 @@ function normalizedUnhandledIpfsProtocol (request, pubGwUrl) {
function findHeaderIndex (name, headers) {
return headers.findIndex(x => x.name && x.name.toLowerCase() === name.toLowerCase())
}

// utility functions for handling redirects
// from onErrorOccurred and onCompleted
function isRecoverableViaPubGw (request, state, ipfsPathValidator) {
return state.recoverViaPublicGateway &&
ipfsPathValidator.publicIpfsOrIpnsResource(request.url) &&
!request.url.startsWith(state.pubGwURLString) &&
request.type === 'main_frame'
}

async function createTabWithURL (redirect, browser) {
const currentTabId = await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0].id)
await browser.tabs.create({
active: true,
openerTabId: currentTabId,
url: redirect.redirectUrl
})
}
1 change: 1 addition & 0 deletions add-on/src/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ exports.optionDefaults = Object.freeze({
automaticMode: true,
linkify: false,
dnslinkPolicy: 'best-effort',
recoverViaPublicGateway: false,
detectIpfsPathHeader: true,
preloadAtPublicGateway: true,
catchUnhandledProtocols: true,
Expand Down
11 changes: 11 additions & 0 deletions add-on/src/options/forms/experiments-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function experimentsForm ({
catchUnhandledProtocols,
linkify,
dnslinkPolicy,
recoverViaPublicGateway,
detectIpfsPathHeader,
ipfsProxy,
logNamespaces,
Expand All @@ -22,6 +23,7 @@ function experimentsForm ({
const onCatchUnhandledProtocolsChange = onOptionChange('catchUnhandledProtocols')
const onLinkifyChange = onOptionChange('linkify')
const onDnslinkPolicyChange = onOptionChange('dnslinkPolicy')
const onrecoverViaPublicGatewayChange = onOptionChange('recoverViaPublicGateway')
const onDetectIpfsPathHeaderChange = onOptionChange('detectIpfsPathHeader')
const onIpfsProxyChange = onOptionChange('ipfsProxy')

Expand Down Expand Up @@ -96,6 +98,15 @@ function experimentsForm ({
</option>
</select>
</div>
<div>
<label for="recoverViaPublicGateway">
<dl>
<dt>${browser.i18n.getMessage('option_recoverViaPublicGateway_title')}</dt>
<dd>${browser.i18n.getMessage('option_recoverViaPublicGateway_description')}</dd>
</dl>
</label>
<div>${switchToggle({ id: 'recoverViaPublicGateway', checked: recoverViaPublicGateway, onchange: onrecoverViaPublicGatewayChange })}</div>
</div>
<div>
<label for="detectIpfsPathHeader">
<dl>
Expand Down
1 change: 1 addition & 0 deletions add-on/src/options/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ module.exports = function optionsPage (state, emit) {
catchUnhandledProtocols: state.options.catchUnhandledProtocols,
linkify: state.options.linkify,
dnslinkPolicy: state.options.dnslinkPolicy,
recoverViaPublicGateway: state.options.recoverViaPublicGateway,
detectIpfsPathHeader: state.options.detectIpfsPathHeader,
ipfsProxy: state.options.ipfsProxy,
logNamespaces: state.options.logNamespaces,
Expand Down