Skip to content

Commit

Permalink
fix(brave): port collisions of embedded js-ipfs (#734)
Browse files Browse the repository at this point in the history
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.

fix(brave): persist External node config
    
    Embedded node used 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.
  • Loading branch information
lidel authored Jul 16, 2019
2 parents 8bc27a2 + b48e643 commit 2a70ff6
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 86 deletions.
134 changes: 48 additions & 86 deletions add-on/src/lib/ipfs-client/embedded-chromesockets.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,16 @@ 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')

// js-ipfs with embedded hapi HTTP server
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: {
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -105,87 +110,44 @@ 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)
}
}

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
}
*/
20 changes: 20 additions & 0 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 2a70ff6

Please sign in to comment.