diff --git a/add-on/src/lib/ipfs-client/embedded-chromesockets.js b/add-on/src/lib/ipfs-client/embedded-chromesockets.js index 1e239fe19..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,25 +24,8 @@ 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') - +async function buildConfig (opts) { const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig) - defaultOpts.libp2p = { config: { dht: { @@ -50,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) @@ -95,7 +101,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 @@ -105,9 +110,10 @@ 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) } } @@ -115,77 +121,33 @@ 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() } 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() - 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 :-)' - } + const stopped = new Promise((resolve, reject) => { + node.on('stop', resolve) + node.on('error', reject) }) - await hapiServer.start() - console.log(`[ipfs-companion] require('@hapi/hapi') HTTP server running at: ${hapiServer.info.uri}`) + 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 } - initHapi() - return hapiServer } -*/ 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 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"