Skip to content

Commit 2a70ff6

Browse files
authored
fix(brave): port collisions of embedded js-ipfs (#734)
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.
2 parents 8bc27a2 + b48e643 commit 2a70ff6

File tree

4 files changed

+76
-86
lines changed

4 files changed

+76
-86
lines changed

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

+48-86
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,16 @@ const Ipfs = require('ipfs')
1616
const HttpApi = require('ipfs/src/http')
1717
const multiaddr = require('multiaddr')
1818
const maToUri = require('multiaddr-to-uri')
19+
const getPort = require('get-port')
1920

2021
const { optionDefaults } = require('../options')
2122

2223
// js-ipfs with embedded hapi HTTP server
2324
let node = null
2425
let nodeHttpApi = null
2526

26-
// additional servers for smoke-tests
27-
// let httpServer = null
28-
// let hapiServer = null
29-
30-
exports.init = function init (opts) {
31-
/*
32-
// TEST RAW require('http') SERVER
33-
if (!httpServer) {
34-
httpServer = startRawHttpServer(9091)
35-
}
36-
// TEST require('@hapi/hapi') HTTP SERVER (same as in js-ipfs)
37-
if (!hapiServer) {
38-
hapiServer = startRawHapiServer(9092)
39-
}
40-
*/
41-
log('init embedded:chromesockets')
42-
27+
async function buildConfig (opts) {
4328
const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig)
44-
4529
defaultOpts.libp2p = {
4630
config: {
4731
dht: {
@@ -50,9 +34,31 @@ exports.init = function init (opts) {
5034
}
5135
}
5236
}
53-
5437
const userOpts = JSON.parse(opts.ipfsNodeConfig)
55-
const ipfsOpts = mergeOptions.call({ concatArrays: true }, defaultOpts, userOpts, { start: false })
38+
const ipfsNodeConfig = mergeOptions.call({ concatArrays: true }, defaultOpts, userOpts, { start: false })
39+
40+
// Detect when API or Gateway port is not available (taken by something else)
41+
// We find the next free port and update configuration to use it instead
42+
const multiaddr2port = (ma) => parseInt(new URL(multiaddr2httpUrl(ma)).port, 10)
43+
const gatewayPort = multiaddr2port(ipfsNodeConfig.config.Addresses.Gateway)
44+
const apiPort = multiaddr2port(ipfsNodeConfig.config.Addresses.API)
45+
log(`checking if ports are available: api: ${apiPort}, gateway: ${gatewayPort}`)
46+
const freeGatewayPort = await getPort({ port: getPort.makeRange(gatewayPort, gatewayPort + 100) })
47+
const freeApiPort = await getPort({ port: getPort.makeRange(apiPort, apiPort + 100) })
48+
if (gatewayPort !== freeGatewayPort || apiPort !== freeApiPort) {
49+
log(`updating config to available ports: api: ${freeApiPort}, gateway: ${freeGatewayPort}`)
50+
const addrs = ipfsNodeConfig.config.Addresses
51+
addrs.Gateway = addrs.Gateway.replace(gatewayPort.toString(), freeGatewayPort.toString())
52+
addrs.API = addrs.API.replace(apiPort.toString(), freeApiPort.toString())
53+
}
54+
55+
return ipfsNodeConfig
56+
}
57+
58+
exports.init = async function init (opts) {
59+
log('init embedded:chromesockets')
60+
61+
const ipfsOpts = await buildConfig(opts)
5662
log('creating js-ipfs with opts: ', ipfsOpts)
5763
node = new Ipfs(ipfsOpts)
5864

@@ -95,7 +101,6 @@ async function updateConfigWithHttpEndpoints (ipfs, opts) {
95101
const apiMa = await ipfs.config.get('Addresses.API')
96102
const httpGateway = multiaddr2httpUrl(gwMa)
97103
const httpApi = multiaddr2httpUrl(apiMa)
98-
log(`updating extension configuration to Gateway=${httpGateway} and API=${httpApi}`)
99104
// update ports in JSON configuration for embedded js-ipfs
100105
const ipfsNodeConfig = JSON.parse(localConfig.ipfsNodeConfig)
101106
ipfsNodeConfig.config.Addresses.Gateway = gwMa
@@ -105,87 +110,44 @@ async function updateConfigWithHttpEndpoints (ipfs, opts) {
105110
ipfsApiUrl: httpApi,
106111
ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2)
107112
}
108-
// update current runtime config (in place, effective without restart)
113+
// update current runtime config (in place)
109114
Object.assign(opts, configChanges)
110-
// update user config in storage (effective on next run)
115+
// update user config in storage (triggers async client restart if ports changed)
116+
log(`synchronizing ipfsNodeConfig with customGatewayUrl (${configChanges.customGatewayUrl}) and ipfsApiUrl (${configChanges.ipfsApiUrl})`)
111117
await browser.storage.local.set(configChanges)
112118
}
113119
}
114120

115121
exports.destroy = async function () {
116122
log('destroy: embedded:chromesockets')
117123

118-
/*
119-
if (httpServer) {
120-
httpServer.close()
121-
httpServer = null
122-
}
123-
if (hapiServer) {
124-
try {
125-
await hapiServer.stop({ timeout: 1000 })
126-
} catch (err) {
127-
if (err) {
128-
console.error(`[ipfs-companion] failed to stop hapi`, err)
129-
} else {
130-
console.log('[ipfs-companion] hapi server stopped')
131-
}
132-
}
133-
hapiServer = null
134-
}
135-
*/
136-
137124
if (nodeHttpApi) {
138125
try {
139126
await nodeHttpApi.stop()
140127
} catch (err) {
141-
log.error('failed to stop HttpApi', err)
128+
// TODO: needs upstream fix like https://github.com/ipfs/js-ipfs/issues/2257
129+
if (err.message !== 'Cannot stop server while in stopping phase') {
130+
log.error('failed to stop HttpApi', err)
131+
}
142132
}
143133
nodeHttpApi = null
144134
}
145135
if (node) {
146-
await node.stop()
147-
node = null
148-
}
149-
}
150-
151-
/*
152-
// Quick smoke-test to confirm require('http') works for MVP
153-
function startRawHttpServer (port) {
154-
const http = require('http') // courtesy of chrome-net
155-
const httpServer = http.createServer(function (req, res) {
156-
res.writeHead(200, { 'Content-Type': 'text/plain' })
157-
res.end('Hello from ipfs-companion exposing HTTP via chrome.sockets in Brave :-)\n')
158-
})
159-
httpServer.listen(port, '127.0.0.1')
160-
console.log(`[ipfs-companion] require('http') HTTP server on http://127.0.0.1:${port}`)
161-
return httpServer
162-
}
163-
164-
function startRawHapiServer (port) {
165-
let options = {
166-
host: '127.0.0.1',
167-
port,
168-
debug: {
169-
log: ['*'],
170-
request: ['*']
171-
}
172-
}
173-
const initHapi = async () => {
174-
// hapi v18 (js-ipfs >=v0.35.0-pre.0)
175-
const Hapi = require('@hapi/hapi') // courtesy of js-ipfs
176-
const hapiServer = new Hapi.Server(options)
177-
await hapiServer.route({
178-
method: 'GET',
179-
path: '/',
180-
handler: (request, h) => {
181-
console.log('[ipfs-companion] hapiServer processing request', request)
182-
return 'Hello from ipfs-companion+Hapi.js exposing HTTP via chrome.sockets in Brave :-)'
183-
}
136+
const stopped = new Promise((resolve, reject) => {
137+
node.on('stop', resolve)
138+
node.on('error', reject)
184139
})
185-
await hapiServer.start()
186-
console.log(`[ipfs-companion] require('@hapi/hapi') HTTP server running at: ${hapiServer.info.uri}`)
140+
try {
141+
await node.stop()
142+
} catch (err) {
143+
// TODO: remove when fixed upstream: https://github.com/ipfs/js-ipfs/issues/2257
144+
if (err.message === 'Not able to stop from state: stopping') {
145+
log('destroy: embedded:chromesockets waiting for node.stop()')
146+
await stopped
147+
} else {
148+
throw err
149+
}
150+
}
151+
node = null
187152
}
188-
initHapi()
189-
return hapiServer
190153
}
191-
*/

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

+20
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,26 @@ module.exports = async function init () {
616616
shouldStopIpfsClient = !state.active
617617
break
618618
case 'ipfsNodeType':
619+
// Switching between External and Embeedded HTTP Gateway in Brave is tricky.
620+
// For now we remove user confusion by persisting and restoring the External config.
621+
// TODO: refactor as a part of https://github.com/ipfs-shipyard/ipfs-companion/issues/491
622+
if (change.oldValue === 'external' && change.newValue === 'embedded:chromesockets') {
623+
const oldGatewayUrl = (await browser.storage.local.get('customGatewayUrl')).customGatewayUrl
624+
const oldApiUrl = (await browser.storage.local.get('ipfsApiUrl')).ipfsApiUrl
625+
log(`storing externalNodeConfig: ipfsApiUrl=${oldApiUrl}, customGatewayUrl=${oldGatewayUrl}"`)
626+
await browser.storage.local.set({ externalNodeConfig: [oldGatewayUrl, oldApiUrl] })
627+
} else if (change.oldValue === 'embedded:chromesockets' && change.newValue === 'external') {
628+
const [oldGatewayUrl, oldApiUrl] = (await browser.storage.local.get('externalNodeConfig')).externalNodeConfig
629+
log(`restoring externalNodeConfig: ipfsApiUrl=${oldApiUrl}, customGatewayUrl=${oldGatewayUrl}"`)
630+
await browser.storage.local.set({
631+
ipfsApiUrl: oldApiUrl,
632+
customGatewayUrl: oldGatewayUrl,
633+
externalNodeConfig: null
634+
})
635+
}
636+
shouldRestartIpfsClient = true
637+
state[key] = change.newValue
638+
break
619639
case 'ipfsNodeConfig':
620640
shouldRestartIpfsClient = true
621641
state[key] = change.newValue

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"drag-and-drop-files": "0.0.1",
122122
"file-type": "12.0.1",
123123
"filesize": "4.1.2",
124+
"get-port": "5.0.0",
124125
"http-dns": "3.0.1",
125126
"http-node": "1.2.0",
126127
"ipfs": "https://github.com/ipfs/js-ipfs/tarball/2ae6b672c222555b1a068141f2acfe4b5f39b709/js-ipfs.tar.gz",

yarn.lock

+7
Original file line numberDiff line numberDiff line change
@@ -5786,6 +5786,13 @@ get-iterator@^1.0.2:
57865786
resolved "https://registry.yarnpkg.com/get-iterator/-/get-iterator-1.0.2.tgz#cd747c02b4c084461fac14f48f6b45a80ed25c82"
57875787
integrity sha512-v+dm9bNVfOYsY1OrhaCrmyOcYoSeVvbt+hHZ0Au+T+p1y+0Uyj9aMaGIeUTT6xdpRbWzDeYKvfOslPhggQMcsg==
57885788

5789+
5790+
version "5.0.0"
5791+
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.0.0.tgz#aa22b6b86fd926dd7884de3e23332c9f70c031a6"
5792+
integrity sha512-imzMU0FjsZqNa6BqOjbbW6w5BivHIuQKopjpPqcnx0AVHJQKCxK1O+Ab3OrVXhrekqfVMjwA9ZYu062R+KcIsQ==
5793+
dependencies:
5794+
type-fest "^0.3.0"
5795+
57895796
get-port@^4.0.0:
57905797
version "4.2.0"
57915798
resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119"

0 commit comments

Comments
 (0)