Skip to content

Commit

Permalink
feat: opt-in for /ipns/webui.ipfs.io
Browse files Browse the repository at this point in the history
Adds an opt-in toggle (disabled by default) to Preferences
which changes the URL of Web UI opened via Browser Action menu
from {API}/webui to {API}/ipns/webui.ipfs.io

This enables user to load the latest webui via DNSLink.

Note that go-ipfs and js-ipfs do not whitelist /ipns/webui.ipfs.io on
the API port yet, so there is a fallback in place that detects HTTP 404
and redirects user to {API}/webui.

Closes: #736
  • Loading branch information
lidel committed Jul 18, 2019
1 parent 707fa29 commit c2daf92
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 8 deletions.
8 changes: 8 additions & 0 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,14 @@
"message": "Turn plaintext /ipfs/ paths into clickable links",
"description": "An option description on the Preferences screen (option_linkify_description)"
},
"option_webuiFromDNSLink_title": {
"message": "Load the latest Web UI",
"description": "An option title on the Preferences screen (option_webuiFromDNSLink_title)"
},
"option_webuiFromDNSLink_description": {
"message": "Replaces stable version provided by your node with one at /ipns/webui.ipfs.io (requires working DNS and a compatible backend)",
"description": "An option description on the Preferences screen (option_webuiFromDNSLink_description)"
},
"option_dnslinkPolicy_title": {
"message": "DNSLink Support",
"description": "An option title on the Preferences screen (option_dnslinkPolicy_title)"
Expand Down
4 changes: 2 additions & 2 deletions add-on/src/lib/ipfs-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ async function destroyIpfsClient () {

function preloadWebui (instance, opts) {
// run only when client still exists and async fetch is possible
if (!(client && instance && opts.webuiRootUrl && typeof fetch === 'function')) return
if (!(client && instance && opts.webuiURLString && typeof fetch === 'function')) return
// Optimization: preload the root CID to speed up the first time
// Web UI is opened. If embedded js-ipfs is used it will trigger
// remote (always recursive) preload of entire DAG to one of preload nodes.
// This way when embedded node wants to load resource related to webui
// it will get it fast from preload nodes.
const webuiUrl = opts.webuiRootUrl
const webuiUrl = opts.webuiURLString
log(`preloading webui root at ${webuiUrl}`)
return fetch(webuiUrl, { redirect: 'follow' })
.then(response => {
Expand Down
10 changes: 7 additions & 3 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ log.error = debug('ipfs-companion:main:error')

const browser = require('webextension-polyfill')
const { optionDefaults, storeMissingOptions, migrateOptions } = require('./options')
const { initState, offlinePeerCount } = require('./state')
const { initState, offlinePeerCount, buildWebuiURLString } = require('./state')
const { createIpfsPathValidator } = require('./ipfs-path')
const createDnslinkResolver = require('./dnslink')
const { createRequestModifier, redirectOptOutHint } = require('./ipfs-request')
Expand Down Expand Up @@ -223,7 +223,7 @@ module.exports = async function init () {
peerCount: state.peerCount,
gwURLString: dropSlash(state.gwURLString),
pubGwURLString: dropSlash(state.pubGwURLString),
webuiRootUrl: state.webuiRootUrl,
webuiURLString: state.webuiURLString,
apiURLString: dropSlash(state.apiURLString),
redirect: state.redirect,
noRedirectHostnames: state.noRedirectHostnames,
Expand Down Expand Up @@ -633,7 +633,7 @@ module.exports = async function init () {
case 'ipfsApiUrl':
state.apiURL = new URL(change.newValue)
state.apiURLString = state.apiURL.toString()
state.webuiRootUrl = `${state.apiURLString}webui`
state.webuiURLString = buildWebuiURLString(state)
shouldRestartIpfsClient = true
break
case 'ipfsApiPollMs':
Expand Down Expand Up @@ -664,6 +664,10 @@ module.exports = async function init () {
shouldReloadExtension = true
state[key] = localStorage.debug = change.newValue
break
case 'webuiFromDNSLink':
state[key] = change.newValue
state.webuiURLString = buildWebuiURLString(state)
break
case 'linkify':
case 'catchUnhandledProtocols':
case 'displayNotifications':
Expand Down
12 changes: 12 additions & 0 deletions add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ log.error = debug('ipfs-companion:request:error')
const LRU = require('lru-cache')
const IsIpfs = require('is-ipfs')
const { pathAtHttpGateway } = require('./ipfs-path')
const { buildWebuiURLString } = require('./state')
const redirectOptOutHint = 'x-ipfs-companion-no-redirect'
const recoverableErrors = new Set([
// Firefox
Expand Down Expand Up @@ -229,6 +230,17 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
return
}

// Recover from a broken DNSLink webui by redirecting back to CID one
// TODO: remove when both GO and JS ship support for /ipns/webui.ipfs.io on the API port
if (request.statusCode === 404 && request.url === state.webuiURLString && state.webuiFromDNSLink) {
const stableWebui = buildWebuiURLString({
apiURLString: state.apiURLString,
webuiFromDNSLink: false
})
log(`opening webui via ${state.webuiURLString} is not supported yet, opening stable webui from ${stableWebui} instead`)
return { redirectUrl: stableWebui }
}

// Skip if request is marked as ignored
if (isIgnored(request.requestId)) {
return
Expand Down
1 change: 1 addition & 0 deletions add-on/src/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ exports.optionDefaults = Object.freeze({
ipfsApiUrl: buildIpfsApiUrl(),
ipfsApiPollMs: 3000,
ipfsProxy: true, // window.ipfs
webuiFromDNSLink: false,
logNamespaces: 'jsipfs*,ipfs*,-*:ipns*,-ipfs:preload*,-ipfs-http-client:request*'
})

Expand Down
11 changes: 10 additions & 1 deletion add-on/src/lib/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/* eslint-env browser, webextensions */

const { safeURL } = require('./options')

const offlinePeerCount = -1

function initState (options) {
Expand All @@ -22,9 +23,17 @@ function initState (options) {
state.gwURLString = state.gwURL.toString()
delete state.customGatewayUrl
state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy
state.webuiRootUrl = `${state.apiURLString}webui`
state.webuiURLString = buildWebuiURLString(state)
return state
}

function buildWebuiURLString ({ apiURLString, webuiFromDNSLink }) {
if (!apiURLString) throw new Error('Missing apiURLString')
return webuiFromDNSLink
? `${apiURLString}ipns/webui.ipfs.io/`
: `${apiURLString}webui/`
}

exports.initState = initState
exports.offlinePeerCount = offlinePeerCount
exports.buildWebuiURLString = buildWebuiURLString
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 @@ -10,6 +10,7 @@ function experimentsForm ({
preloadAtPublicGateway,
catchUnhandledProtocols,
linkify,
webuiFromDNSLink,
dnslinkPolicy,
detectIpfsPathHeader,
ipfsProxy,
Expand All @@ -24,6 +25,7 @@ function experimentsForm ({
const onDnslinkPolicyChange = onOptionChange('dnslinkPolicy')
const onDetectIpfsPathHeaderChange = onOptionChange('detectIpfsPathHeader')
const onIpfsProxyChange = onOptionChange('ipfsProxy')
const onWebuiFromDNSLinkChange = onOptionChange('webuiFromDNSLink')

return html`
<form>
Expand Down Expand Up @@ -66,6 +68,15 @@ function experimentsForm ({
</label>
<div>${switchToggle({ id: 'linkify', checked: linkify, onchange: onLinkifyChange })}</div>
</div>
<div>
<label for="webuiFromDNSLink">
<dl>
<dt>${browser.i18n.getMessage('option_webuiFromDNSLink_title')}</dt>
<dd>${browser.i18n.getMessage('option_webuiFromDNSLink_description')}</dd>
</dl>
</label>
<div>${switchToggle({ id: 'webuiFromDNSLink', checked: webuiFromDNSLink, onchange: onWebuiFromDNSLinkChange })}</div>
</div>
<div>
<label for="dnslinkPolicy">
<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 @@ -74,6 +74,7 @@ module.exports = function optionsPage (state, emit) {
preloadAtPublicGateway: state.options.preloadAtPublicGateway,
catchUnhandledProtocols: state.options.catchUnhandledProtocols,
linkify: state.options.linkify,
webuiFromDNSLink: state.options.webuiFromDNSLink,
dnslinkPolicy: state.options.dnslinkPolicy,
detectIpfsPathHeader: state.options.detectIpfsPathHeader,
ipfsProxy: state.options.ipfsProxy,
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/popup/browser-action/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ module.exports = (state, emitter) => {

emitter.on('openWebUi', async () => {
try {
browser.tabs.create({ url: state.webuiRootUrl })
browser.tabs.create({ url: state.webuiURLString })
window.close()
} catch (error) {
console.error(`Unable Open Web UI due to ${error}`)
Expand Down
23 changes: 22 additions & 1 deletion test/functional/lib/ipfs-request-workarounds.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { describe, it, before, beforeEach, after } = require('mocha')
const { expect } = require('chai')
const { URL } = require('url') // URL implementation with support for .origin attribute
const browser = require('sinon-chrome')
const { initState } = require('../../../add-on/src/lib/state')
const { initState, buildWebuiURLString } = require('../../../add-on/src/lib/state')
const { createRuntimeChecks } = require('../../../add-on/src/lib/runtime-checks')
const { createRequestModifier } = require('../../../add-on/src/lib/ipfs-request')
const createDnslinkResolver = require('../../../add-on/src/lib/dnslink')
Expand Down Expand Up @@ -112,6 +112,27 @@ describe('modifyRequest processing', function () {
})
})

describe('a request to <apiURL>/ipns/webui.ipfs.io/ when webuiFromDNSLink = true', function () {
it('should not be left untouched by onHeadersReceived if statusCode is 200', function () {
state.webuiFromDNSLink = true
state.webuiURLString = buildWebuiURLString(state)
const request = {
url: state.webuiURLString,
statusCode: 200
}
expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
})
it('should be redirected in onHeadersReceived to <apiURL>/webui/ if statusCode is 404', function () {
state.webuiFromDNSLink = true
state.webuiURLString = buildWebuiURLString(state)
const request = {
url: state.webuiURLString,
statusCode: 404
}
expect(modifyRequest.onHeadersReceived(request).redirectUrl).to.equal(`${state.apiURLString}webui/`)
})
})

after(function () {
delete global.URL
delete global.browser
Expand Down
80 changes: 80 additions & 0 deletions test/functional/lib/state.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use strict'
const { describe, it, beforeEach, before } = require('mocha')
const { expect } = require('chai')
const { initState, offlinePeerCount, buildWebuiURLString } = require('../../../add-on/src/lib/state')
const { optionDefaults } = require('../../../add-on/src/lib/options')
const { URL } = require('url')

describe('state.js', function () {
describe('initState', function () {
before(function () {
global.URL = URL
})
it('should copy passed options as-is', () => {
const expectedProps = Object.assign({}, optionDefaults)
delete expectedProps.publicGatewayUrl
delete expectedProps.useCustomGateway
delete expectedProps.ipfsApiUrl
delete expectedProps.customGatewayUrl
const state = initState(optionDefaults)
for (const prop in expectedProps) {
expect(state).to.have.property(prop, optionDefaults[prop])
}
})
it('should generate pubGwURL*', () => {
const state = initState(optionDefaults)
expect(state).to.not.have.property('publicGatewayUrl')
expect(state).to.have.property('pubGwURL')
expect(state).to.have.property('pubGwURLString')
})
it('should generate redirect state', () => {
const state = initState(optionDefaults)
expect(state).to.not.have.property('useCustomGateway')
expect(state).to.have.property('redirect')
})
it('should generate apiURL*', () => {
const state = initState(optionDefaults)
expect(state).to.not.have.property('ipfsApiUrl')
expect(state).to.have.property('apiURL')
expect(state).to.have.property('apiURLString')
})
it('should generate gwURL*', () => {
const state = initState(optionDefaults)
expect(state).to.not.have.property('customGatewayUrl')
expect(state).to.have.property('gwURL')
expect(state).to.have.property('gwURLString')
})
it('should generate webuiURLString', () => {
const state = initState(optionDefaults)
expect(state).to.have.property('webuiURLString')
})
})

describe('offlinePeerCount', function () {
it('should be equal -1', () => {
expect(offlinePeerCount).to.be.equal(-1)
})
})

describe('buildWebuiURLString', function () {
let fakeState
beforeEach(() => {
fakeState = { apiURLString: 'http://127.0.0.1:5001/' }
})
it('should be throw error on missing apiURLString', () => {
expect(() => buildWebuiURLString({})).to.throw('Missing apiURLString')
})
it('should return /webui for optionDefaults', () => {
fakeState.webuiFromDNSLink = optionDefaults.webuiFromDNSLink
expect(buildWebuiURLString(fakeState)).to.be.equal(`${fakeState.apiURLString}webui/`)
})
it('should return /webui when webuiFromDNSLink is falsy', () => {
fakeState.webuiFromDNSLink = undefined
expect(buildWebuiURLString(fakeState)).to.be.equal(`${fakeState.apiURLString}webui/`)
})
it('should return /ipns/webui.ipfs.io when webuiFromDNSLink is true', () => {
fakeState.webuiFromDNSLink = true
expect(buildWebuiURLString(fakeState)).to.be.equal(`${fakeState.apiURLString}ipns/webui.ipfs.io/`)
})
})
})

0 comments on commit c2daf92

Please sign in to comment.