diff --git a/add-on/_locales/en/messages.json b/add-on/_locales/en/messages.json index 7374a55f6..f241e0783 100644 --- a/add-on/_locales/en/messages.json +++ b/add-on/_locales/en/messages.json @@ -227,12 +227,16 @@ "message": "Embedded (experimental): run js-ipfs node in your browser (use only for development, read about its limitations under the link below)", "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" }, + "option_ipfsNodeType_embedded_chromesockets_description": { + "message": "Embedded with Chrome Sockets (experimental): run js-ipfs node in your browser with access to chrome.sockets APIs (details under the link below)", + "description": "An option description on the Preferences screen (option_ipfsNodeType_description)" + }, "option_ipfsNodeConfig_title": { "message": "IPFS Node Config", "description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)" }, "option_ipfsNodeConfig_description": { - "message": "Configuration for the embedded IPFS node. Must be valid JSON.", + "message": "Additional configuration for the embedded IPFS node (arrays will be merged). Must be valid JSON.", "description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)" }, "option_ipfsNodeType_external": { @@ -243,6 +247,10 @@ "message": "Embedded", "description": "An option on the Preferences screen (option_ipfsNodeType_embedded)" }, + "option_ipfsNodeType_embedded_chromesockets": { + "message": "Embedded + chrome.sockets", + "description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)" + }, "option_header_gateways": { "message": "Gateways", "description": "A section header on the Preferences screen (option_header_gateways)" diff --git a/add-on/src/lib/dnslink.js b/add-on/src/lib/dnslink.js index 1107f1a7b..a39332179 100644 --- a/add-on/src/lib/dnslink.js +++ b/add-on/src/lib/dnslink.js @@ -105,7 +105,7 @@ module.exports = function createDnslinkResolver (getState) { readDnslinkFromTxtRecord (fqdn) { const state = getState() let apiProvider - if (state.ipfsNodeType === 'external' && state.peerCount !== offlinePeerCount) { + if (state.ipfsNodeType !== 'embedded' && state.peerCount !== offlinePeerCount) { apiProvider = state.apiURLString } else { // fallback to resolver at public gateway diff --git a/add-on/src/lib/ipfs-client/embedded-brave.js b/add-on/src/lib/ipfs-client/embedded-chromesockets.js similarity index 69% rename from add-on/src/lib/ipfs-client/embedded-brave.js rename to add-on/src/lib/ipfs-client/embedded-chromesockets.js index 31e9cde0c..c07d71976 100644 --- a/add-on/src/lib/ipfs-client/embedded-brave.js +++ b/add-on/src/lib/ipfs-client/embedded-chromesockets.js @@ -1,14 +1,18 @@ 'use strict' /* eslint-env browser, webextensions */ +const browser = require('webextension-polyfill') +const debug = require('debug') // Polyfills required by embedded HTTP server const uptimeStart = Date.now() process.uptime = () => Math.floor((Date.now() - uptimeStart) / 1000) process.hrtime = require('browser-process-hrtime') -const defaultsDeep = require('@nodeutils/defaults-deep') +const mergeOptions = require('merge-options') const Ipfs = require('ipfs') const HttpApi = require('ipfs/src/http') +const multiaddr = require('multiaddr') +const maToUri = require('multiaddr-to-uri') const { optionDefaults } = require('../options') @@ -25,6 +29,9 @@ let nodeHttpApi = null // to include everything (mplex, libp2p, mss): localStorage.debug = '*' localStorage.debug = 'jsipfs*,ipfs*,-*:mfs*,-*:ipns*,-ipfs:preload*' +const log = debug('ipfs-companion:client:embedded') +log.error = debug('ipfs-companion:client:embedded:error') + exports.init = function init (opts) { /* // TEST RAW require('http') SERVER @@ -36,17 +43,25 @@ exports.init = function init (opts) { hapiServer = startRawHapiServer(9092) } */ - console.log('[ipfs-companion] Embedded ipfs init') + log('init: embedded js-ipfs+chrome.sockets') - const defaultOpts = optionDefaults.ipfsNodeConfig - const userOpts = JSON.parse(opts.ipfsNodeConfig) - const ipfsOpts = defaultsDeep(defaultOpts, userOpts, { start: false }) + const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig) + defaultOpts.libp2p = { + config: { + dht: { + enabled: false + } + } + } + const userOpts = JSON.parse(opts.ipfsNodeConfig) + const ipfsOpts = mergeOptions.call({ concatArrays: true }, defaultOpts, userOpts, { start: false }) + log('creating js-ipfs with opts: ', ipfsOpts) node = new Ipfs(ipfsOpts) return new Promise((resolve, reject) => { node.once('error', (error) => { - console.error('[ipfs-companion] Something went terribly wrong during startup of js-ipfs!', error) + log.error('something went terribly wrong during startup of js-ipfs!', error) reject(error) }) node.once('ready', async () => { @@ -55,13 +70,14 @@ exports.init = function init (opts) { try { const httpServers = new HttpApi(node, ipfsOpts) nodeHttpApi = await httpServers.start() + await updateConfigWithHttpEndpoints(node) resolve(node) } catch (err) { reject(err) } }) node.on('error', error => { - console.error('[ipfs-companion] Something went terribly wrong in embedded js-ipfs!', error) + log.error('something went terribly wrong in embedded js-ipfs!', error) }) try { await node.start() @@ -72,8 +88,21 @@ exports.init = function init (opts) { }) } +// Update internal configuration to HTTP Endpoints from js-ipfs instance +async function updateConfigWithHttpEndpoints (ipfs) { + const ma = await ipfs.config.get('Addresses.Gateway') + log(`synchronizing Addresses.Gateway=${ma} to customGatewayUrl and ipfsNodeConfig`) + const httpGateway = maToUri(ma.includes('/http') ? ma : multiaddr(ma).encapsulate('/http')) + const ipfsNodeConfig = JSON.parse((await browser.storage.local.get('ipfsNodeConfig')).ipfsNodeConfig) + ipfsNodeConfig.config.Addresses.Gateway = ma + await browser.storage.local.set({ + customGatewayUrl: httpGateway, + ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2) + }) +} + exports.destroy = async function () { - console.log('[ipfs-companion] Embedded ipfs destroy') + log('destroy: embedded js-ipfs+chrome.sockets') /* if (httpServer) { @@ -98,7 +127,7 @@ exports.destroy = async function () { try { await nodeHttpApi.stop() } catch (err) { - console.error(`[ipfs-companion] failed to stop HttpApi`, err) + log.error('failed to stop HttpApi', err) } nodeHttpApi = null } diff --git a/add-on/src/lib/ipfs-client/embedded.js b/add-on/src/lib/ipfs-client/embedded.js index d49ef758a..a795eb04d 100644 --- a/add-on/src/lib/ipfs-client/embedded.js +++ b/add-on/src/lib/ipfs-client/embedded.js @@ -1,6 +1,6 @@ 'use strict' -const defaultsDeep = require('@nodeutils/defaults-deep') +const mergeOptions = require('merge-options') const Ipfs = require('ipfs') const { optionDefaults } = require('../options') @@ -9,9 +9,9 @@ let node = null exports.init = function init (opts) { console.log('[ipfs-companion] Embedded ipfs init') - const defaultOpts = optionDefaults.ipfsNodeConfig + const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig) const userOpts = JSON.parse(opts.ipfsNodeConfig) - const ipfsOpts = defaultsDeep(defaultOpts, userOpts, { start: false }) + const ipfsOpts = mergeOptions.call({ concatArrays: true }, defaultOpts, userOpts, { start: false }) node = new Ipfs(ipfsOpts) diff --git a/add-on/src/lib/ipfs-client/index.js b/add-on/src/lib/ipfs-client/index.js index bf4cfb139..abfc485ff 100644 --- a/add-on/src/lib/ipfs-client/index.js +++ b/add-on/src/lib/ipfs-client/index.js @@ -2,28 +2,25 @@ /* eslint-env browser, webextensions */ +const debug = require('debug') +const log = debug('ipfs-companion:client') +log.error = debug('ipfs-companion:client:error') + const browser = require('webextension-polyfill') const external = require('./external') -const embeddedJs = require('./embedded') -const embeddedJsBrave = require('./embedded-brave') +const embedded = require('./embedded') +const embeddedWithChromeSockets = require('./embedded-chromesockets') let client -// TODO: make generic -const hasChromeSocketsForTcp = typeof chrome === 'object' && - typeof chrome.runtime === 'object' && - typeof chrome.runtime.id === 'string' && - typeof chrome.sockets === 'object' && - typeof chrome.sockets.tcpServer === 'object' && - typeof chrome.sockets === 'object' && - typeof chrome.sockets.tcp === 'object' - async function initIpfsClient (opts) { await destroyIpfsClient() - switch (opts.ipfsNodeType) { case 'embedded': - client = hasChromeSocketsForTcp ? embeddedJsBrave : embeddedJs // TODO: make generic + client = embedded + break + case 'embedded:chromesockets': + client = embeddedWithChromeSockets break case 'external': client = external @@ -63,7 +60,7 @@ async function _reloadIpfsClientDependents () { // detect bundled webui in any of open tabs if (_isWebuiTab(tab.url)) { browser.tabs.reload(tab.id) - console.log('[ipfs-companion] reloading bundled webui') + log('reloading bundled webui') } }) } diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index 2be52b21e..eb2396e9a 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -12,7 +12,7 @@ const { initIpfsClient, destroyIpfsClient } = require('./ipfs-client') const { createIpfsUrlProtocolHandler } = require('./ipfs-protocol') const createNotifier = require('./notifier') const createCopier = require('./copier') -const createRuntimeChecks = require('./runtime-checks') +const { createRuntimeChecks } = require('./runtime-checks') const { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress } = require('./context-menus') const createIpfsProxy = require('./ipfs-proxy') const { showPendingLandingPages } = require('./on-installed') diff --git a/add-on/src/lib/ipfs-path.js b/add-on/src/lib/ipfs-path.js index c393b93b7..4ff23ce21 100644 --- a/add-on/src/lib/ipfs-path.js +++ b/add-on/src/lib/ipfs-path.js @@ -85,7 +85,7 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { // Test if actions such as 'per site redirect toggle' should be enabled for the URL isRedirectPageActionsContext (url) { const state = getState() - return state.ipfsNodeType === 'external' && // hide with embedded node + return state.ipfsNodeType !== 'embedded' && // hide with embedded node (IsIpfs.ipnsUrl(url) || // show on /ipns/ (url.startsWith('http') && // hide on non-HTTP pages !url.startsWith(state.gwURLString) && // hide on /ipfs/* diff --git a/add-on/src/lib/options.js b/add-on/src/lib/options.js index fdb3a4e54..8ab70ac44 100644 --- a/add-on/src/lib/options.js +++ b/add-on/src/lib/options.js @@ -1,28 +1,14 @@ 'use strict' const isFQDN = require('is-fqdn') +const { hasChromeSocketsForTcp } = require('./runtime-checks') exports.optionDefaults = Object.freeze({ active: true, // global ON/OFF switch, overrides everything else - ipfsNodeType: 'embedded', // Brave should default to js-ipfs: https://github.com/ipfs-shipyard/ipfs-companion/issues/664 - ipfsNodeConfig: JSON.stringify({ - config: { - Addresses: { - Swarm: [], - API: '/ip4/127.0.0.1/tcp/5002', - Gateway: '/ip4/127.0.0.1/tcp/9090' - } - }, - libp2p: { - config: { - dht: { - enabled: false - } - } - } - }, null, 2), + ipfsNodeType: buildDefaultIpfsNodeType(), + ipfsNodeConfig: buildDefaultIpfsNodeConfig(), publicGatewayUrl: 'https://ipfs.io', - useCustomGateway: false, // TODO: Brave should not redirect to public one, but own + useCustomGateway: true, noRedirectHostnames: [], automaticMode: true, linkify: false, @@ -37,6 +23,27 @@ exports.optionDefaults = Object.freeze({ ipfsProxy: true // window.ipfs }) +function buildDefaultIpfsNodeType () { + // Right now Brave is the only vendor giving us access to chrome.sockets + return hasChromeSocketsForTcp() ? 'embedded:chromesockets' : 'external' +} + +function buildDefaultIpfsNodeConfig () { + let config = { + config: { + Addresses: { + Swarm: [] + } + } + } + if (hasChromeSocketsForTcp()) { + // config.config.Addresses.API = '/ip4/127.0.0.1/tcp/5002' + config.config.Addresses.API = '' // disable API port + config.config.Addresses.Gateway = '/ip4/127.0.0.1/tcp/8080' + } + return JSON.stringify(config, null, 2) +} + // `storage` should be a browser.storage.local or similar exports.storeMissingOptions = (read, defaults, storage) => { const requiredKeys = Object.keys(defaults) @@ -105,4 +112,16 @@ exports.migrateOptions = async (storage) => { }) await storage.remove('dnslink') } + // ~ v2.8.x + Brave + // Upgrade js-ipfs to js-ipfs + chrome.sockets + const { ipfsNodeType } = await storage.get('ipfsNodeType') + if (ipfsNodeType === 'embedded' && hasChromeSocketsForTcp()) { + console.log(`[ipfs-companion] migrating ipfsNodeType to 'embedded:chromesockets'`) + // Overwrite old config + const ipfsNodeConfig = JSON.parse(exports.optionDefaults.ipfsNodeConfig) + await storage.set({ + ipfsNodeType: 'embedded:chromesockets', + ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2) + }) + } } diff --git a/add-on/src/lib/runtime-checks.js b/add-on/src/lib/runtime-checks.js index f164b2b07..81eb41d8b 100644 --- a/add-on/src/lib/runtime-checks.js +++ b/add-on/src/lib/runtime-checks.js @@ -39,9 +39,11 @@ async function createRuntimeChecks (browser) { browser, isFirefox: runtimeIsFirefox, isAndroid: runtimeIsAndroid, + isBrave: runtimeHasSocketsForTcp, // TODO: make it more robust hasChromeSocketsForTcp: runtimeHasSocketsForTcp, hasNativeProtocolHandler: runtimeHasNativeProtocol }) } -module.exports = createRuntimeChecks +module.exports.createRuntimeChecks = createRuntimeChecks +module.exports.hasChromeSocketsForTcp = hasChromeSocketsForTcp diff --git a/add-on/src/options/forms/gateways-form.js b/add-on/src/options/forms/gateways-form.js index b16f0e763..724619e98 100644 --- a/add-on/src/options/forms/gateways-form.js +++ b/add-on/src/options/forms/gateways-form.js @@ -23,7 +23,7 @@ function gatewaysForm ({ const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', normalizeGatewayURL) const onNoRedirectHostnamesChange = onOptionChange('noRedirectHostnames', hostTextToArray) const mixedContentWarning = !secureContextUrl.test(customGatewayUrl) - const supportRedirectToCustomGateway = ipfsNodeType === 'external' + const supportRedirectToCustomGateway = ipfsNodeType !== 'embedded' return html`
@@ -66,6 +66,7 @@ function gatewaysForm ({ spellcheck="false" title="Enter URL without any sub-path" onchange=${onCustomGatewayUrlChange} + ${ipfsNodeType !== 'external' ? 'disabled' : ''} value=${customGatewayUrl} /> diff --git a/add-on/src/options/forms/ipfs-node-form.js b/add-on/src/options/forms/ipfs-node-form.js index 3ae72e8ce..036e35b5b 100644 --- a/add-on/src/options/forms/ipfs-node-form.js +++ b/add-on/src/options/forms/ipfs-node-form.js @@ -3,11 +3,12 @@ const browser = require('webextension-polyfill') const html = require('choo/html') +const { hasChromeSocketsForTcp } = require('../../lib/runtime-checks') function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) { const onIpfsNodeTypeChange = onOptionChange('ipfsNodeType') const onIpfsNodeConfigChange = onOptionChange('ipfsNodeConfig') - + const withChromeSockets = hasChromeSocketsForTcp() return html`
@@ -18,7 +19,7 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
${browser.i18n.getMessage('option_ipfsNodeType_title')}

${browser.i18n.getMessage('option_ipfsNodeType_external_description')}

-

${browser.i18n.getMessage('option_ipfsNodeType_embedded_description')}

+

${browser.i18n.getMessage(withChromeSockets ? 'option_ipfsNodeType_embedded_chromesockets_description' : 'option_ipfsNodeType_embedded_description')}

${browser.i18n.getMessage('option_legend_readMore')}

@@ -31,14 +32,22 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) { selected=${ipfsNodeType === 'external'}> ${browser.i18n.getMessage('option_ipfsNodeType_external')} - + ${withChromeSockets ? html` + + ` : html` + + `} - ${ipfsNodeType === 'embedded' ? html` + ${ipfsNodeType.startsWith('embedded') ? html`