Skip to content

Commit

Permalink
Merge pull request #711 from ipfs-shipyard/feat/brave-build-with-chro…
Browse files Browse the repository at this point in the history
…me-sockets

This PR adds runtime dectection of `chrome.sockets` APIs and a build target for testing it with Brave Nightly: `yarn dev-build bundle:brave`

External node type remains the default, but Brave users can opt-in to "Embedded + chrome.sockets" node type on the Preferences screen, which will enable Embedded HTTP Gateway.

Cherry-picked js-ipfs improvements:

- streaming compressed payload
  - Content-type sniffing is now done over a meaningful amount of bytes instead of arbitrary number.
- decoupling HTTP Servers from cli/commands/daemon

Other
  - Brave users will see a new node type: `embedded:chromesockets`
  - cleanup boot on clean install 
  - new runtime-checks: `isBrave`, `hasChromeSocketsForTcp` 
  - peer discovery over a ws-star to  improve things until we have TCP transport, MulticastDNS and DHT
  • Loading branch information
lidel committed Apr 16, 2019
2 parents 78ff3da + 2cc5084 commit 01bcdf2
Show file tree
Hide file tree
Showing 32 changed files with 1,304 additions and 712 deletions.
10 changes: 9 additions & 1 deletion add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,16 @@
"message": "Embedded (experimental): run js-ipfs node in your browser (use only for development, read about its limitations under the link below)",
"description": "An option description on the Preferences screen (option_ipfsNodeType_description)"
},
"option_ipfsNodeType_embedded_chromesockets_description": {
"message": "Embedded with Chrome Sockets (experimental): run js-ipfs node in your browser with access to chrome.sockets APIs (details under the link below)",
"description": "An option description on the Preferences screen (option_ipfsNodeType_description)"
},
"option_ipfsNodeConfig_title": {
"message": "IPFS Node Config",
"description": "An option title on the Preferences screen (option_ipfsNodeConfig_title)"
},
"option_ipfsNodeConfig_description": {
"message": "Configuration for the embedded IPFS node. Must be valid JSON.",
"message": "Additional configuration for the embedded IPFS node (arrays will be merged). Must be valid JSON.",
"description": "An option description on the Preferences screen (option_ipfsNodeConfig_description)"
},
"option_ipfsNodeType_external": {
Expand All @@ -243,6 +247,10 @@
"message": "Embedded",
"description": "An option on the Preferences screen (option_ipfsNodeType_embedded)"
},
"option_ipfsNodeType_embedded_chromesockets": {
"message": "Embedded + chrome.sockets",
"description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)"
},
"option_header_gateways": {
"message": "Gateways",
"description": "A section header on the Preferences screen (option_header_gateways)"
Expand Down
3 changes: 3 additions & 0 deletions add-on/manifest.brave-beta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAshQhe/y/j0adZqOFes0nqgRgjDNx4eX6oNrESKeKbpUjH9eSN+lCYqBM3PT+BhPxo+sj/aVgCYsddiIbO43Bq/LsQLBFd+kD1I4qZSN4pJAX9AdsbMmXR9XV0W/O9zlyqkXAfxV13Hwmy+e6IH3p59ytQbpcuLnyipspQ4VXZprLkiWdvPMdifT9wgf5gmD30S1n7uaNrKCu8yZk/Lz5Z+KjoxRdk7X7FJYW+hoUGKb6Ld3Q99iLeKPIvcTjK6/xNHXTbaZfRYbfI8i/mSaxetGxSo7/XkMB8VvAiUkZ4gVSp786oMciQVwK2UyVFXw9pJhGD+O4ozcNk0PSq8aE7QIDAQAB"
}
6 changes: 6 additions & 0 deletions add-on/src/background/background.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
'use strict'
/* eslint-env browser, webextensions */

// 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*'

const browser = require('webextension-polyfill')
const createIpfsCompanion = require('../lib/ipfs-companion')
const { onInstalled } = require('../lib/on-installed')
Expand Down
32 changes: 14 additions & 18 deletions add-on/src/landing-pages/welcome/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,22 @@ function createWelcomePageStore (i18n, runtime) {
return function welcomePageStore (state, emitter) {
state.isIpfsOnline = null
state.peerCount = null

const port = runtime.connect({ name: 'browser-action-port' })

const onMessage = (message) => {
if (message.statusUpdate) {
const peerCount = message.statusUpdate.peerCount
const isIpfsOnline = peerCount > -1

if (isIpfsOnline !== state.isIpfsOnline || peerCount !== state.peerCount) {
state.isIpfsOnline = isIpfsOnline
state.peerCount = peerCount
emitter.emit('render')
}
}
}

port.onMessage.addListener(onMessage)

let port
emitter.on('DOMContentLoaded', async () => {
emitter.emit('render')
port = runtime.connect({ name: 'browser-action-port' })
port.onMessage.addListener(async (message) => {
console.log('port.onMessage', message)
if (message.statusUpdate) {
const peerCount = message.statusUpdate.peerCount
const isIpfsOnline = peerCount > -1
if (isIpfsOnline !== state.isIpfsOnline || peerCount !== state.peerCount) {
state.isIpfsOnline = isIpfsOnline
state.peerCount = peerCount
emitter.emit('render')
}
}
})
})
}
}
Expand Down
3 changes: 2 additions & 1 deletion add-on/src/lib/dnslink.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ module.exports = function createDnslinkResolver (getState) {
readDnslinkFromTxtRecord (fqdn) {
const state = getState()
let apiProvider
if (state.ipfsNodeType === 'external' && state.peerCount !== offlinePeerCount) {
// TODO: fix DNS resolver for ipfsNodeType='embedded:chromesockets', for now use ipfs.io
if (!state.ipfsNodeType.startsWith('embedded') && state.peerCount !== offlinePeerCount) {
apiProvider = state.apiURLString
} else {
// fallback to resolver at public gateway
Expand Down
191 changes: 191 additions & 0 deletions add-on/src/lib/ipfs-client/embedded-chromesockets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
'use strict'
/* eslint-env browser, webextensions */
const browser = require('webextension-polyfill')
const debug = require('debug')

// 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 mergeOptions = require('merge-options')
const Ipfs = require('ipfs')
const HttpApi = require('ipfs/src/http')
const multiaddr = require('multiaddr')
const maToUri = require('multiaddr-to-uri')

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

const log = debug('ipfs-companion:client:embedded')
log.error = debug('ipfs-companion:client:embedded:error')

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)
}
*/
log('init embedded:chromesockets')

const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig)

defaultOpts.libp2p = {
config: {
dht: {
// TODO: check if below is needed after js-ipfs is released with DHT disabled
enabled: false
}
}
}

const userOpts = JSON.parse(opts.ipfsNodeConfig)
const ipfsOpts = mergeOptions.call({ concatArrays: true }, defaultOpts, userOpts, { start: false })
log('creating js-ipfs with opts: ', ipfsOpts)
node = new Ipfs(ipfsOpts)

return new Promise((resolve, reject) => {
node.once('error', (error) => {
log.error('something went terribly wrong during startup of js-ipfs!', error)
reject(error)
})
node.once('ready', async () => {
node.once('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()
await updateConfigWithHttpEndpoints(node, opts)
resolve(node)
} catch (err) {
reject(err)
}
})
try {
node.on('error', error => {
log.error('something went terribly wrong in embedded js-ipfs!', error)
})
await node.start()
} catch (err) {
reject(err)
}
})
})
}

const multiaddr2httpUrl = (ma) => maToUri(ma.includes('/http') ? ma : multiaddr(ma).encapsulate('/http'))

// Update internal configuration to HTTP Endpoints from js-ipfs instance
async function updateConfigWithHttpEndpoints (ipfs, opts) {
const localConfig = await browser.storage.local.get('ipfsNodeConfig')
if (localConfig && localConfig.ipfsNodeConfig) {
const gwMa = await ipfs.config.get('Addresses.Gateway')
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
ipfsNodeConfig.config.Addresses.API = apiMa
const configChanges = {
customGatewayUrl: httpGateway,
ipfsApiUrl: httpApi,
ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2)
}
// update current runtime config (in place, effective without restart)
Object.assign(opts, configChanges)
// update user config in storage (effective on next run)
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)
}
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
}
*/
31 changes: 22 additions & 9 deletions add-on/src/lib/ipfs-client/embedded.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const mergeOptions = require('merge-options')
const Ipfs = require('ipfs')
const { optionDefaults } = require('../options')

Expand All @@ -8,18 +9,30 @@ let node = null
exports.init = function init (opts) {
console.log('[ipfs-companion] Embedded ipfs init')

node = new Ipfs(
JSON.parse(opts.ipfsNodeConfig || optionDefaults.ipfsNodeConfig)
)
const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig)
const userOpts = JSON.parse(opts.ipfsNodeConfig)
const ipfsOpts = mergeOptions.call({ concatArrays: true }, defaultOpts, userOpts, { start: false })

if (node.isOnline()) {
return Promise.resolve(node)
}
node = new Ipfs(ipfsOpts)

return new Promise((resolve, reject) => {
// TODO: replace error listener after a 'ready' event.
node.once('error', (err) => reject(err))
node.once('ready', () => 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', async () => {
node.once('start', () => {
resolve(node)
})
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)
}
})
})
}

Expand Down
27 changes: 21 additions & 6 deletions add-on/src/lib/ipfs-client/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
'use strict'

/* eslint-env browser, webextensions */

const debug = require('debug')
const log = debug('ipfs-companion:client')
log.error = debug('ipfs-companion:client:error')

const browser = require('webextension-polyfill')
const external = require('./external')
const embedded = require('./embedded')
const embeddedWithChromeSockets = require('./embedded-chromesockets')

let client

async function initIpfsClient (opts) {
log('init ipfs client')
await destroyIpfsClient()

if (opts.ipfsNodeType === 'embedded') {
client = embedded
} else {
client = external
switch (opts.ipfsNodeType) {
case 'embedded':
client = embedded
break
case 'embedded:chromesockets':
client = embeddedWithChromeSockets
break
case 'external':
client = external
break
default:
throw new Error(`Unsupported ipfsNodeType: ${opts.ipfsNodeType}`)
}

const instance = await client.init(opts)
Expand Down Expand Up @@ -46,7 +61,7 @@ async function _reloadIpfsClientDependents () {
// detect bundled webui in any of open tabs
if (_isWebuiTab(tab.url)) {
browser.tabs.reload(tab.id)
console.log('[ipfs-companion] reloading bundled webui')
log('reloading bundled webui')
}
})
}
Expand Down
Loading

0 comments on commit 01bcdf2

Please sign in to comment.