Skip to content

Commit

Permalink
feat: leverage IPFS node provided by Brave (#956)
Browse files Browse the repository at this point in the history
* feat: leverage IPFS node from Brave

Adds "Provided by Brave" node type and closes #947

* feat: trigger Brave with proper onboarding page

This replaces ipfs:// URI used for triggering activation dropbar
with a page that can be updated by Companion to reflect the stage
of activation.

Co-authored-by: Jessica Schilling <jessica@protocol.ai>
  • Loading branch information
lidel and jessicaschilling authored Jan 7, 2021
1 parent cec022d commit b5cddcf
Show file tree
Hide file tree
Showing 18 changed files with 376 additions and 69 deletions.
36 changes: 32 additions & 4 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,19 @@
},
"option_ipfsNodeType_external_description": {
"message": "Set to \"External\" to connect to a local node using the HTTP API.",
"description": "An option description on the Preferences screen (option_ipfsNodeType_description)"
"description": "An option description on the Preferences screen (option_ipfsNodeType_external_description)"
},
"option_ipfsNodeType_embedded_description": {
"message": "Set to \"Embedded\" to run a js-ipfs node directly in your browser. (Click \"Read more\" to learn about the limitations of this experimental feature.)",
"description": "An option description on the Preferences screen (option_ipfsNodeType_description)"
"description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_description)"
},
"option_ipfsNodeType_embedded_chromesockets_description": {
"message": "Embedded with Chrome Sockets: 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)"
"message": "\"Embedded + chrome.sockets\" is no longer supported by Chromium. If you are using this option, please migrate to a different node type ASAP.",
"description": "An option description on the Preferences screen (option_ipfsNodeType_embedded_chromesockets_description)"
},
"option_ipfsNodeType_brave_description": {
"message": "Set to \"Provided by Brave\" to leverage the Brave browser's native IPFS support.",
"description": "An option description on the Preferences screen (option_ipfsNodeType_brave_description)"
},
"option_ipfsNodeConfig_title": {
"message": "IPFS Node Config",
Expand All @@ -307,6 +311,18 @@
"message": "Embedded + chrome.sockets",
"description": "An option on the Preferences screen (option_ipfsNodeType_embedded_chromesockets)"
},
"option_ipfsNodeType_brave": {
"message": "Provided by Brave",
"description": "An option on the Preferences screen (option_ipfsNodeType_brave)"
},
"option_hint_url": {
"message": "Enter URL without any sub-path",
"description": "An option description on the Preferences screen (option_hint_url)"
},
"option_hint_readonly": {
"message": "This value is read-only",
"description": "An option description on the Preferences screen (option_hint_readonly)"
},
"option_header_gateways": {
"message": "Gateways",
"description": "A section header on the Preferences screen (option_header_gateways)"
Expand Down Expand Up @@ -435,6 +451,10 @@
"message": "experimental",
"description": "Warning label added to experimental options on the Preferences screen (option_experimental)"
},
"option_deprecated": {
"message": "deprecated",
"description": "Warning label added to deprecated options on the Preferences screen (option_deprecated)"
},
"option_experiments_warning": {
"message": "Warning! These experimental features are works in progress and are subject to changes in availability and functionality.",
"description": "Warning about Experiments section on the Preferences screen (option_experiments_warning)"
Expand Down Expand Up @@ -711,6 +731,14 @@
"message": "IPFS is not running",
"description": "Install steps title (page_landingWelcome_installSteps_notRunning_title)"
},
"page_landingWelcome_installSteps_brave_title": {
"message": "Brave users",
"description": "Install steps title (page_landingWelcome_installSteps_brave_title)"
},
"page_landingWelcome_installSteps_brave_install": {
"message": "You can run IPFS directly in your browser — no need for IPFS Desktop or the command line. Open <0>Companion Preferences</0> and set the IPFS node type to “Provided by Brave”.",
"description": "Install steps copy (page_landingWelcome_installSteps_brave_install)"
},
"page_landingWelcome_installSteps_desktop_title": {
"message": "IPFS Desktop users",
"description": "Install steps title (page_landingWelcome_installSteps_desktop_title)"
Expand Down
10 changes: 10 additions & 0 deletions add-on/src/landing-pages/welcome/page.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
'use strict'

const browser = require('webextension-polyfill')
const html = require('choo/html')
const logo = require('../../popup/logo')
const { renderTranslatedLinks, renderTranslatedSpans } = require('../../utils/i18n')

// Brave detection
const { brave } = require('../../../src/lib/ipfs-client/brave')

// Assets
const libp2pLogo = '../../../images/libp2p.svg'
const multiformatsLogo = '../../../images/multiformats.svg'
Expand Down Expand Up @@ -95,12 +99,18 @@ const renderInstallSteps = (i18n, isIpfsOnline) => {
</svg>
`

const optionsUrl = browser.extension.getURL('dist/options/options.html')
return html`
<div class="w-80 mt0 flex flex-column transition-all ${stateUnknown && 'state-unknown'}">
<div class="mb4 flex flex-column justify-center items-center">
${nodeOffSvg()}
<p class="mt0 mb0 f3 tc">${i18n.getMessage('page_landingWelcome_installSteps_notRunning_title')}</p>
</div>
${brave
? html`
<p class="mb2 aqua b f4 lh-title">${i18n.getMessage('page_landingWelcome_installSteps_brave_title')}</p>
<p class="${copyClass}">${renderTranslatedLinks('page_landingWelcome_installSteps_brave_install', [optionsUrl], `target="_blank" class="${anchorClass}"`)}</p>`
: null}
<p class="mb2 aqua b f4 lh-title">${i18n.getMessage('page_landingWelcome_installSteps_desktop_title')}</p>
<p class="${copyClass}">${renderTranslatedLinks('page_landingWelcome_installSteps_desktop_install', ['https://github.com/ipfs-shipyard/ipfs-desktop#ipfs-desktop'], `target="_blank" class="${anchorClass}"`)}</p>
<p class="mb2 aqua b f4 lh-title">${i18n.getMessage('page_landingWelcome_installSteps_cli_title')}</p>
Expand Down
229 changes: 229 additions & 0 deletions add-on/src/lib/ipfs-client/brave.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
'use strict'
/* eslint-env browser, webextensions */

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

const external = require('./external')
const toUri = require('multiaddr-to-uri')
const pWaitFor = require('p-wait-for')

// increased interval to decrease impact of IPFS service process spawns
const waitFor = (f, t) => pWaitFor(f, { interval: 250, timeout: t || Infinity })

exports.init = async function (browser, opts) {
log('ensuring Brave Settings are correct')
const { brave } = exports
await initBraveSettings(browser, brave)
log('delegating API client init to "external" backend pointed at node managed by Brave')
return external.init(browser, opts)
}

exports.destroy = async function (browser) {
log('shuting down node managed by Brave')
const { brave } = exports
const method = await brave.getResolveMethodType()
if (method === 'local') {
// shut down local node when this backend is not active
log('waiting for brave.shutdown() to finish')
await waitFor(() => brave.shutdown())
log('brave.shutdown() done')
}
log('delegating API client destroy to "external" backend pointed at node managed by Brave')
return external.destroy(browser)
}

// ---------------- Brave-specifics -------------------

// ipfs:// URI that will be used for triggering the "Enable IPFS" dropbar in Brave
const braveIpfsUriTrigger = 'ipfs://bafkreigxbf77se2an2u6hmg2kxxbhmenetc7dzvkd3rl4m2orlobjvqcqq'

// Settings screen in Brave where user can manage IPFS support
const braveSettingsPage = 'brave://settings/extensions'

// Diagnostic page for manually starting/stopping Brave's node
// const braveIpfsDiagnosticPage = 'brave://ipfs'

// ipfsNodeType for this backend
exports.braveNodeType = 'external:brave'

// wrapper for chrome.ipfs.* that gets us closer to ergonomics of promise-based browser.*
exports.brave = hasBraveChromeIpfs()
? Object.freeze({
// This is the main check - returns true only in Brave and only when
// feature flag is enabled brave://flags and can be used for high level UI
// decisions such as showing custom node type on Preferences
getIPFSEnabled: async () =>
Boolean(await promisifyBraveCheck(chrome.ipfs.getIPFSEnabled)),

// Obtains a string representation of the resolve method
// method is one of the following strings:
// "ask" uses a gateway but also prompts them to install a local node
// "gateway" uses a gateway but also prompts them to install a local node
// "local" uses a gateway but also prompts them to install a local node
// "disabled" disabled by the user
// "undefined" everything else (IPFS feature flag is not enabled, error etc)
getResolveMethodType: async () =>
String(await promisifyBraveCheck(chrome.ipfs.getResolveMethodType)),

// Obtains the config contents of the local IPFS node
// Returns undefined if missing for any reason
getConfig: async () =>
await promisifyBraveCheck(chrome.ipfs.getConfig),

// Returns true if binary is present
getExecutableAvailable: async () =>
Boolean(await promisifyBraveCheck(chrome.ipfs.getExecutableAvailable)),

// Attempts to start the daemon and returns true if finished
launch: async () =>
Boolean(await promisifyBraveCheck(chrome.ipfs.launch)),

// Attempts to stop the daemon and returns true if finished
shutdown: async () =>
Boolean(await promisifyBraveCheck(chrome.ipfs.shutdown))
})
: undefined

// Detect chrome.ipfs.* APIs provided by Brave to IPFS Companion
function hasBraveChromeIpfs () {
return typeof chrome === 'object' &&
typeof chrome.ipfs === 'object' &&
typeof chrome.ipfs.getIPFSEnabled === 'function' &&
typeof chrome.ipfs.getResolveMethodType === 'function' &&
typeof chrome.ipfs.launch === 'function' &&
typeof chrome.ipfs.shutdown === 'function' &&
typeof chrome.ipfs.getExecutableAvailable === 'function' &&
typeof chrome.ipfs.getConfig === 'function'
}

// Reads value via chrome.ipfs and returns it.
// Never throws: missing/error is returned as undefined.
const promisifyBraveCheck = (fn) => {
return new Promise((resolve, reject) => {
try {
if (fn === chrome.ipfs.getConfig) {
fn((ok, config) => {
if (ok && config) return resolve(JSON.parse(config))
return resolve(undefined)
})
}
fn(val => resolve(val))
} catch (e) {
log.error('unexpected error during promisifyBraveCheck', e)
reject(e)
}
})
}

// We preserve original "external" config, so user can switch between
// nodes provided by Brave and IPFS Desktop without the need for
// manually editing the address of IPFS API endpoint.

exports.useBraveEndpoint = async function (browser) {
const { brave } = exports
const braveConfig = await brave.getConfig()
if (typeof braveConfig === 'undefined') {
log.error('useBraveEndpoint: IPFS_PATH/config is missing, unable to use endpoint from Brave at this time, will try later')
return
}

const {
externalNodeConfig: oldExternalNodeConfig,
customGatewayUrl: oldGatewayUrl,
ipfsApiUrl: oldApiUrl
} = (await browser.storage.local.get(['customGatewayUrl', 'ipfsApiUrl', 'externalNodeConfig']))
const braveApiUrl = addrs2url(braveConfig.Addresses.API)
const braveGatewayUrl = addrs2url(braveConfig.Addresses.Gateway)

if (braveApiUrl === oldApiUrl && braveGatewayUrl === oldGatewayUrl) {
log('useBraveEndpoint: ok')
return
}

log(`useBraveEndpoint: setting api=${braveApiUrl}, gw=${braveGatewayUrl} (before: api=${oldApiUrl}, gw=${oldGatewayUrl})`)
await browser.storage.local.set({
ipfsApiUrl: braveApiUrl,
customGatewayUrl: braveGatewayUrl,
externalNodeConfig: oldExternalNodeConfig || [oldGatewayUrl, oldApiUrl]
})
}

exports.releaseBraveEndpoint = async function (browser) {
const [oldGatewayUrl, oldApiUrl] = (await browser.storage.local.get('externalNodeConfig')).externalNodeConfig
log(`releaseBraveEndpoint: restoring api=${oldApiUrl}, gw=${oldGatewayUrl}`)
await browser.storage.local.set({
ipfsApiUrl: oldApiUrl,
customGatewayUrl: oldGatewayUrl,
externalNodeConfig: null
})
}

// Addresses in go-ipfs config can be a String or array of strings with multiaddrs
function addrs2url (addr) {
if (Array.isArray(addr)) {
addr = addr[0]
}
return toUri(addr, { assumeHttp: true })
}

async function initBraveSettings (browser, brave) {
let showState = () => {}
let tabId
let method = await brave.getResolveMethodType()
log(`brave.resolveMethodType is '${method}'`)

if (method === 'ask') {
// Trigger the dropbar with "Enable IPFS" button by opening ipfs:// URI in a new tab.
// The trigger is a HTML page with some text to make onboarding easier.
tabId = (await browser.tabs.create({ url: braveIpfsUriTrigger })).id

// Reuse the tab for state updates (or create a new one if user closes it)
// Caveat: we inject JS as we can't use tab.update during the init of local gateway
// because Brave will try to use it and fail as it is not ready yet :-))
showState = async (s) => {
try {
await browser.tabs.executeScript(tabId, { code: `window.location.hash = '#${s}';` })
} catch (e) { // noop, just log, don't break if user closed the tab etc
log.error('error while showState', e)
}
}
showState('ask')

// IPFS Companion is unable to change Brave settings,
// all we can do is to poll chrome.ipfs.* and detect when user made a decision
log('waiting for user to make a decision how IPFS resources should be resolved')
await waitFor(async () => {
method = await brave.getResolveMethodType()
return method && method !== 'ask'
})
log(`user set resolveMethodType to '${method}'`)

if (method === 'local') {
log('waiting while Brave downloads IPFS executable..')
showState('download')
await waitFor(() => brave.getExecutableAvailable())

log('waiting while Brave creates repo and config via ipfs init..')
await showState('init')
await waitFor(async () => typeof (await brave.getConfig()) !== 'undefined')
}
}

if (method !== 'local') {
await showState('ask')
await browser.tabs.create({ url: braveSettingsPage })
throw new Error('"Method to resolve IPFS resources" in Brave settings should be "Local node"')
}

// ensure local node is started
log('waiting while brave.launch() starts ipfs daemon..')
await showState('start')
await waitFor(() => brave.launch())
log('brave.launch() finished')
await showState('done')

// ensure Companion uses the endpoint provided by Brave
await exports.useBraveEndpoint(browser)
}
4 changes: 2 additions & 2 deletions add-on/src/lib/ipfs-client/embedded-chromesockets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const { buildConfig, syncConfig } = require('./config')
let node
let nodeHttpApi

exports.init = async function init (opts) {
exports.init = async function init (browser, opts) {
log('init embedded:chromesockets')

const ipfsOpts = await buildConfig(opts, log)
Expand All @@ -40,7 +40,7 @@ exports.init = async function init (opts) {
return node
}

exports.destroy = async function () {
exports.destroy = async function (browser) {
log('destroy: embedded:chromesockets')

if (nodeHttpApi) {
Expand Down
4 changes: 2 additions & 2 deletions add-on/src/lib/ipfs-client/embedded.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const { optionDefaults } = require('../options')

let node = null

exports.init = async function init (opts) {
exports.init = async function init (browser, opts) {
log('init')
const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig)
const userOpts = JSON.parse(opts.ipfsNodeConfig)
Expand All @@ -35,7 +35,7 @@ exports.init = async function init (opts) {
return node
}

exports.destroy = async function () {
exports.destroy = async function (browser) {
log('destroy')
if (!node) return

Expand Down
4 changes: 2 additions & 2 deletions add-on/src/lib/ipfs-client/external.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ log.error = debug('ipfs-companion:client:external:error')

const httpClient = require('ipfs-http-client')

exports.init = async function (opts) {
exports.init = async function (browser, opts) {
log(`init with IPFS API at ${opts.apiURLString}`)
const clientConfig = opts.apiURLString
// https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-http-client#importing-the-module-and-usage
const api = httpClient(clientConfig)
return api
}

exports.destroy = async function () {
exports.destroy = async function (browser) {
log('destroy')
}
Loading

0 comments on commit b5cddcf

Please sign in to comment.