diff --git a/add-on/_locales/en/messages.json b/add-on/_locales/en/messages.json index c4bf21393..85c7b310d 100644 --- a/add-on/_locales/en/messages.json +++ b/add-on/_locales/en/messages.json @@ -375,6 +375,14 @@ "message": "Check before HTTP request", "description": "A select field option description on the Preferences screen (option_dnslinkPolicy_enabled)" }, + "option_recoverFailedHttpRequests_title": { + "message": "Recover Failed HTTP Requests", + "description": "An option title on the Preferences screen (option_recoverFailedHttpRequests_title)" + }, + "option_recoverFailedHttpRequests_description": { + "message": "Recover failed HTTP requests for IPFS resources by redirecting to the public gateway.", + "description": "An option description on the Preferences screen (option_recoverFailedHttpRequests_description)" + }, "option_detectIpfsPathHeader_title": { "message": "Detect X-Ipfs-Path Header", "description": "An option title on the Preferences screen (option_detectIpfsPathHeader_title)" diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index c1aa2f608..82fb05fc3 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -106,6 +106,7 @@ module.exports = async function init () { browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, { urls: [''] }, ['blocking']) browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, { urls: [''] }, ['blocking', 'responseHeaders']) browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, { urls: [''] }) + browser.webRequest.onCompleted.addListener(onCompleted, { urls: [''] }) browser.storage.onChanged.addListener(onStorageChange) browser.webNavigation.onCommitted.addListener(onNavigationCommitted) browser.webNavigation.onDOMContentLoaded.addListener(onDOMContentLoaded) @@ -170,6 +171,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 @@ -693,6 +698,9 @@ module.exports = async function init () { await browser.storage.local.set({ detectIpfsPathHeader: true }) } break + case 'recoverFailedHttpRequests': + state[key] = change.newValue + break case 'logNamespaces': shouldReloadExtension = true state[key] = localStorage.debug = change.newValue diff --git a/add-on/src/lib/ipfs-request.js b/add-on/src/lib/ipfs-request.js index 03d179e60..888993ab5 100644 --- a/add-on/src/lib/ipfs-request.js +++ b/add-on/src/lib/ipfs-request.js @@ -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) { @@ -380,6 +400,7 @@ 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' && @@ -387,22 +408,40 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru 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 recoverable = isRecoverable(request, state, ipfsPathValidator) + if (!redirect && recoverable) { + const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url, state.pubGwURLString) + redirect = { redirectUrl } + log(`onErrorOccurred: attempting to recover failed request 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 recoverable = + isRecoverable(request, state, ipfsPathValidator) && + recoverableErrorCodes.has(request.statusCode) + if (recoverable) { + const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url, state.pubGwURLString) + const redirect = { redirectUrl } + if (redirect) { + log(`onCompleted: attempting to recover failed request for ${request.url}`, redirect) + createTabWithURL(redirect, browser) } } } - } } @@ -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 isRecoverable (request, state, ipfsPathValidator) { + return state.recoverFailedHttpRequests && + 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 + }) +} diff --git a/add-on/src/lib/options.js b/add-on/src/lib/options.js index 1bb01861b..cc43ef8b4 100644 --- a/add-on/src/lib/options.js +++ b/add-on/src/lib/options.js @@ -16,6 +16,7 @@ exports.optionDefaults = Object.freeze({ automaticMode: true, linkify: false, dnslinkPolicy: 'best-effort', + recoverFailedHttpRequests: true, detectIpfsPathHeader: true, preloadAtPublicGateway: true, catchUnhandledProtocols: true, diff --git a/add-on/src/options/forms/experiments-form.js b/add-on/src/options/forms/experiments-form.js index ccd03a9a9..7314b26e7 100644 --- a/add-on/src/options/forms/experiments-form.js +++ b/add-on/src/options/forms/experiments-form.js @@ -11,6 +11,7 @@ function experimentsForm ({ catchUnhandledProtocols, linkify, dnslinkPolicy, + recoverFailedHttpRequests, detectIpfsPathHeader, ipfsProxy, logNamespaces, @@ -22,6 +23,7 @@ function experimentsForm ({ const onCatchUnhandledProtocolsChange = onOptionChange('catchUnhandledProtocols') const onLinkifyChange = onOptionChange('linkify') const onDnslinkPolicyChange = onOptionChange('dnslinkPolicy') + const onrecoverFailedHttpRequestsChange = onOptionChange('recoverFailedHttpRequests') const onDetectIpfsPathHeaderChange = onOptionChange('detectIpfsPathHeader') const onIpfsProxyChange = onOptionChange('ipfsProxy') @@ -96,6 +98,15 @@ function experimentsForm ({ +
+ +
${switchToggle({ id: 'recoverFailedHttpRequests', checked: recoverFailedHttpRequests, onchange: onrecoverFailedHttpRequestsChange })}
+