Skip to content

Commit 5c0b495

Browse files
committed
feat: add ipfsNodeType embedded:chromesockets
This adds new ipfsNodeType for use in Chromium contexts such as Brave. For now we do naive check if chrome.sockets.tcp* APIs are available and switch to ipfsNodeType=embedded:chromesockets if true. User can customize configuration of embedded js-ipfs node via Preferences.
1 parent 9abb740 commit 5c0b495

26 files changed

+173
-102
lines changed

add-on/_locales/en/messages.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,16 @@
227227
"message": "Embedded (experimental): run js-ipfs node in your browser (use only for development, read about its limitations under the link below)",
228228
"description": "An option description on the Preferences screen (option_ipfsNodeType_description)"
229229
},
230+
"option_ipfsNodeType_embedded_chromesockets_description": {
231+
"message": "Embedded with Chrome Sockets (experimental): run js-ipfs node in your browser with access to chrome.sockets APIs (details under the link below)",
232+
"description": "An option description on the Preferences screen (option_ipfsNodeType_description)"
233+
},
230234
"option_ipfsNodeConfig_title": {
231235
"message": "IPFS Node Config",
232236
"description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)"
233237
},
234238
"option_ipfsNodeConfig_description": {
235-
"message": "Configuration for the embedded IPFS node. Must be valid JSON.",
239+
"message": "Additional configuration for the embedded IPFS node (arrays will be merged). Must be valid JSON.",
236240
"description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)"
237241
},
238242
"option_ipfsNodeType_external": {
@@ -243,6 +247,10 @@
243247
"message": "Embedded",
244248
"description": "An option on the Preferences screen (option_ipfsNodeType_embedded)"
245249
},
250+
"option_ipfsNodeType_embedded_chromesockets": {
251+
"message": "Embedded + chrome.sockets",
252+
"description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)"
253+
},
246254
"option_header_gateways": {
247255
"message": "Gateways",
248256
"description": "A section header on the Preferences screen (option_header_gateways)"

add-on/src/lib/dnslink.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ module.exports = function createDnslinkResolver (getState) {
105105
readDnslinkFromTxtRecord (fqdn) {
106106
const state = getState()
107107
let apiProvider
108-
if (state.ipfsNodeType === 'external' && state.peerCount !== offlinePeerCount) {
108+
if (state.ipfsNodeType !== 'embedded' && state.peerCount !== offlinePeerCount) {
109109
apiProvider = state.apiURLString
110110
} else {
111111
// fallback to resolver at public gateway

add-on/src/lib/ipfs-client/embedded-brave.js renamed to add-on/src/lib/ipfs-client/embedded-chromesockets.js

+38-9
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
'use strict'
22
/* eslint-env browser, webextensions */
3+
const browser = require('webextension-polyfill')
4+
const debug = require('debug')
35

46
// Polyfills required by embedded HTTP server
57
const uptimeStart = Date.now()
68
process.uptime = () => Math.floor((Date.now() - uptimeStart) / 1000)
79
process.hrtime = require('browser-process-hrtime')
810

9-
const defaultsDeep = require('@nodeutils/defaults-deep')
11+
const mergeOptions = require('merge-options')
1012
const Ipfs = require('ipfs')
1113
const HttpApi = require('ipfs/src/http')
14+
const multiaddr = require('multiaddr')
15+
const maToUri = require('multiaddr-to-uri')
1216

1317
const { optionDefaults } = require('../options')
1418

@@ -25,6 +29,9 @@ let nodeHttpApi = null
2529
// to include everything (mplex, libp2p, mss): localStorage.debug = '*'
2630
localStorage.debug = 'jsipfs*,ipfs*,-*:mfs*,-*:ipns*,-ipfs:preload*'
2731

32+
const log = debug('ipfs-companion:client:embedded')
33+
log.error = debug('ipfs-companion:client:embedded:error')
34+
2835
exports.init = function init (opts) {
2936
/*
3037
// TEST RAW require('http') SERVER
@@ -36,17 +43,25 @@ exports.init = function init (opts) {
3643
hapiServer = startRawHapiServer(9092)
3744
}
3845
*/
39-
console.log('[ipfs-companion] Embedded ipfs init')
46+
log('init: embedded js-ipfs+chrome.sockets')
4047

41-
const defaultOpts = optionDefaults.ipfsNodeConfig
42-
const userOpts = JSON.parse(opts.ipfsNodeConfig)
43-
const ipfsOpts = defaultsDeep(defaultOpts, userOpts, { start: false })
48+
const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig)
49+
defaultOpts.libp2p = {
50+
config: {
51+
dht: {
52+
enabled: false
53+
}
54+
}
55+
}
4456

57+
const userOpts = JSON.parse(opts.ipfsNodeConfig)
58+
const ipfsOpts = mergeOptions.call({ concatArrays: true }, defaultOpts, userOpts, { start: false })
59+
log('creating js-ipfs with opts: ', ipfsOpts)
4560
node = new Ipfs(ipfsOpts)
4661

4762
return new Promise((resolve, reject) => {
4863
node.once('error', (error) => {
49-
console.error('[ipfs-companion] Something went terribly wrong during startup of js-ipfs!', error)
64+
log.error('something went terribly wrong during startup of js-ipfs!', error)
5065
reject(error)
5166
})
5267
node.once('ready', async () => {
@@ -55,13 +70,14 @@ exports.init = function init (opts) {
5570
try {
5671
const httpServers = new HttpApi(node, ipfsOpts)
5772
nodeHttpApi = await httpServers.start()
73+
await updateConfigWithHttpEndpoints(node)
5874
resolve(node)
5975
} catch (err) {
6076
reject(err)
6177
}
6278
})
6379
node.on('error', error => {
64-
console.error('[ipfs-companion] Something went terribly wrong in embedded js-ipfs!', error)
80+
log.error('something went terribly wrong in embedded js-ipfs!', error)
6581
})
6682
try {
6783
await node.start()
@@ -72,8 +88,21 @@ exports.init = function init (opts) {
7288
})
7389
}
7490

91+
// Update internal configuration to HTTP Endpoints from js-ipfs instance
92+
async function updateConfigWithHttpEndpoints (ipfs) {
93+
const ma = await ipfs.config.get('Addresses.Gateway')
94+
log(`synchronizing Addresses.Gateway=${ma} to customGatewayUrl and ipfsNodeConfig`)
95+
const httpGateway = maToUri(ma.includes('/http') ? ma : multiaddr(ma).encapsulate('/http'))
96+
const ipfsNodeConfig = JSON.parse((await browser.storage.local.get('ipfsNodeConfig')).ipfsNodeConfig)
97+
ipfsNodeConfig.config.Addresses.Gateway = ma
98+
await browser.storage.local.set({
99+
customGatewayUrl: httpGateway,
100+
ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2)
101+
})
102+
}
103+
75104
exports.destroy = async function () {
76-
console.log('[ipfs-companion] Embedded ipfs destroy')
105+
log('destroy: embedded js-ipfs+chrome.sockets')
77106

78107
/*
79108
if (httpServer) {
@@ -98,7 +127,7 @@ exports.destroy = async function () {
98127
try {
99128
await nodeHttpApi.stop()
100129
} catch (err) {
101-
console.error(`[ipfs-companion] failed to stop HttpApi`, err)
130+
log.error('failed to stop HttpApi', err)
102131
}
103132
nodeHttpApi = null
104133
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
const defaultsDeep = require('@nodeutils/defaults-deep')
3+
const mergeOptions = require('merge-options')
44
const Ipfs = require('ipfs')
55
const { optionDefaults } = require('../options')
66

@@ -9,9 +9,9 @@ let node = null
99
exports.init = function init (opts) {
1010
console.log('[ipfs-companion] Embedded ipfs init')
1111

12-
const defaultOpts = optionDefaults.ipfsNodeConfig
12+
const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig)
1313
const userOpts = JSON.parse(opts.ipfsNodeConfig)
14-
const ipfsOpts = defaultsDeep(defaultOpts, userOpts, { start: false })
14+
const ipfsOpts = mergeOptions.call({ concatArrays: true }, defaultOpts, userOpts, { start: false })
1515

1616
node = new Ipfs(ipfsOpts)
1717

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

+11-14
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,25 @@
22

33
/* eslint-env browser, webextensions */
44

5+
const debug = require('debug')
6+
const log = debug('ipfs-companion:client')
7+
log.error = debug('ipfs-companion:client:error')
8+
59
const browser = require('webextension-polyfill')
610
const external = require('./external')
7-
const embeddedJs = require('./embedded')
8-
const embeddedJsBrave = require('./embedded-brave')
11+
const embedded = require('./embedded')
12+
const embeddedWithChromeSockets = require('./embedded-chromesockets')
913

1014
let client
1115

12-
// TODO: make generic
13-
const hasChromeSocketsForTcp = typeof chrome === 'object' &&
14-
typeof chrome.runtime === 'object' &&
15-
typeof chrome.runtime.id === 'string' &&
16-
typeof chrome.sockets === 'object' &&
17-
typeof chrome.sockets.tcpServer === 'object' &&
18-
typeof chrome.sockets === 'object' &&
19-
typeof chrome.sockets.tcp === 'object'
20-
2116
async function initIpfsClient (opts) {
2217
await destroyIpfsClient()
23-
2418
switch (opts.ipfsNodeType) {
2519
case 'embedded':
26-
client = hasChromeSocketsForTcp ? embeddedJsBrave : embeddedJs // TODO: make generic
20+
client = embedded
21+
break
22+
case 'embedded:chromesockets':
23+
client = embeddedWithChromeSockets
2724
break
2825
case 'external':
2926
client = external
@@ -63,7 +60,7 @@ async function _reloadIpfsClientDependents () {
6360
// detect bundled webui in any of open tabs
6461
if (_isWebuiTab(tab.url)) {
6562
browser.tabs.reload(tab.id)
66-
console.log('[ipfs-companion] reloading bundled webui')
63+
log('reloading bundled webui')
6764
}
6865
})
6966
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const { initIpfsClient, destroyIpfsClient } = require('./ipfs-client')
1212
const { createIpfsUrlProtocolHandler } = require('./ipfs-protocol')
1313
const createNotifier = require('./notifier')
1414
const createCopier = require('./copier')
15-
const createRuntimeChecks = require('./runtime-checks')
15+
const { createRuntimeChecks } = require('./runtime-checks')
1616
const { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress } = require('./context-menus')
1717
const createIpfsProxy = require('./ipfs-proxy')
1818
const { showPendingLandingPages } = require('./on-installed')

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {
8585
// Test if actions such as 'per site redirect toggle' should be enabled for the URL
8686
isRedirectPageActionsContext (url) {
8787
const state = getState()
88-
return state.ipfsNodeType === 'external' && // hide with embedded node
88+
return state.ipfsNodeType !== 'embedded' && // hide with embedded node
8989
(IsIpfs.ipnsUrl(url) || // show on /ipns/<fqdn>
9090
(url.startsWith('http') && // hide on non-HTTP pages
9191
!url.startsWith(state.gwURLString) && // hide on /ipfs/*

add-on/src/lib/options.js

+37-18
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,14 @@
11
'use strict'
22

33
const isFQDN = require('is-fqdn')
4+
const { hasChromeSocketsForTcp } = require('./runtime-checks')
45

56
exports.optionDefaults = Object.freeze({
67
active: true, // global ON/OFF switch, overrides everything else
7-
ipfsNodeType: 'embedded', // Brave should default to js-ipfs: https://github.com/ipfs-shipyard/ipfs-companion/issues/664
8-
ipfsNodeConfig: JSON.stringify({
9-
config: {
10-
Addresses: {
11-
Swarm: [],
12-
API: '/ip4/127.0.0.1/tcp/5002',
13-
Gateway: '/ip4/127.0.0.1/tcp/9090'
14-
}
15-
},
16-
libp2p: {
17-
config: {
18-
dht: {
19-
enabled: false
20-
}
21-
}
22-
}
23-
}, null, 2),
8+
ipfsNodeType: buildDefaultIpfsNodeType(),
9+
ipfsNodeConfig: buildDefaultIpfsNodeConfig(),
2410
publicGatewayUrl: 'https://ipfs.io',
25-
useCustomGateway: false, // TODO: Brave should not redirect to public one, but own
11+
useCustomGateway: true,
2612
noRedirectHostnames: [],
2713
automaticMode: true,
2814
linkify: false,
@@ -37,6 +23,27 @@ exports.optionDefaults = Object.freeze({
3723
ipfsProxy: true // window.ipfs
3824
})
3925

26+
function buildDefaultIpfsNodeType () {
27+
// Right now Brave is the only vendor giving us access to chrome.sockets
28+
return hasChromeSocketsForTcp() ? 'embedded:chromesockets' : 'external'
29+
}
30+
31+
function buildDefaultIpfsNodeConfig () {
32+
let config = {
33+
config: {
34+
Addresses: {
35+
Swarm: []
36+
}
37+
}
38+
}
39+
if (hasChromeSocketsForTcp()) {
40+
// config.config.Addresses.API = '/ip4/127.0.0.1/tcp/5002'
41+
config.config.Addresses.API = '' // disable API port
42+
config.config.Addresses.Gateway = '/ip4/127.0.0.1/tcp/8080'
43+
}
44+
return JSON.stringify(config, null, 2)
45+
}
46+
4047
// `storage` should be a browser.storage.local or similar
4148
exports.storeMissingOptions = (read, defaults, storage) => {
4249
const requiredKeys = Object.keys(defaults)
@@ -105,4 +112,16 @@ exports.migrateOptions = async (storage) => {
105112
})
106113
await storage.remove('dnslink')
107114
}
115+
// ~ v2.8.x + Brave
116+
// Upgrade js-ipfs to js-ipfs + chrome.sockets
117+
const { ipfsNodeType } = await storage.get('ipfsNodeType')
118+
if (ipfsNodeType === 'embedded' && hasChromeSocketsForTcp()) {
119+
console.log(`[ipfs-companion] migrating ipfsNodeType to 'embedded:chromesockets'`)
120+
// Overwrite old config
121+
const ipfsNodeConfig = JSON.parse(exports.optionDefaults.ipfsNodeConfig)
122+
await storage.set({
123+
ipfsNodeType: 'embedded:chromesockets',
124+
ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2)
125+
})
126+
}
108127
}

add-on/src/lib/runtime-checks.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ async function createRuntimeChecks (browser) {
3939
browser,
4040
isFirefox: runtimeIsFirefox,
4141
isAndroid: runtimeIsAndroid,
42+
isBrave: runtimeHasSocketsForTcp, // TODO: make it more robust
4243
hasChromeSocketsForTcp: runtimeHasSocketsForTcp,
4344
hasNativeProtocolHandler: runtimeHasNativeProtocol
4445
})
4546
}
4647

47-
module.exports = createRuntimeChecks
48+
module.exports.createRuntimeChecks = createRuntimeChecks
49+
module.exports.hasChromeSocketsForTcp = hasChromeSocketsForTcp

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function gatewaysForm ({
2323
const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', normalizeGatewayURL)
2424
const onNoRedirectHostnamesChange = onOptionChange('noRedirectHostnames', hostTextToArray)
2525
const mixedContentWarning = !secureContextUrl.test(customGatewayUrl)
26-
const supportRedirectToCustomGateway = ipfsNodeType === 'external'
26+
const supportRedirectToCustomGateway = ipfsNodeType !== 'embedded'
2727

2828
return html`
2929
<form>
@@ -66,6 +66,7 @@ function gatewaysForm ({
6666
spellcheck="false"
6767
title="Enter URL without any sub-path"
6868
onchange=${onCustomGatewayUrlChange}
69+
${ipfsNodeType !== 'external' ? 'disabled' : ''}
6970
value=${customGatewayUrl} />
7071
7172
</div>

add-on/src/options/forms/ipfs-node-form.js

+17-8
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33

44
const browser = require('webextension-polyfill')
55
const html = require('choo/html')
6+
const { hasChromeSocketsForTcp } = require('../../lib/runtime-checks')
67

78
function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
89
const onIpfsNodeTypeChange = onOptionChange('ipfsNodeType')
910
const onIpfsNodeConfigChange = onOptionChange('ipfsNodeConfig')
10-
11+
const withChromeSockets = hasChromeSocketsForTcp()
1112
return html`
1213
<form>
1314
<fieldset>
@@ -18,7 +19,7 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
1819
<dt>${browser.i18n.getMessage('option_ipfsNodeType_title')}</dt>
1920
<dd>
2021
<p>${browser.i18n.getMessage('option_ipfsNodeType_external_description')}</p>
21-
<p>${browser.i18n.getMessage('option_ipfsNodeType_embedded_description')}</p>
22+
<p>${browser.i18n.getMessage(withChromeSockets ? 'option_ipfsNodeType_embedded_chromesockets_description' : 'option_ipfsNodeType_embedded_description')}</p>
2223
<p><a href="https://github.com/ipfs-shipyard/ipfs-companion/blob/master/docs/node-types.md#node-types-in-ipfs-companion" target="_blank">
2324
${browser.i18n.getMessage('option_legend_readMore')}
2425
</a></p>
@@ -31,14 +32,22 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
3132
selected=${ipfsNodeType === 'external'}>
3233
${browser.i18n.getMessage('option_ipfsNodeType_external')}
3334
</option>
34-
<option
35-
value='embedded'
36-
selected=${ipfsNodeType === 'embedded'}>
37-
${browser.i18n.getMessage('option_ipfsNodeType_embedded')}
38-
</option>
35+
${withChromeSockets ? html`
36+
<option
37+
value='embedded:chromesockets'
38+
selected=${ipfsNodeType === 'embedded:chromesockets'}>
39+
${browser.i18n.getMessage('option_ipfsNodeType_embedded_chromesockets')}
40+
</option>
41+
` : html`
42+
<option
43+
value='embedded'
44+
selected=${ipfsNodeType === 'embedded'}>
45+
${browser.i18n.getMessage('option_ipfsNodeType_embedded')}
46+
</option>
47+
`}
3948
</select>
4049
</div>
41-
${ipfsNodeType === 'embedded' ? html`
50+
${ipfsNodeType.startsWith('embedded') ? html`
4251
<div>
4352
<label for="ipfsNodeConfig">
4453
<dl>

0 commit comments

Comments
 (0)