Skip to content

Commit

Permalink
fix: feature-detection for chrome.sockets
Browse files Browse the repository at this point in the history
chrome-net patch:
https://github.com/lidel/chrome-net/commit/838ffde4a33721888f74783821e0486dfcc88797

  chrome-net did not check if chrome.sockets.* actually exist.
  This caused problems with extensions built for Chromium-based browsers
  that tried to maintain the single codebase and do feature-detection
  at runtime.

feature detection for embedded js-ipfs:

  If chrome.sockets is available, embedded-brave.js will be used
  instead of regular embedded.js

notify user about socket errors:

  If port is already taken, a notification is displayed.
  We will most likely improve this soon (eg. by finding the next free port)
  but is good enough for now.
  • Loading branch information
lidel committed Apr 4, 2019
1 parent 04e842d commit b441d27
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 130 deletions.
151 changes: 151 additions & 0 deletions add-on/src/lib/ipfs-client/embedded-brave.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
'use strict'
/* eslint-env browser, webextensions */

// 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 Ipfs = require('ipfs')
const HttpApi = require('ipfs/src/http')

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

// Enable some debug output from js-ipfs
// (borrowed from https://github.com/ipfs-shipyard/ipfs-companion/pull/557)
// to include everything (mplex, libp2p, mss): localStorage.debug = '*'
localStorage.debug = 'jsipfs*,ipfs*,-*:mfs*,-*:ipns*,-ipfs:preload*'

exports.init = function init (opts) {
/*
// TEST RAW require('http') SERVER
if (!httpServer) {
httpServer = startRawHttpServer(9091)
}
// TEST require('hapi') HTTP SERVER (same as in js-ipfs)
if (!hapiServer) {
hapiServer = startRawHapiServer(9092)
}
*/
console.log('[ipfs-companion] Embedded ipfs init')

const defaultOpts = optionDefaults.ipfsNodeConfig
const userOpts = JSON.parse(opts.ipfsNodeConfig)
const ipfsOpts = defaultsDeep(defaultOpts, userOpts, { start: false })

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)
reject(error)
})
node.once('ready', async () => {
node.on('start', async () => {
// HttpApi is off in browser context and needs to be started separately
try {
const httpServers = new HttpApi(node, ipfsOpts)
nodeHttpApi = await httpServers.start()
resolve(node)
} catch (err) {
reject(err)
}
})
node.on('error', error => {
console.error('[ipfs-companion] Something went terribly wrong in embedded js-ipfs!', error)
})
try {
await node.start()
} catch (err) {
reject(err)
}
})
})
}

exports.destroy = async function () {
console.log('[ipfs-companion] Embedded ipfs destroy')

/*
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) {
console.error(`[ipfs-companion] 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') // 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') HTTP server running at: ${hapiServer.info.uri}`)
}
initHapi()
return hapiServer
}
*/
136 changes: 17 additions & 119 deletions add-on/src/lib/ipfs-client/embedded.js
Original file line number Diff line number Diff line change
@@ -1,147 +1,45 @@
'use strict'
/* eslint-env browser, webextensions */

// Required by HTTP server
process.hrtime = require('browser-process-hrtime')
const uptimeStart = Date.now()
process.uptime = () => Math.floor((Date.now() - uptimeStart) / 1000)

const defaultsDeep = require('@nodeutils/defaults-deep')
const Ipfs = require('ipfs')
const HttpApi = require('ipfs/src/http')

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

// Enable some debug output from js-ipfs
// (borrowed from https://github.com/ipfs-shipyard/ipfs-companion/pull/557)
// to include everything (mplex, libp2p, mss): localStorage.debug = '*'
localStorage.debug = 'jsipfs*,ipfs*,-*:mfs*,-*:ipns*,-ipfs:preload*'

// 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') // 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') HTTP server running at: ${hapiServer.info.uri}`)
}
initHapi()
return hapiServer
}

exports.init = function init (opts) {
// BRAVE TESTS FIRST
// TODO: remove after experiments are done
// =======================================
// [x] start raw http server (http.createServer)
// [x] start raw Hapi server (Hapi.Server)
// [x] return response
// [ ] start js-ipfs with Gateway exposed by embedded Hapi server
// - [x] disabling DHT in libp2p solved `TypeError: this._dht.on is not a function`,
// - [x] API port starts and returns valid response
// - [ ] Gateway port starts, but returns invalid response for things different than QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn
// =======================================
// TEST RAW require('http') SERVER
if (!httpServer) {
httpServer = startRawHttpServer(9091)
}
// =======================================
// TEST require('hapi') HTTP SERVER (same as in js-ipfs)
if (!hapiServer) {
hapiServer = startRawHapiServer(9092)
}
// =======================================
// Resume regular startup
console.log('[ipfs-companion] Embedded ipfs init')

// TODO: replace with @nodeutils/defaults-deep
const ipfsOpts = Object.assign(JSON.parse(opts.ipfsNodeConfig || optionDefaults.ipfsNodeConfig), { start: false })
const defaultOpts = optionDefaults.ipfsNodeConfig
const userOpts = JSON.parse(opts.ipfsNodeConfig)
const ipfsOpts = defaultsDeep(defaultOpts, userOpts, { start: false })

node = new Ipfs(ipfsOpts)

return new Promise((resolve, reject) => {
node.on('start', async () => {
// HttpApi is off in browser context and needs to be started separately
const httpServers = new HttpApi(node, ipfsOpts)
nodeHttpApi = await httpServers.start()
return resolve(node)
})
node.once('error', (error) => {
console.error('[ipfs-companion] Something went terribly wrong during startup of js-ipfs!', error)
reject(error)
})
node.once('ready', () => {
node.once('ready', async () => {
node.on('start', () => {
resolve(node)
})
node.on('error', error => {
console.error('[ipfs-companion] Something went terribly wrong in embedded js-ipfs!', error)
})
node.start()
try {
await node.start()
} catch (err) {
reject(err)
}
})
})
}

exports.destroy = async function () {
console.log('[ipfs-companion] Embedded ipfs destroy')
if (!node) return

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) {
console.error(`[ipfs-companion] failed to stop HttpApi`, err)
}
nodeHttpApi = null
}
if (node) {
await node.stop()
node = null
}
await node.stop()
node = null
}
27 changes: 22 additions & 5 deletions add-on/src/lib/ipfs-client/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
'use strict'

/* eslint-env browser, webextensions */

const browser = require('webextension-polyfill')
const external = require('./external')
const embedded = require('./embedded')
const embeddedJs = require('./embedded')
const embeddedJsBrave = require('./embedded-brave')

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()

if (opts.ipfsNodeType === 'embedded') {
client = embedded
} else {
client = external
switch (opts.ipfsNodeType) {
case 'embedded':
client = hasChromeSocketsForTcp ? embeddedJsBrave : embeddedJs // TODO: make generic
break
case 'external':
client = external
break
default:
throw new Error(`Unsupported ipfsNodeType: ${opts.ipfsNodeType}`)
}

const instance = await client.init(opts)
Expand Down
13 changes: 12 additions & 1 deletion add-on/src/lib/runtime-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ function getPlatformInfo (browser) {
return Promise.resolve()
}

function hasChromeSocketsForTcp () {
return 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 createRuntimeChecks (browser) {
// browser
const browserInfo = await getBrowserInfo(browser)
Expand All @@ -24,11 +34,12 @@ async function createRuntimeChecks (browser) {
// platform
const platformInfo = await getPlatformInfo(browser)
const runtimeIsAndroid = platformInfo ? platformInfo.os === 'android' : false
//
const runtimeHasSocketsForTcp = hasChromeSocketsForTcp()
return Object.freeze({
browser,
isFirefox: runtimeIsFirefox,
isAndroid: runtimeIsAndroid,
hasChromeSocketsForTcp: runtimeHasSocketsForTcp,
hasNativeProtocolHandler: runtimeHasNativeProtocol
})
}
Expand Down
Loading

0 comments on commit b441d27

Please sign in to comment.