From 6b38376d84e05ed668c3419b30672aa2c241a14a Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Mon, 15 Jul 2019 20:57:22 +0200 Subject: [PATCH 1/3] fix(brave): persist External node config In current implementation the Embedded node uses same config keys as External one. When switching between External and Embedded in Brave we now persist the old (External) config and restore it when user switched back to External node type. --- .../lib/ipfs-client/embedded-chromesockets.js | 21 +++++++++++++++++-- add-on/src/lib/ipfs-companion.js | 20 ++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/add-on/src/lib/ipfs-client/embedded-chromesockets.js b/add-on/src/lib/ipfs-client/embedded-chromesockets.js index 1e239fe19..7eecb5c2f 100644 --- a/add-on/src/lib/ipfs-client/embedded-chromesockets.js +++ b/add-on/src/lib/ipfs-client/embedded-chromesockets.js @@ -138,12 +138,29 @@ exports.destroy = async function () { try { await nodeHttpApi.stop() } catch (err) { - log.error('failed to stop HttpApi', err) + // TODO: needs upstream fix like https://github.com/ipfs/js-ipfs/issues/2257 + if (err.message !== 'Cannot stop server while in stopping phase') { + log.error('failed to stop HttpApi', err) + } } nodeHttpApi = null } if (node) { - await node.stop() + const stopped = new Promise((resolve, reject) => { + node.on('stop', resolve) + node.on('error', reject) + }) + try { + await node.stop() + } catch (err) { + // TODO: remove when fixed upstream: https://github.com/ipfs/js-ipfs/issues/2257 + if (err.message === 'Not able to stop from state: stopping') { + log('destroy: embedded:chromesockets waiting for node.stop()') + await stopped + } else { + throw err + } + } node = null } } diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index 714c41d8c..ffc6f455b 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -616,6 +616,26 @@ module.exports = async function init () { shouldStopIpfsClient = !state.active break case 'ipfsNodeType': + // Switching between External and Embeedded HTTP Gateway in Brave is tricky. + // For now we remove user confusion by persisting and restoring the External config. + // TODO: refactor as a part of https://github.com/ipfs-shipyard/ipfs-companion/issues/491 + if (change.oldValue === 'external' && change.newValue === 'embedded:chromesockets') { + const oldGatewayUrl = (await browser.storage.local.get('customGatewayUrl')).customGatewayUrl + const oldApiUrl = (await browser.storage.local.get('ipfsApiUrl')).ipfsApiUrl + log(`storing externalNodeConfig: ipfsApiUrl=${oldApiUrl}, customGatewayUrl=${oldGatewayUrl}"`) + await browser.storage.local.set({ externalNodeConfig: [oldGatewayUrl, oldApiUrl] }) + } else if (change.oldValue === 'embedded:chromesockets' && change.newValue === 'external') { + const [oldGatewayUrl, oldApiUrl] = (await browser.storage.local.get('externalNodeConfig')).externalNodeConfig + log(`restoring externalNodeConfig: ipfsApiUrl=${oldApiUrl}, customGatewayUrl=${oldGatewayUrl}"`) + await browser.storage.local.set({ + ipfsApiUrl: oldApiUrl, + customGatewayUrl: oldGatewayUrl, + externalNodeConfig: null + }) + } + shouldRestartIpfsClient = true + state[key] = change.newValue + break case 'ipfsNodeConfig': shouldRestartIpfsClient = true state[key] = change.newValue From b9a4c377a17c19a5b240fbb564e917a821fe803b Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Mon, 15 Jul 2019 21:41:32 +0200 Subject: [PATCH 2/3] chore: remove unused poc code --- .../lib/ipfs-client/embedded-chromesockets.js | 77 +------------------ 1 file changed, 1 insertion(+), 76 deletions(-) diff --git a/add-on/src/lib/ipfs-client/embedded-chromesockets.js b/add-on/src/lib/ipfs-client/embedded-chromesockets.js index 7eecb5c2f..e1b4ded3b 100644 --- a/add-on/src/lib/ipfs-client/embedded-chromesockets.js +++ b/add-on/src/lib/ipfs-client/embedded-chromesockets.js @@ -23,21 +23,7 @@ const { optionDefaults } = require('../options') let node = null let nodeHttpApi = null -// additional servers for smoke-tests -// let httpServer = null -// let hapiServer = null - exports.init = function init (opts) { - /* - // TEST RAW require('http') SERVER - if (!httpServer) { - httpServer = startRawHttpServer(9091) - } - // TEST require('@hapi/hapi') HTTP SERVER (same as in js-ipfs) - if (!hapiServer) { - hapiServer = startRawHapiServer(9092) - } - */ log('init embedded:chromesockets') const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig) @@ -95,7 +81,6 @@ async function updateConfigWithHttpEndpoints (ipfs, opts) { const apiMa = await ipfs.config.get('Addresses.API') const httpGateway = multiaddr2httpUrl(gwMa) const httpApi = multiaddr2httpUrl(apiMa) - log(`updating extension configuration to Gateway=${httpGateway} and API=${httpApi}`) // update ports in JSON configuration for embedded js-ipfs const ipfsNodeConfig = JSON.parse(localConfig.ipfsNodeConfig) ipfsNodeConfig.config.Addresses.Gateway = gwMa @@ -108,6 +93,7 @@ async function updateConfigWithHttpEndpoints (ipfs, opts) { // update current runtime config (in place, effective without restart) Object.assign(opts, configChanges) // update user config in storage (effective on next run) + log(`synchronizing ipfsNodeConfig with customGatewayUrl (${configChanges.customGatewayUrl}) and ipfsApiUrl (${configChanges.ipfsApiUrl})`) await browser.storage.local.set(configChanges) } } @@ -115,25 +101,6 @@ async function updateConfigWithHttpEndpoints (ipfs, opts) { exports.destroy = async function () { log('destroy: embedded:chromesockets') - /* - if (httpServer) { - httpServer.close() - httpServer = null - } - if (hapiServer) { - try { - await hapiServer.stop({ timeout: 1000 }) - } catch (err) { - if (err) { - console.error(`[ipfs-companion] failed to stop hapi`, err) - } else { - console.log('[ipfs-companion] hapi server stopped') - } - } - hapiServer = null - } - */ - if (nodeHttpApi) { try { await nodeHttpApi.stop() @@ -164,45 +131,3 @@ exports.destroy = async function () { node = null } } - -/* -// Quick smoke-test to confirm require('http') works for MVP -function startRawHttpServer (port) { - const http = require('http') // courtesy of chrome-net - const httpServer = http.createServer(function (req, res) { - res.writeHead(200, { 'Content-Type': 'text/plain' }) - res.end('Hello from ipfs-companion exposing HTTP via chrome.sockets in Brave :-)\n') - }) - httpServer.listen(port, '127.0.0.1') - console.log(`[ipfs-companion] require('http') HTTP server on http://127.0.0.1:${port}`) - return httpServer -} - -function startRawHapiServer (port) { - let options = { - host: '127.0.0.1', - port, - debug: { - log: ['*'], - request: ['*'] - } - } - const initHapi = async () => { - // hapi v18 (js-ipfs >=v0.35.0-pre.0) - const Hapi = require('@hapi/hapi') // courtesy of js-ipfs - const hapiServer = new Hapi.Server(options) - await hapiServer.route({ - method: 'GET', - path: '/', - handler: (request, h) => { - console.log('[ipfs-companion] hapiServer processing request', request) - return 'Hello from ipfs-companion+Hapi.js exposing HTTP via chrome.sockets in Brave :-)' - } - }) - await hapiServer.start() - console.log(`[ipfs-companion] require('@hapi/hapi') HTTP server running at: ${hapiServer.info.uri}`) - } - initHapi() - return hapiServer -} -*/ From b48e643ea55a0c1b4bc283dc2c451cffcd2c86c1 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Mon, 15 Jul 2019 23:00:58 +0200 Subject: [PATCH 3/3] fix(brave): no port collisions Before starting embedded js-ipfs we now check if API and Gateway ports are free. If not, we find available ones and update the config. This way user does not need to deal with "port taken" errors and embedded node provides seamless experience without surprises. --- .../lib/ipfs-client/embedded-chromesockets.js | 36 ++++++++++++++----- package.json | 1 + yarn.lock | 7 ++++ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/add-on/src/lib/ipfs-client/embedded-chromesockets.js b/add-on/src/lib/ipfs-client/embedded-chromesockets.js index e1b4ded3b..8b62ed18f 100644 --- a/add-on/src/lib/ipfs-client/embedded-chromesockets.js +++ b/add-on/src/lib/ipfs-client/embedded-chromesockets.js @@ -16,6 +16,7 @@ const Ipfs = require('ipfs') const HttpApi = require('ipfs/src/http') const multiaddr = require('multiaddr') const maToUri = require('multiaddr-to-uri') +const getPort = require('get-port') const { optionDefaults } = require('../options') @@ -23,11 +24,8 @@ const { optionDefaults } = require('../options') let node = null let nodeHttpApi = null -exports.init = function init (opts) { - log('init embedded:chromesockets') - +async function buildConfig (opts) { const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig) - defaultOpts.libp2p = { config: { dht: { @@ -36,9 +34,31 @@ exports.init = function init (opts) { } } } - const userOpts = JSON.parse(opts.ipfsNodeConfig) - const ipfsOpts = mergeOptions.call({ concatArrays: true }, defaultOpts, userOpts, { start: false }) + const ipfsNodeConfig = mergeOptions.call({ concatArrays: true }, defaultOpts, userOpts, { start: false }) + + // Detect when API or Gateway port is not available (taken by something else) + // We find the next free port and update configuration to use it instead + const multiaddr2port = (ma) => parseInt(new URL(multiaddr2httpUrl(ma)).port, 10) + const gatewayPort = multiaddr2port(ipfsNodeConfig.config.Addresses.Gateway) + const apiPort = multiaddr2port(ipfsNodeConfig.config.Addresses.API) + log(`checking if ports are available: api: ${apiPort}, gateway: ${gatewayPort}`) + const freeGatewayPort = await getPort({ port: getPort.makeRange(gatewayPort, gatewayPort + 100) }) + const freeApiPort = await getPort({ port: getPort.makeRange(apiPort, apiPort + 100) }) + if (gatewayPort !== freeGatewayPort || apiPort !== freeApiPort) { + log(`updating config to available ports: api: ${freeApiPort}, gateway: ${freeGatewayPort}`) + const addrs = ipfsNodeConfig.config.Addresses + addrs.Gateway = addrs.Gateway.replace(gatewayPort.toString(), freeGatewayPort.toString()) + addrs.API = addrs.API.replace(apiPort.toString(), freeApiPort.toString()) + } + + return ipfsNodeConfig +} + +exports.init = async function init (opts) { + log('init embedded:chromesockets') + + const ipfsOpts = await buildConfig(opts) log('creating js-ipfs with opts: ', ipfsOpts) node = new Ipfs(ipfsOpts) @@ -90,9 +110,9 @@ async function updateConfigWithHttpEndpoints (ipfs, opts) { ipfsApiUrl: httpApi, ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2) } - // update current runtime config (in place, effective without restart) + // update current runtime config (in place) Object.assign(opts, configChanges) - // update user config in storage (effective on next run) + // update user config in storage (triggers async client restart if ports changed) log(`synchronizing ipfsNodeConfig with customGatewayUrl (${configChanges.customGatewayUrl}) and ipfsApiUrl (${configChanges.ipfsApiUrl})`) await browser.storage.local.set(configChanges) } diff --git a/package.json b/package.json index 949678081..c43307387 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "drag-and-drop-files": "0.0.1", "file-type": "12.0.1", "filesize": "4.1.2", + "get-port": "5.0.0", "http-dns": "3.0.1", "http-node": "1.2.0", "ipfs": "https://github.com/ipfs/js-ipfs/tarball/2ae6b672c222555b1a068141f2acfe4b5f39b709/js-ipfs.tar.gz", diff --git a/yarn.lock b/yarn.lock index 03246b2ac..315203e7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5786,6 +5786,13 @@ get-iterator@^1.0.2: resolved "https://registry.yarnpkg.com/get-iterator/-/get-iterator-1.0.2.tgz#cd747c02b4c084461fac14f48f6b45a80ed25c82" integrity sha512-v+dm9bNVfOYsY1OrhaCrmyOcYoSeVvbt+hHZ0Au+T+p1y+0Uyj9aMaGIeUTT6xdpRbWzDeYKvfOslPhggQMcsg== +get-port@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.0.0.tgz#aa22b6b86fd926dd7884de3e23332c9f70c031a6" + integrity sha512-imzMU0FjsZqNa6BqOjbbW6w5BivHIuQKopjpPqcnx0AVHJQKCxK1O+Ab3OrVXhrekqfVMjwA9ZYu062R+KcIsQ== + dependencies: + type-fest "^0.3.0" + get-port@^4.0.0: version "4.2.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119"