Skip to content

Commit c2daf92

Browse files
committed
feat: opt-in for /ipns/webui.ipfs.io
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
1 parent 707fa29 commit c2daf92

File tree

11 files changed

+155
-8
lines changed

11 files changed

+155
-8
lines changed

add-on/_locales/en/messages.json

+8
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,14 @@
351351
"message": "Turn plaintext /ipfs/ paths into clickable links",
352352
"description": "An option description on the Preferences screen (option_linkify_description)"
353353
},
354+
"option_webuiFromDNSLink_title": {
355+
"message": "Load the latest Web UI",
356+
"description": "An option title on the Preferences screen (option_webuiFromDNSLink_title)"
357+
},
358+
"option_webuiFromDNSLink_description": {
359+
"message": "Replaces stable version provided by your node with one at /ipns/webui.ipfs.io (requires working DNS and a compatible backend)",
360+
"description": "An option description on the Preferences screen (option_webuiFromDNSLink_description)"
361+
},
354362
"option_dnslinkPolicy_title": {
355363
"message": "DNSLink Support",
356364
"description": "An option title on the Preferences screen (option_dnslinkPolicy_title)"

add-on/src/lib/ipfs-client/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ async function destroyIpfsClient () {
4747

4848
function preloadWebui (instance, opts) {
4949
// run only when client still exists and async fetch is possible
50-
if (!(client && instance && opts.webuiRootUrl && typeof fetch === 'function')) return
50+
if (!(client && instance && opts.webuiURLString && typeof fetch === 'function')) return
5151
// Optimization: preload the root CID to speed up the first time
5252
// Web UI is opened. If embedded js-ipfs is used it will trigger
5353
// remote (always recursive) preload of entire DAG to one of preload nodes.
5454
// This way when embedded node wants to load resource related to webui
5555
// it will get it fast from preload nodes.
56-
const webuiUrl = opts.webuiRootUrl
56+
const webuiUrl = opts.webuiURLString
5757
log(`preloading webui root at ${webuiUrl}`)
5858
return fetch(webuiUrl, { redirect: 'follow' })
5959
.then(response => {

add-on/src/lib/ipfs-companion.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ log.error = debug('ipfs-companion:main:error')
77

88
const browser = require('webextension-polyfill')
99
const { optionDefaults, storeMissingOptions, migrateOptions } = require('./options')
10-
const { initState, offlinePeerCount } = require('./state')
10+
const { initState, offlinePeerCount, buildWebuiURLString } = require('./state')
1111
const { createIpfsPathValidator } = require('./ipfs-path')
1212
const createDnslinkResolver = require('./dnslink')
1313
const { createRequestModifier, redirectOptOutHint } = require('./ipfs-request')
@@ -223,7 +223,7 @@ module.exports = async function init () {
223223
peerCount: state.peerCount,
224224
gwURLString: dropSlash(state.gwURLString),
225225
pubGwURLString: dropSlash(state.pubGwURLString),
226-
webuiRootUrl: state.webuiRootUrl,
226+
webuiURLString: state.webuiURLString,
227227
apiURLString: dropSlash(state.apiURLString),
228228
redirect: state.redirect,
229229
noRedirectHostnames: state.noRedirectHostnames,
@@ -633,7 +633,7 @@ module.exports = async function init () {
633633
case 'ipfsApiUrl':
634634
state.apiURL = new URL(change.newValue)
635635
state.apiURLString = state.apiURL.toString()
636-
state.webuiRootUrl = `${state.apiURLString}webui`
636+
state.webuiURLString = buildWebuiURLString(state)
637637
shouldRestartIpfsClient = true
638638
break
639639
case 'ipfsApiPollMs':
@@ -664,6 +664,10 @@ module.exports = async function init () {
664664
shouldReloadExtension = true
665665
state[key] = localStorage.debug = change.newValue
666666
break
667+
case 'webuiFromDNSLink':
668+
state[key] = change.newValue
669+
state.webuiURLString = buildWebuiURLString(state)
670+
break
667671
case 'linkify':
668672
case 'catchUnhandledProtocols':
669673
case 'displayNotifications':

add-on/src/lib/ipfs-request.js

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ log.error = debug('ipfs-companion:request:error')
88
const LRU = require('lru-cache')
99
const IsIpfs = require('is-ipfs')
1010
const { pathAtHttpGateway } = require('./ipfs-path')
11+
const { buildWebuiURLString } = require('./state')
1112
const redirectOptOutHint = 'x-ipfs-companion-no-redirect'
1213
const recoverableErrors = new Set([
1314
// Firefox
@@ -229,6 +230,17 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
229230
return
230231
}
231232

233+
// Recover from a broken DNSLink webui by redirecting back to CID one
234+
// TODO: remove when both GO and JS ship support for /ipns/webui.ipfs.io on the API port
235+
if (request.statusCode === 404 && request.url === state.webuiURLString && state.webuiFromDNSLink) {
236+
const stableWebui = buildWebuiURLString({
237+
apiURLString: state.apiURLString,
238+
webuiFromDNSLink: false
239+
})
240+
log(`opening webui via ${state.webuiURLString} is not supported yet, opening stable webui from ${stableWebui} instead`)
241+
return { redirectUrl: stableWebui }
242+
}
243+
232244
// Skip if request is marked as ignored
233245
if (isIgnored(request.requestId)) {
234246
return

add-on/src/lib/options.js

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ exports.optionDefaults = Object.freeze({
2424
ipfsApiUrl: buildIpfsApiUrl(),
2525
ipfsApiPollMs: 3000,
2626
ipfsProxy: true, // window.ipfs
27+
webuiFromDNSLink: false,
2728
logNamespaces: 'jsipfs*,ipfs*,-*:ipns*,-ipfs:preload*,-ipfs-http-client:request*'
2829
})
2930

add-on/src/lib/state.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/* eslint-env browser, webextensions */
33

44
const { safeURL } = require('./options')
5+
56
const offlinePeerCount = -1
67

78
function initState (options) {
@@ -22,9 +23,17 @@ function initState (options) {
2223
state.gwURLString = state.gwURL.toString()
2324
delete state.customGatewayUrl
2425
state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy
25-
state.webuiRootUrl = `${state.apiURLString}webui`
26+
state.webuiURLString = buildWebuiURLString(state)
2627
return state
2728
}
2829

30+
function buildWebuiURLString ({ apiURLString, webuiFromDNSLink }) {
31+
if (!apiURLString) throw new Error('Missing apiURLString')
32+
return webuiFromDNSLink
33+
? `${apiURLString}ipns/webui.ipfs.io/`
34+
: `${apiURLString}webui/`
35+
}
36+
2937
exports.initState = initState
3038
exports.offlinePeerCount = offlinePeerCount
39+
exports.buildWebuiURLString = buildWebuiURLString

add-on/src/options/forms/experiments-form.js

+11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function experimentsForm ({
1010
preloadAtPublicGateway,
1111
catchUnhandledProtocols,
1212
linkify,
13+
webuiFromDNSLink,
1314
dnslinkPolicy,
1415
detectIpfsPathHeader,
1516
ipfsProxy,
@@ -24,6 +25,7 @@ function experimentsForm ({
2425
const onDnslinkPolicyChange = onOptionChange('dnslinkPolicy')
2526
const onDetectIpfsPathHeaderChange = onOptionChange('detectIpfsPathHeader')
2627
const onIpfsProxyChange = onOptionChange('ipfsProxy')
28+
const onWebuiFromDNSLinkChange = onOptionChange('webuiFromDNSLink')
2729

2830
return html`
2931
<form>
@@ -66,6 +68,15 @@ function experimentsForm ({
6668
</label>
6769
<div>${switchToggle({ id: 'linkify', checked: linkify, onchange: onLinkifyChange })}</div>
6870
</div>
71+
<div>
72+
<label for="webuiFromDNSLink">
73+
<dl>
74+
<dt>${browser.i18n.getMessage('option_webuiFromDNSLink_title')}</dt>
75+
<dd>${browser.i18n.getMessage('option_webuiFromDNSLink_description')}</dd>
76+
</dl>
77+
</label>
78+
<div>${switchToggle({ id: 'webuiFromDNSLink', checked: webuiFromDNSLink, onchange: onWebuiFromDNSLinkChange })}</div>
79+
</div>
6980
<div>
7081
<label for="dnslinkPolicy">
7182
<dl>

add-on/src/options/page.js

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ module.exports = function optionsPage (state, emit) {
7474
preloadAtPublicGateway: state.options.preloadAtPublicGateway,
7575
catchUnhandledProtocols: state.options.catchUnhandledProtocols,
7676
linkify: state.options.linkify,
77+
webuiFromDNSLink: state.options.webuiFromDNSLink,
7778
dnslinkPolicy: state.options.dnslinkPolicy,
7879
detectIpfsPathHeader: state.options.detectIpfsPathHeader,
7980
ipfsProxy: state.options.ipfsProxy,

add-on/src/popup/browser-action/store.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ module.exports = (state, emitter) => {
129129

130130
emitter.on('openWebUi', async () => {
131131
try {
132-
browser.tabs.create({ url: state.webuiRootUrl })
132+
browser.tabs.create({ url: state.webuiURLString })
133133
window.close()
134134
} catch (error) {
135135
console.error(`Unable Open Web UI due to ${error}`)

test/functional/lib/ipfs-request-workarounds.test.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { describe, it, before, beforeEach, after } = require('mocha')
33
const { expect } = require('chai')
44
const { URL } = require('url') // URL implementation with support for .origin attribute
55
const browser = require('sinon-chrome')
6-
const { initState } = require('../../../add-on/src/lib/state')
6+
const { initState, buildWebuiURLString } = require('../../../add-on/src/lib/state')
77
const { createRuntimeChecks } = require('../../../add-on/src/lib/runtime-checks')
88
const { createRequestModifier } = require('../../../add-on/src/lib/ipfs-request')
99
const createDnslinkResolver = require('../../../add-on/src/lib/dnslink')
@@ -112,6 +112,27 @@ describe('modifyRequest processing', function () {
112112
})
113113
})
114114

115+
describe('a request to <apiURL>/ipns/webui.ipfs.io/ when webuiFromDNSLink = true', function () {
116+
it('should not be left untouched by onHeadersReceived if statusCode is 200', function () {
117+
state.webuiFromDNSLink = true
118+
state.webuiURLString = buildWebuiURLString(state)
119+
const request = {
120+
url: state.webuiURLString,
121+
statusCode: 200
122+
}
123+
expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
124+
})
125+
it('should be redirected in onHeadersReceived to <apiURL>/webui/ if statusCode is 404', function () {
126+
state.webuiFromDNSLink = true
127+
state.webuiURLString = buildWebuiURLString(state)
128+
const request = {
129+
url: state.webuiURLString,
130+
statusCode: 404
131+
}
132+
expect(modifyRequest.onHeadersReceived(request).redirectUrl).to.equal(`${state.apiURLString}webui/`)
133+
})
134+
})
135+
115136
after(function () {
116137
delete global.URL
117138
delete global.browser

test/functional/lib/state.test.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use strict'
2+
const { describe, it, beforeEach, before } = require('mocha')
3+
const { expect } = require('chai')
4+
const { initState, offlinePeerCount, buildWebuiURLString } = require('../../../add-on/src/lib/state')
5+
const { optionDefaults } = require('../../../add-on/src/lib/options')
6+
const { URL } = require('url')
7+
8+
describe('state.js', function () {
9+
describe('initState', function () {
10+
before(function () {
11+
global.URL = URL
12+
})
13+
it('should copy passed options as-is', () => {
14+
const expectedProps = Object.assign({}, optionDefaults)
15+
delete expectedProps.publicGatewayUrl
16+
delete expectedProps.useCustomGateway
17+
delete expectedProps.ipfsApiUrl
18+
delete expectedProps.customGatewayUrl
19+
const state = initState(optionDefaults)
20+
for (const prop in expectedProps) {
21+
expect(state).to.have.property(prop, optionDefaults[prop])
22+
}
23+
})
24+
it('should generate pubGwURL*', () => {
25+
const state = initState(optionDefaults)
26+
expect(state).to.not.have.property('publicGatewayUrl')
27+
expect(state).to.have.property('pubGwURL')
28+
expect(state).to.have.property('pubGwURLString')
29+
})
30+
it('should generate redirect state', () => {
31+
const state = initState(optionDefaults)
32+
expect(state).to.not.have.property('useCustomGateway')
33+
expect(state).to.have.property('redirect')
34+
})
35+
it('should generate apiURL*', () => {
36+
const state = initState(optionDefaults)
37+
expect(state).to.not.have.property('ipfsApiUrl')
38+
expect(state).to.have.property('apiURL')
39+
expect(state).to.have.property('apiURLString')
40+
})
41+
it('should generate gwURL*', () => {
42+
const state = initState(optionDefaults)
43+
expect(state).to.not.have.property('customGatewayUrl')
44+
expect(state).to.have.property('gwURL')
45+
expect(state).to.have.property('gwURLString')
46+
})
47+
it('should generate webuiURLString', () => {
48+
const state = initState(optionDefaults)
49+
expect(state).to.have.property('webuiURLString')
50+
})
51+
})
52+
53+
describe('offlinePeerCount', function () {
54+
it('should be equal -1', () => {
55+
expect(offlinePeerCount).to.be.equal(-1)
56+
})
57+
})
58+
59+
describe('buildWebuiURLString', function () {
60+
let fakeState
61+
beforeEach(() => {
62+
fakeState = { apiURLString: 'http://127.0.0.1:5001/' }
63+
})
64+
it('should be throw error on missing apiURLString', () => {
65+
expect(() => buildWebuiURLString({})).to.throw('Missing apiURLString')
66+
})
67+
it('should return /webui for optionDefaults', () => {
68+
fakeState.webuiFromDNSLink = optionDefaults.webuiFromDNSLink
69+
expect(buildWebuiURLString(fakeState)).to.be.equal(`${fakeState.apiURLString}webui/`)
70+
})
71+
it('should return /webui when webuiFromDNSLink is falsy', () => {
72+
fakeState.webuiFromDNSLink = undefined
73+
expect(buildWebuiURLString(fakeState)).to.be.equal(`${fakeState.apiURLString}webui/`)
74+
})
75+
it('should return /ipns/webui.ipfs.io when webuiFromDNSLink is true', () => {
76+
fakeState.webuiFromDNSLink = true
77+
expect(buildWebuiURLString(fakeState)).to.be.equal(`${fakeState.apiURLString}ipns/webui.ipfs.io/`)
78+
})
79+
})
80+
})

0 commit comments

Comments
 (0)