Skip to content

Commit

Permalink
feat: add ipfsNodeType embedded:chromesockets
Browse files Browse the repository at this point in the history
This adds new ipfsNodeType for use in Chromium contexts such as Brave.

For now we do naive check if chrome.sockets.tcp* APIs are available
and switch to ipfsNodeType=embedded:chromesockets if true.

User can customize configuration of embedded js-ipfs node via Preferences.
  • Loading branch information
lidel committed Apr 9, 2019
1 parent 9abb740 commit 5c0b495
Show file tree
Hide file tree
Showing 26 changed files with 173 additions and 102 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
2 changes: 1 addition & 1 deletion add-on/src/lib/dnslink.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ module.exports = function createDnslinkResolver (getState) {
readDnslinkFromTxtRecord (fqdn) {
const state = getState()
let apiProvider
if (state.ipfsNodeType === 'external' && state.peerCount !== offlinePeerCount) {
if (state.ipfsNodeType !== 'embedded' && state.peerCount !== offlinePeerCount) {
apiProvider = state.apiURLString
} else {
// fallback to resolver at public gateway
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
'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 defaultsDeep = require('@nodeutils/defaults-deep')
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')

Expand All @@ -25,6 +29,9 @@ let nodeHttpApi = null
// to include everything (mplex, libp2p, mss): localStorage.debug = '*'
localStorage.debug = 'jsipfs*,ipfs*,-*:mfs*,-*:ipns*,-ipfs:preload*'

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
Expand All @@ -36,17 +43,25 @@ exports.init = function init (opts) {
hapiServer = startRawHapiServer(9092)
}
*/
console.log('[ipfs-companion] Embedded ipfs init')
log('init: embedded js-ipfs+chrome.sockets')

const defaultOpts = optionDefaults.ipfsNodeConfig
const userOpts = JSON.parse(opts.ipfsNodeConfig)
const ipfsOpts = defaultsDeep(defaultOpts, userOpts, { start: false })
const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig)
defaultOpts.libp2p = {
config: {
dht: {
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) => {
console.error('[ipfs-companion] Something went terribly wrong during startup of js-ipfs!', error)
log.error('something went terribly wrong during startup of js-ipfs!', error)
reject(error)
})
node.once('ready', async () => {
Expand All @@ -55,13 +70,14 @@ exports.init = function init (opts) {
try {
const httpServers = new HttpApi(node, ipfsOpts)
nodeHttpApi = await httpServers.start()
await updateConfigWithHttpEndpoints(node)
resolve(node)
} catch (err) {
reject(err)
}
})
node.on('error', error => {
console.error('[ipfs-companion] Something went terribly wrong in embedded js-ipfs!', error)
log.error('something went terribly wrong in embedded js-ipfs!', error)
})
try {
await node.start()
Expand All @@ -72,8 +88,21 @@ exports.init = function init (opts) {
})
}

// Update internal configuration to HTTP Endpoints from js-ipfs instance
async function updateConfigWithHttpEndpoints (ipfs) {
const ma = await ipfs.config.get('Addresses.Gateway')
log(`synchronizing Addresses.Gateway=${ma} to customGatewayUrl and ipfsNodeConfig`)
const httpGateway = maToUri(ma.includes('/http') ? ma : multiaddr(ma).encapsulate('/http'))
const ipfsNodeConfig = JSON.parse((await browser.storage.local.get('ipfsNodeConfig')).ipfsNodeConfig)
ipfsNodeConfig.config.Addresses.Gateway = ma
await browser.storage.local.set({
customGatewayUrl: httpGateway,
ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2)
})
}

exports.destroy = async function () {
console.log('[ipfs-companion] Embedded ipfs destroy')
log('destroy: embedded js-ipfs+chrome.sockets')

/*
if (httpServer) {
Expand All @@ -98,7 +127,7 @@ exports.destroy = async function () {
try {
await nodeHttpApi.stop()
} catch (err) {
console.error(`[ipfs-companion] failed to stop HttpApi`, err)
log.error('failed to stop HttpApi', err)
}
nodeHttpApi = null
}
Expand Down
6 changes: 3 additions & 3 deletions add-on/src/lib/ipfs-client/embedded.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const defaultsDeep = require('@nodeutils/defaults-deep')
const mergeOptions = require('merge-options')
const Ipfs = require('ipfs')
const { optionDefaults } = require('../options')

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

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

node = new Ipfs(ipfsOpts)

Expand Down
25 changes: 11 additions & 14 deletions add-on/src/lib/ipfs-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,25 @@

/* 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 embeddedJs = require('./embedded')
const embeddedJsBrave = require('./embedded-brave')
const embedded = require('./embedded')
const embeddedWithChromeSockets = require('./embedded-chromesockets')

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

switch (opts.ipfsNodeType) {
case 'embedded':
client = hasChromeSocketsForTcp ? embeddedJsBrave : embeddedJs // TODO: make generic
client = embedded
break
case 'embedded:chromesockets':
client = embeddedWithChromeSockets
break
case 'external':
client = external
Expand Down Expand Up @@ -63,7 +60,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
2 changes: 1 addition & 1 deletion add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const { initIpfsClient, destroyIpfsClient } = require('./ipfs-client')
const { createIpfsUrlProtocolHandler } = require('./ipfs-protocol')
const createNotifier = require('./notifier')
const createCopier = require('./copier')
const createRuntimeChecks = require('./runtime-checks')
const { createRuntimeChecks } = require('./runtime-checks')
const { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress } = require('./context-menus')
const createIpfsProxy = require('./ipfs-proxy')
const { showPendingLandingPages } = require('./on-installed')
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/lib/ipfs-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {
// Test if actions such as 'per site redirect toggle' should be enabled for the URL
isRedirectPageActionsContext (url) {
const state = getState()
return state.ipfsNodeType === 'external' && // hide with embedded node
return state.ipfsNodeType !== 'embedded' && // hide with embedded node
(IsIpfs.ipnsUrl(url) || // show on /ipns/<fqdn>
(url.startsWith('http') && // hide on non-HTTP pages
!url.startsWith(state.gwURLString) && // hide on /ipfs/*
Expand Down
55 changes: 37 additions & 18 deletions add-on/src/lib/options.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
'use strict'

const isFQDN = require('is-fqdn')
const { hasChromeSocketsForTcp } = require('./runtime-checks')

exports.optionDefaults = Object.freeze({
active: true, // global ON/OFF switch, overrides everything else
ipfsNodeType: 'embedded', // Brave should default to js-ipfs: https://github.com/ipfs-shipyard/ipfs-companion/issues/664
ipfsNodeConfig: JSON.stringify({
config: {
Addresses: {
Swarm: [],
API: '/ip4/127.0.0.1/tcp/5002',
Gateway: '/ip4/127.0.0.1/tcp/9090'
}
},
libp2p: {
config: {
dht: {
enabled: false
}
}
}
}, null, 2),
ipfsNodeType: buildDefaultIpfsNodeType(),
ipfsNodeConfig: buildDefaultIpfsNodeConfig(),
publicGatewayUrl: 'https://ipfs.io',
useCustomGateway: false, // TODO: Brave should not redirect to public one, but own
useCustomGateway: true,
noRedirectHostnames: [],
automaticMode: true,
linkify: false,
Expand All @@ -37,6 +23,27 @@ exports.optionDefaults = Object.freeze({
ipfsProxy: true // window.ipfs
})

function buildDefaultIpfsNodeType () {
// Right now Brave is the only vendor giving us access to chrome.sockets
return hasChromeSocketsForTcp() ? 'embedded:chromesockets' : 'external'
}

function buildDefaultIpfsNodeConfig () {
let config = {
config: {
Addresses: {
Swarm: []
}
}
}
if (hasChromeSocketsForTcp()) {
// config.config.Addresses.API = '/ip4/127.0.0.1/tcp/5002'
config.config.Addresses.API = '' // disable API port
config.config.Addresses.Gateway = '/ip4/127.0.0.1/tcp/8080'
}
return JSON.stringify(config, null, 2)
}

// `storage` should be a browser.storage.local or similar
exports.storeMissingOptions = (read, defaults, storage) => {
const requiredKeys = Object.keys(defaults)
Expand Down Expand Up @@ -105,4 +112,16 @@ exports.migrateOptions = async (storage) => {
})
await storage.remove('dnslink')
}
// ~ v2.8.x + Brave
// Upgrade js-ipfs to js-ipfs + chrome.sockets
const { ipfsNodeType } = await storage.get('ipfsNodeType')
if (ipfsNodeType === 'embedded' && hasChromeSocketsForTcp()) {
console.log(`[ipfs-companion] migrating ipfsNodeType to 'embedded:chromesockets'`)
// Overwrite old config
const ipfsNodeConfig = JSON.parse(exports.optionDefaults.ipfsNodeConfig)
await storage.set({
ipfsNodeType: 'embedded:chromesockets',
ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2)
})
}
}
4 changes: 3 additions & 1 deletion add-on/src/lib/runtime-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ async function createRuntimeChecks (browser) {
browser,
isFirefox: runtimeIsFirefox,
isAndroid: runtimeIsAndroid,
isBrave: runtimeHasSocketsForTcp, // TODO: make it more robust
hasChromeSocketsForTcp: runtimeHasSocketsForTcp,
hasNativeProtocolHandler: runtimeHasNativeProtocol
})
}

module.exports = createRuntimeChecks
module.exports.createRuntimeChecks = createRuntimeChecks
module.exports.hasChromeSocketsForTcp = hasChromeSocketsForTcp
3 changes: 2 additions & 1 deletion add-on/src/options/forms/gateways-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function gatewaysForm ({
const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', normalizeGatewayURL)
const onNoRedirectHostnamesChange = onOptionChange('noRedirectHostnames', hostTextToArray)
const mixedContentWarning = !secureContextUrl.test(customGatewayUrl)
const supportRedirectToCustomGateway = ipfsNodeType === 'external'
const supportRedirectToCustomGateway = ipfsNodeType !== 'embedded'

return html`
<form>
Expand Down Expand Up @@ -66,6 +66,7 @@ function gatewaysForm ({
spellcheck="false"
title="Enter URL without any sub-path"
onchange=${onCustomGatewayUrlChange}
${ipfsNodeType !== 'external' ? 'disabled' : ''}
value=${customGatewayUrl} />
</div>
Expand Down
25 changes: 17 additions & 8 deletions add-on/src/options/forms/ipfs-node-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

const browser = require('webextension-polyfill')
const html = require('choo/html')
const { hasChromeSocketsForTcp } = require('../../lib/runtime-checks')

function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
const onIpfsNodeTypeChange = onOptionChange('ipfsNodeType')
const onIpfsNodeConfigChange = onOptionChange('ipfsNodeConfig')

const withChromeSockets = hasChromeSocketsForTcp()
return html`
<form>
<fieldset>
Expand All @@ -18,7 +19,7 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
<dt>${browser.i18n.getMessage('option_ipfsNodeType_title')}</dt>
<dd>
<p>${browser.i18n.getMessage('option_ipfsNodeType_external_description')}</p>
<p>${browser.i18n.getMessage('option_ipfsNodeType_embedded_description')}</p>
<p>${browser.i18n.getMessage(withChromeSockets ? 'option_ipfsNodeType_embedded_chromesockets_description' : 'option_ipfsNodeType_embedded_description')}</p>
<p><a href="https://github.com/ipfs-shipyard/ipfs-companion/blob/master/docs/node-types.md#node-types-in-ipfs-companion" target="_blank">
${browser.i18n.getMessage('option_legend_readMore')}
</a></p>
Expand All @@ -31,14 +32,22 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
selected=${ipfsNodeType === 'external'}>
${browser.i18n.getMessage('option_ipfsNodeType_external')}
</option>
<option
value='embedded'
selected=${ipfsNodeType === 'embedded'}>
${browser.i18n.getMessage('option_ipfsNodeType_embedded')}
</option>
${withChromeSockets ? html`
<option
value='embedded:chromesockets'
selected=${ipfsNodeType === 'embedded:chromesockets'}>
${browser.i18n.getMessage('option_ipfsNodeType_embedded_chromesockets')}
</option>
` : html`
<option
value='embedded'
selected=${ipfsNodeType === 'embedded'}>
${browser.i18n.getMessage('option_ipfsNodeType_embedded')}
</option>
`}
</select>
</div>
${ipfsNodeType === 'embedded' ? html`
${ipfsNodeType.startsWith('embedded') ? html`
<div>
<label for="ipfsNodeConfig">
<dl>
Expand Down
Loading

0 comments on commit 5c0b495

Please sign in to comment.