Skip to content

Commit

Permalink
feat: recovery page when local gateway is unreachable (#1125)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcin Rataj <[email protected]>
  • Loading branch information
whizzzkid and lidel authored Feb 3, 2023
1 parent 5487486 commit a74fbb3
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 18 deletions.
28 changes: 28 additions & 0 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -750,5 +750,33 @@
"option_telemetryGroupTracking_description": {
"message": "Tracking description",
"description": "A description for the 'tracking' grouping of metrics we collect (option_telemetryGroupTracking_description)"
},
"recovery_page_title" : {
"message": "Problem with your IPFS node | IPFS Companion",
"description": "Title of the recovery page (recovery_page_title)"
},
"recovery_page_sub_header": {
"message": "Unable to reach your IPFS node :(",
"description": "Sub-Header on the recovery screen (recovery_page_sub_header)"
},
"recovery_page_message_p1": {
"message": "Ensure your IPFS node runs and provides HTTP Gateway.",
"description": "Message Para-1 on the recovery screen (recovery_page_message_p1)"
},
"recovery_page_message_p2": {
"message": "You can also access deserialized version of the requested resource through the preferred public gateway set up in IPFS Companion. This delegates trust to a third-party address below, and skips local hash validation.",
"description": "Message Para-2 on the recovery screen (recovery_page_message_p2)"
},
"recovery_page_button": {
"message": "Continue to the public gateway",
"description": "Button on the recovery screen (recovery_page_button)"
},
"recovery_page_learn_more": {
"message": "Learn more about public gateways",
"description": "Learn more link on the recovery screen (recovery_page_learn_more)"
},
"recovery_page_update_preferences": {
"message": "Update your IPFS Companion preferences",
"description": "Learn more link on the recovery screen (recovery_page_learn_more)"
}
}
5 changes: 4 additions & 1 deletion add-on/manifest.common.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
"icons/png/ipfs-logo-off_38.png",
"icons/png/ipfs-logo-off_128.png",
"icons/ipfs-logo-on.svg",
"icons/ipfs-logo-off.svg"
"icons/ipfs-logo-off.svg",
"dist/recovery/recovery.css",
"dist/recovery/recovery.html",
"dist/recovery/recovery.js"
],
"content_security_policy": "script-src 'self'; object-src 'self'; frame-src 'self';",
"default_locale": "en"
Expand Down
31 changes: 19 additions & 12 deletions add-on/src/landing-pages/welcome/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,24 @@ export default function createWelcomePage (i18n) {
/* ========================================================
Render functions for the left side
======================================================== */

const renderCompanionLogo = (i18n, isIpfsOnline) => {
export const renderLogo = (isIpfsOnline, logoSize = 128) => {
const logoPath = '../../../icons'
const logoSize = 128

return html`
${logo({ path: logoPath, size: logoSize, isIpfsOnline })}
`
}

export const renderCompanionLogo = (i18n, isIpfsOnline, showTitle = true) => {
const stateUnknown = isIpfsOnline === null

return html`
<div class="mt4 mb2 flex flex-column justify-center items-center transition-all ${stateUnknown && 'state-unknown'}">
${logo({ path: logoPath, size: logoSize, isIpfsOnline })}
<p class="montserrat mt3 mb0 f2">${i18n.getMessage('page_landingWelcome_logo_title')}</p>
${renderLogo(isIpfsOnline)}
${showTitle
? html`<p class="montserrat mt3 mb0 f2">${i18n.getMessage('page_landingWelcome_logo_title')}</p>`
: ''
}
</div>
`
}
Expand Down Expand Up @@ -88,17 +96,16 @@ const renderWelcome = (i18n, peerCount, openWebUi) => {
`
}

export const nodeOffSvg = (svgWidth = 130) => html`
<svg x="0px" y="0px" viewBox="0 0 100 100" width="${svgWidth}">
<path fill="${colorYellow}" d="M82.84 71.14L55.06 23a5.84 5.84 0 00-10.12 0L17.16 71.14a5.85 5.85 0 005.06 8.77h55.56a5.85 5.85 0 005.06-8.77zm-30.1-.66h-5.48V65h5.48zm0-10.26h-5.48V38.46h5.48z"/>
</svg>
`

const renderInstallSteps = (i18n, isIpfsOnline) => {
const copyClass = 'mv0 white f5 lh-copy'
const anchorClass = 'aqua hover-white'
const stateUnknown = isIpfsOnline === null
const svgWidth = 130

const nodeOffSvg = () => html`
<svg x="0px" y="0px" viewBox="0 0 100 100" width="${svgWidth}">
<path fill="${colorYellow}" d="M82.84 71.14L55.06 23a5.84 5.84 0 00-10.12 0L17.16 71.14a5.85 5.85 0 005.06 8.77h55.56a5.85 5.85 0 005.06-8.77zm-30.1-.66h-5.48V65h5.48zm0-10.26h-5.48V38.46h5.48z"/>
</svg>
`

const optionsUrl = browser.runtime.getURL(optionsPage)
return html`
Expand Down
1 change: 1 addition & 0 deletions add-on/src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

export const welcomePage = '/dist/landing-pages/welcome/index.html'
export const optionsPage = '/dist/options/options.html'
export const recoveryPagePath = '/dist/recovery/recovery.html'
export const tickMs = 250 // no CPU spike, but still responsive enough
3 changes: 1 addition & 2 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import LRU from 'lru-cache'
import all from 'it-all'
import { optionDefaults, storeMissingOptions, migrateOptions, guiURLString, safeURL } from './options.js'
import { initState, offlinePeerCount } from './state.js'
import { createIpfsPathValidator, sameGateway, safeHostname } from './ipfs-path.js'
import { createIpfsPathValidator, dropSlash, sameGateway, safeHostname } from './ipfs-path.js'
import createDnslinkResolver from './dnslink.js'
import { createRequestModifier } from './ipfs-request.js'
import { initIpfsClient, destroyIpfsClient, reloadIpfsClientOfflinePages } from './ipfs-client/index.js'
Expand Down Expand Up @@ -224,7 +224,6 @@ export default async function init () {

async function sendStatusUpdateToBrowserAction () {
if (!browserActionPort) return
const dropSlash = url => url.replace(/\/$/, '')
const currentTab = await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0])
const { version } = browser.runtime.getManifest()
const info = {
Expand Down
2 changes: 2 additions & 0 deletions add-on/src/lib/ipfs-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import isFQDN from 'is-fqdn'
// For how long more expensive lookups (DAG traversal etc) should be cached
const RESULT_TTL_MS = 300000 // 5 minutes

export const dropSlash = url => url.replace(/\/$/, '')

// Turns URL or URIencoded path into a content path
export function ipfsContentPath (urlOrPath, opts) {
opts = opts || {}
Expand Down
11 changes: 10 additions & 1 deletion add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import debug from 'debug'
import LRU from 'lru-cache'
import isIPFS from 'is-ipfs'
import isFQDN from 'is-fqdn'
import { pathAtHttpGateway, sameGateway, ipfsUri } from './ipfs-path.js'
import { dropSlash, ipfsUri, pathAtHttpGateway, sameGateway } from './ipfs-path.js'
import { safeURL } from './options.js'
import { braveNodeType } from './ipfs-client/brave.js'
import { recoveryPagePath } from './constants.js'

const log = debug('ipfs-companion:request')
log.error = debug('ipfs-companion:request:error')

Expand Down Expand Up @@ -140,6 +142,13 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida
const state = getState()
if (!state.active) return

// When local IPFS node is unreachable , show recovery page where user can redirect
// to public gateway.
if (!state.nodeActive && request.type === 'main_frame' && sameGateway(request.url, state.gwURL)) {
const publicUri = ipfsPathValidator.resolveToPublicUrl(request.url, state.pubGwURLString)
return { redirectUrl: `${dropSlash(runtimeRoot)}${recoveryPagePath}#${encodeURIComponent(publicUri)}` }
}

// 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
Expand Down
7 changes: 5 additions & 2 deletions add-on/src/lib/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ export function initState (options, overrides) {
return false
}
}
// TODO state.connected ~= state.peerCount > 0
// TODO state.nodeActive ~= API is online,eg. state.peerCount > offlinePeerCount
// TODO refactor this into a class. It's getting too big and messy.
Object.defineProperty(state, 'nodeActive', {
// TODO: make quick fetch to confirm it works?
get: function () { return this.peerCount !== offlinePeerCount }
})
Object.defineProperty(state, 'localGwAvailable', {
// TODO: make quick fetch to confirm it works?
get: function () { return this.ipfsNodeType !== 'embedded' }
Expand Down
54 changes: 54 additions & 0 deletions add-on/src/recovery/recovery.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@import url('~tachyons/css/tachyons.css');
@import url('~ipfs-css/ipfs.css');

#left-col {
background-image: url('../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%);
background-size: 100%;
background-repeat: repeat;
}

a:hover {
text-decoration: none;
}

a:visited {
color: inherit;
}

/*
https://github.com/tachyons-css/tachyons-queries
Tachyons: $point == large
*/
@media (min-width: 60em) {
#left-col {
position: fixed;
top: 0;
right: 55%;
width: 45%;
background-image: url('../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%);
background-size: 100%;
background-repeat: repeat;
}

#right-col {
margin-left: 54%;
margin-right: 6%;
}
}

@media (max-height: 800px) {
#left-col img {
width: 98px !important;
height: 98px !important;
}

#left-col svg {
width: 60px;
}
}

.recovery-root {
width: 100%;
height: 100%;
text-align: left;
}
20 changes: 20 additions & 0 deletions add-on/src/recovery/recovery.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>IPFS Node is Offline</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA==" />
<link rel="stylesheet" href="/dist/bundles/uiCommons.css">
<link rel="stylesheet" href="/dist/bundles/recoveryPage.css">
</head>
<body class="navy bg-white sans-serif">
<app class="flex flex-column transition-all vh-100">
<main class="bg-white flex-grow-1">
<div id="root"></div>
</main>
<script src="/dist/bundles/uiCommons.bundle.js"></script>
<script src="/dist/bundles/recoveryPage.bundle.js"></script>
</app>
</body>
</html>
75 changes: 75 additions & 0 deletions add-on/src/recovery/recovery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict'
/* eslint-env browser, webextensions */

import choo from 'choo'
import html from 'choo/html/index.js'
import browser, { i18n, runtime } from 'webextension-polyfill'
import { nodeOffSvg } from '../landing-pages/welcome/page.js'
import createWelcomePageStore from '../landing-pages/welcome/store.js'
import { optionsPage } from '../lib/constants.js'
import './recovery.css'

const app = choo()

const learnMoreLink = html`<a class="navy link underline-under hover-aqua" href="https://docs.ipfs.tech/how-to/companion-node-types/" target="_blank" rel="noopener noreferrer">${i18n.getMessage('recovery_page_learn_more')}</a>`

const optionsPageLink = html`<a class="navy link underline-under hover-aqua" id="learn-more" href="${optionsPage}" target="_blank" rel="noopener noreferrer">${i18n.getMessage('recovery_page_update_preferences')}</a>`

// TODO (whizzzkid): refactor base store to be more generic.
app.use(createWelcomePageStore(i18n, runtime))
// Register our single route
app.route('*', (state) => {
browser.runtime.sendMessage({ telemetry: { trackView: 'recovery' } })
const { hash } = window.location
const { href: publicURI } = new URL(decodeURIComponent(hash.slice(1)))

if (!publicURI) {
return
}

const openURLFromHash = () => {
try {
console.log('Opening URL from hash:', publicURI)
window.location.replace(publicURI)
} catch (err) {
console.error('Failed to open URL from hash:', err)
}
}

// if the IPFS node is online, open the URL from the hash, this will redirect to the local node.
if (state.isIpfsOnline) {
openURLFromHash()
return
}

return html`<div class="flex flex-column flex-row-l">
<div id="left-col" class="min-vh-100 flex flex-column justify-center items-center bg-navy white">
<div class="mb4 flex flex-column justify-center items-center">
${nodeOffSvg(200)}
<p class="mt0 mb0 f3 tc">${i18n.getMessage('recovery_page_sub_header')}</p>
</div>
</div>
<div id="right-col" class="pt7 mt5 w-100 flex flex-column justify-around items-center">
<p class="f3 fw5">${i18n.getMessage('recovery_page_message_p1')}</p>
<p class="f4 fw4">${i18n.getMessage('recovery_page_message_p2')}</p>
<p class="f4 fw4 w-100"><span class="b-ns">Public URL:</span> <a class="no-underline no-underline navy link hover-aqua" href="${publicURI}" rel="noopener noreferrer" target="_blank">${publicURI}</a></p>
<button
class="fade-in ba bw1 b--teal bg-teal snow f7 ph2 pv3 br2 ma4 pointer"
onclick=${openURLFromHash}
href="${publicURI}"
>
<span class="f5 fw6">${i18n.getMessage('recovery_page_button')}</span>
</button>
<p class="f5 fw2 pt5">
${learnMoreLink} | ${optionsPageLink}
</span>
</div>
</div>`
})

// Start the application and render it to the given querySelector
app.mount('#root')

// Set page title and header translation
document.title = i18n.getMessage('recovery_page_title')
19 changes: 19 additions & 0 deletions test/functional/lib/ipfs-request-gateway-redirect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('modifyRequest.onBeforeRequest:', function () {
global.URL = URL
global.browser = browser
browser.runtime.id = 'testid'
browser.runtime.getURL.returns('chrome-extension://testid/')
})

beforeEach(async function () {
Expand Down Expand Up @@ -425,6 +426,24 @@ describe('modifyRequest.onBeforeRequest:', function () {
})
})

describe('Recovers Page if node is unreachable', function () {
beforeEach(function () {
global.browser = browser
state.ipfsNodeType = 'external'
state.redirect = true
state.peerCount = -1
state.gwURLString = 'http://localhost:8080'
state.gwURL = new URL('http://localhost:8080')
state.pubGwURLString = 'https://ipfs.io'
state.pubGwURL = new URL('https://ipfs.io')
})
it('should present recovery page if node is offline', function () {
expect(state.nodeActive).to.be.equal(false)
const request = url2request('https://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar')
expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('chrome-extension://testid/dist/recovery/recovery.html#https%3A%2F%2Fipfs.io%2Fipfs%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%2Ffoo%2Fbar')
})
})

after(function () {
delete global.URL
delete global.browser
Expand Down
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const uiConfig = merge(commonConfig, {
browserAction: './add-on/src/popup/browser-action/index.js',
importPage: './add-on/src/popup/quick-import.js',
optionsPage: './add-on/src/options/options.js',
recoveryPage: './add-on/src/recovery/recovery.js',
welcomePage: './add-on/src/landing-pages/welcome/index.js'
},
optimization: {
Expand Down

0 comments on commit a74fbb3

Please sign in to comment.