Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: leverage IPFS node provided by Brave #956

Merged
merged 3 commits into from
Jan 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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