text:
'use strict'
/* eslint-env browser, webextensions */

import debug from 'debug'

import LRU from 'lru-cache'
import isIPFS from 'is-ipfs'
import isFQDN from 'is-fqdn'
import { dropSlash, ipfsUri, pathAtHttpGateway, sameGateway } from './ipfs-path.js'
import { safeURL } from './options.js'
import { braveNodeType } from './ipfs-client/brave.js'
import { recoveryPagePath } from './constants.js'

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

export const redirectOptOutHint = 'x-ipfs-companion-no-redirect'
const recoverableNetworkErrors = new Set([
  // Firefox
  'NS_ERROR_UNKNOWN_HOST', // dns failure
  'NS_ERROR_NET_TIMEOUT', // eg. httpd is offline
  'NS_ERROR_NET_RESET', // failed to load because the server kept reseting the connection
  'NS_ERROR_NET_ON_RESOLVED', // no network
  // Chrome
  'net::ERR_NAME_NOT_RESOLVED', // dns failure
  'net::ERR_CONNECTION_TIMED_OUT', // eg. httpd is offline
  'net::ERR_INTERNET_DISCONNECTED' // no network
])
const recoverableHttpError = (code) => code && code >= 400

// Tracking late redirects for edge cases such as https://github.com/ipfs-shipyard/ipfs-companion/issues/436
const onHeadersReceivedRedirect = new Set()

// Request modifier provides event listeners for the various stages of making an HTTP request
// API Details: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest
export function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, runtime) {
  const browser = runtime.browser
  const runtimeRoot = browser.runtime.getURL('/')
  const webExtensionOrigin = runtimeRoot ? new URL(runtimeRoot).origin : 'http://companion-origin' // avoid 'null' because it has special meaning
  const isCompanionRequest = (request) => {
    // We inspect webRequest object (WebExtension API) instead of Origin HTTP
    // header because the value of the latter changed over the years ad
    // absurdum. It leaks the unique extension ID and no vendor seem to have
    // coherent  policy around it, Firefox and Chromium flip back and forth:
    // Firefox  Nightly 65 sets moz-extension://{extension-installation-id}
    // Chromium        <72 sets null
    // Chromium Beta    72 sets chrome-extension://{uid}
    // Firefox  Nightly 85 sets null
    const { originUrl, initiator } = request
    // Of course, getting "Origin" is vendor-specific:
    // FF: originUrl (Referer-like Origin URL with path)
    // Chromium: initiator (just Origin, no path)
    // Because of this mess, we normalize Origin by reading it from URL.origin
    const { origin } = new URL(originUrl || initiator || 'http://missing-origin')
    return origin === webExtensionOrigin
  }

  // Various types of requests are identified once and cached across all browser.webRequest hooks
  const requestCacheCfg = { max: 128, ttl: 1000 * 30 }
  const ignoredRequests = new LRU(requestCacheCfg)
  const ignore = (id) => ignoredRequests.set(id, true)
  const isIgnored = (id) => ignoredRequests.get(id) !== undefined
  const errorInFlight = new LRU({ max: 3, ttl: 1000 })

  // Returns a canonical hostname representing the site from url
  // Main reason for this is unwrapping DNSLink from local subdomain
  // <fqdn>.ipns.localhost → <fqdn>
  const findSiteFqdn = (url) => {
    if (isIPFS.ipnsSubdomain(url)) {
      // convert subdomain's <fqdn>.ipns.gateway.tld to <fqdn>
      const fqdn = dnslinkResolver.findDNSLinkHostname(url)
      if (fqdn) return fqdn
    }
    return new URL(url).hostname
  }

  // Finds canonical hostname of request.url and its parent page (if present)
  const findSiteHostnames = (request) => {
    const { url, originUrl, initiator } = request
    const fqdn = findSiteFqdn(url)
    // FF: originUrl (Referer-like Origin URL), Chrome: initiator (just Origin)
    const parentUrl = originUrl || initiator
    // String value 'null' is explicitly set by Chromium in some contexts
    const parentFqdn = parentUrl && parentUrl !== 'null' && url !== parentUrl
      ? findSiteFqdn(parentUrl)
      : null
    return { fqdn, parentFqdn }
  }

  const preNormalizationSkip = (state, request) => {
    // skip requests to the custom gateway or API (otherwise we have too much recursion)
    if (sameGateway(request.url, state.gwURL) || sameGateway(request.url, state.apiURL)) {
      ignore(request.requestId)
    }
    // skip websocket handshake (not supported by HTTP2IPFS gateways)
    if (request.type === 'websocket') {
      ignore(request.requestId)
    }
    // skip all local requests
    if (request.url.startsWith('http://127.0.0.1') || request.url.startsWith('http://localhost') || request.url.startsWith('http://[::1]')) {
      ignore(request.requestId)
    }

    // skip if a per-site opt-out exists
    const { fqdn, parentFqdn } = findSiteHostnames(request)
    const triggerOptOut = (optout) => {
      // Disable optout on canonical public gateway
      if (fqdn === 'gateway.ipfs.io') return false
      if (fqdn.endsWith(optout) || (parentFqdn && parentFqdn.endsWith(optout))) return true
      return false
    }
    if (state.disabledOn.some(triggerOptOut)) {
      ignore(request.requestId)
    }

    // additional checks limited to requests for root documents
    if (request.type === 'main_frame') {
      // lazily trigger DNSLink lookup (will do anything only if status for root domain is not in cache)
      if (state.dnslinkPolicy && dnslinkResolver.canLookupURL(request.url)) {
        dnslinkResolver.resolve(request.url) // no await: preload record in background
      }
    }
    return isIgnored(request.requestId)
  }

  const postNormalizationSkip = (state, request) => {
    // skip requests to the public gateway if we can't reedirect them to local
    // node is running (otherwise we have too much recursion)
    if (!state.localGwAvailable && sameGateway(request.url, state.pubGwURL)) {
      ignore(request.requestId)
      // TODO: do not skip and redirect to `ipfs://` and `ipns://` if hasNativeProtocolHandler === true
    }
    return isIgnored(request.requestId)
  }

  // Build RequestModifier
  return {
    // browser.webRequest.onBeforeRequest
    // This event is triggered when a request is about to be made, and before headers are available.
    // This is a good place to listen if you want to cancel or redirect the request.
    onBeforeRequest (request) {
      const state = getState()
      if (!state.active) return

      // When local IPFS node is unreachable , show recovery page where user can redirect
      // to public gateway.
      if (!state.nodeActive && request.type === 'main_frame' && sameGateway(request.url, state.gwURL)) {
        const publicUri = ipfsPathValidator.resolveToPublicUrl(request.url, state.pubGwURLString)
        return { redirectUrl: `${dropSlash(runtimeRoot)}${recoveryPagePath}#${encodeURIComponent(publicUri)}` }
      }

      // When Subdomain Proxy is enabled we normalize address bar requests made
      // to the local gateway and replace raw IP with 'localhost' hostname to
      // take advantage of subdomain redirect provided by go-ipfs >= 0.5
      if (state.redirect && request.type === 'main_frame' && sameGateway(request.url, state.gwURL)) {
        const redirectUrl = safeURL(request.url, { useLocalhostName: state.useSubdomains }).toString()
        if (redirectUrl !== request.url) return { redirectUrl }
      }
      // For now normalize API to the IP to comply with go-ipfs checks
      if (state.redirect && request.type === 'main_frame' && sameGateway(request.url, state.apiURL)) {
        const redirectUrl = safeURL(request.url, { useLocalhostName: false }).toString()
        if (redirectUrl !== request.url) return { redirectUrl }
      }

      // early sanity checks
      if (preNormalizationSkip(state, request)) {
        return
      }
      // poor-mans protocol handlers - https://github.com/ipfs/ipfs-companion/issues/164#issuecomment-328374052
      if (state.catchUnhandledProtocols && mayContainUnhandledIpfsProtocol(request)) {
        const fix = normalizedUnhandledIpfsProtocol(request, state.pubGwURLString)
        if (fix) {
          return fix
        }
      }
      // handler for protocol_handlers from manifest.json
      if (redirectingProtocolRequest(request)) {
        // fix path passed via custom protocol
        const fix = normalizedRedirectingProtocolRequest(request, state.pubGwURLString)
        if (fix) {
          return fix
        }
      }
      // handle redirects to custom gateway
      if (state.redirect) {
        // late sanity checks
        if (postNormalizationSkip(state, request)) {
          return
        }
        // Detect valid /ipfs/ and /ipns/ on any site
        if (ipfsPathValidator.publicIpfsOrIpnsResource(request.url) && isSafeToRedirect(request, runtime)) {
          return redirectToGateway(request, request.url, state, ipfsPathValidator, runtime)
        }
        // Detect dnslink using heuristics enabled in Preferences
        if (state.dnslinkPolicy && dnslinkResolver.canLookupURL(request.url)) {
          if (state.dnslinkRedirect) {
            const dnslinkAtGw = dnslinkResolver.dnslinkAtGateway(request.url)
            if (dnslinkAtGw && isSafeToRedirect(request, runtime)) {
              return redirectToGateway(request, dnslinkAtGw, state, ipfsPathValidator, runtime)
            }
          } else if (state.dnslinkDataPreload) {
            dnslinkResolver.preloadData(request.url)
          }
          if (state.dnslinkPolicy === 'best-effort') {
            dnslinkResolver.resolve(request.url)
          }
        }
      }
    },

    // browser.webRequest.onBeforeSendHeaders
    // This event is triggered before sending any HTTP data, but after all HTTP headers are available.
    // This is a good place to listen if you want to modify HTTP request headers.
    onBeforeSendHeaders (request) {
      const state = getState()
      if (!state.active) return

      // Special handling of requests made to API
      if (sameGateway(request.url, state.apiURL)) {
        const { requestHeaders } = request

        if (isCompanionRequest(request)) {
          // '403 - Forbidden' fix for Chrome and Firefox
          // --------------------------------------------
          // We update "Origin: *-extension://" HTTP headers in requests made to API
          // by js-kubo-rpc-client running in the background page of browser
          // extension.  Without this, some users would need to do manual CORS
          // whitelisting by adding "..extension://<UUID>" to
          // API.HTTPHeaders.Access-Control-Allow-Origin in go-ipfs config.
          // With this, API calls made by browser extension look like ones made
          // by webui loaded from the API port.
          // More info:
          // Firefox 65: https://github.com/ipfs-shipyard/ipfs-companion/issues/622
          // Firefox 85: https://github.com/ipfs-shipyard/ipfs-companion/issues/955
          // Chromium 71: https://github.com/ipfs-shipyard/ipfs-companion/pull/616
          // Chromium 72: https://github.com/ipfs-shipyard/ipfs-companion/issues/630
          const foundAt = requestHeaders.findIndex(h => h.name.toLowerCase() === 'origin')
          const { origin } = state.apiURL
          if (foundAt > -1) {
            // Replace existing Origin with the origin of the API itself.
            // This removes the need for CORS setup in go-ipfs config and
            // ensures there is no HTTP Error 403 Forbidden.
            requestHeaders[foundAt].value = origin
          } else { // future-proofing
            // Origin is missing, and go-ipfs requires it in browsers:
            // https://github.com/ipfs/go-ipfs-cmds/pull/193
            requestHeaders.push({ name: 'Origin', value: origin })
          }
        }

        // Fix "http: invalid Read on closed Body"
        // ----------------------------------
        // There is a bug in go-ipfs related to keep-alive connections
        // that results in partial response for ipfs.add
        // mangled by error "http: invalid Read on closed Body"
        // More info (ipfs-companion): https://github.com/ipfs-shipyard/ipfs-companion/issues/480
        // More info (go-ipfs): https://github.com/ipfs/go-ipfs/issues/5168
        if (request.url.includes('/api/v0/add') && request.url.includes('stream-channels=true')) {
          let addExpectHeader = true
          const expectHeader = { name: 'Expect', value: '100-continue' }
          const warningMsg = 'Executing "Expect: 100-continue" workaround for ipfs.add due to https://github.com/ipfs/go-ipfs/issues/5168'
          for (const header of requestHeaders) {
            // Workaround A: https://github.com/ipfs/go-ipfs/issues/5168#issuecomment-401417420
            // (works in Firefox, but Chromium does not expose Connection header)
            /* (disabled so we use the workaround B in all browsers)
            if (header.name === 'Connection' && header.value !== 'close') {
              console.warn('[ipfs-companion] Executing "Connection: close" workaround for ipfs.add due to https://github.com/ipfs/go-ipfs/issues/5168')
              header.value = 'close'
              addExpectHeader = false
              break
            }
            */
            // Workaround B: https://github.com/ipfs-shipyard/ipfs-companion/issues/480#issuecomment-417657758
            // (works in Firefox 63 AND Chromium 67)
            if (header.name === expectHeader.name) {
              addExpectHeader = false
              if (header.value !== expectHeader.value) {
                log(warningMsg)
                header.value = expectHeader.value
              }
              break
            }
          }
          if (addExpectHeader) {
            log(warningMsg)
            requestHeaders.push(expectHeader)
          }
        }
        return { requestHeaders }
      }
    },

    // browser.webRequest.onHeadersReceived
    // Fired when the HTTP response headers associated with a request have been received.
    // You can use this event to modify HTTP response headers or do a very late redirect.
    onHeadersReceived (request) {
      const state = getState()
      if (!state.active) return

      // Skip if request is marked as ignored
      if (isIgnored(request.requestId)) {
        return
      }

      if (state.redirect) {
        // Late redirect as a workaround for edge cases such as:
        // - CORS XHR in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
        if (runtime.requiresXHRCORSfix && onHeadersReceivedRedirect.has(request.requestId)) {
          onHeadersReceivedRedirect.delete(request.requestId)
          if (state.dnslinkPolicy) {
            const dnslinkAtGw = dnslinkResolver.dnslinkAtGateway(request.url)
            if (dnslinkAtGw) {
              return redirectToGateway(request, dnslinkAtGw, state, ipfsPathValidator, runtime)
            }
          }
          return redirectToGateway(request, request.url, state, ipfsPathValidator, runtime)
        }

        // Detect X-Ipfs-Path Header and upgrade transport to IPFS:
        // 1. Check if DNSLink exists and redirect to it.
        // 2. If there is no DNSLink, validate path from the header and redirect
        const { url } = request
        const notActiveGatewayOrApi = !(sameGateway(url, state.pubGwURL) || sameGateway(url, state.gwURL) || sameGateway(url, state.apiURL))
        if (state.detectIpfsPathHeader && request.responseHeaders && notActiveGatewayOrApi) {
          // console.log('onHeadersReceived.request', request)
          for (const header of request.responseHeaders) {
            if (header.name.toLowerCase() === 'x-ipfs-path' && isSafeToRedirect(request, runtime)) {
              // console.log('onHeadersReceived.request.responseHeaders', request.responseHeaders.length)
              const xIpfsPath = header.value
              // First: Check if dnslink heuristic yields any results
              // Note: this depends on which dnslink lookup policy is selecten in Preferences
              if (state.dnslinkRedirect && state.dnslinkPolicy && dnslinkResolver.canLookupURL(request.url)) {
                // x-ipfs-path is a strong indicator of IPFS support
                // so we force dnslink lookup to pre-populate dnslink cache
                // in a way that works even when state.dnslinkPolicy !== 'enabled'
                // All the following requests will be upgraded to IPNS
                const cachedDnslink = dnslinkResolver.readAndCacheDnslink(new URL(request.url).hostname)
                const dnslinkAtGw = dnslinkResolver.dnslinkAtGateway(request.url, cachedDnslink)
                // redirect only if local node is around, as we can't guarantee DNSLink support
                // at a public subdomain gateway (requires more than 1 level of wildcard TLS certs)
                if (dnslinkAtGw && state.localGwAvailable) {
                  log(`onHeadersReceived: dnslinkRedirect from ${request.url} to ${dnslinkAtGw}`)
                  return redirectToGateway(request, dnslinkAtGw, state, ipfsPathValidator, runtime)
                }
              }
              // Additional validation of X-Ipfs-Path
              if (isIPFS.ipnsPath(xIpfsPath)) {
                // Ignore unhandled IPNS path by this point
                // (means DNSLink is disabled so we don't want to make a redirect that works like DNSLink)
                // log(`onHeadersReceived: ignoring x-ipfs-path=${xIpfsPath} (dnslinkRedirect=false, dnslinkPolicy=false or missing DNS TXT record)`)
              } else if (isIPFS.ipfsPath(xIpfsPath)) {
                // It is possible that someone exposed /ipfs/<cid>/ under /
                // and our path-based onBeforeRequest heuristics were unable
                // to identify request as IPFS one until onHeadersReceived revealed
                // presence of x-ipfs-path header.
                // Solution: convert header value to a path at public gateway
                // (optional redirect to custom one can happen later)
                const url = new URL(request.url)
                const pathWithArgs = `${xIpfsPath}${url.search}${url.hash}`
                const newUrl = pathAtHttpGateway(pathWithArgs, state.pubGwURLString)
                // redirect only if local node is around
                if (newUrl && state.localGwAvailable) {
                  log(`onHeadersReceived: normalized ${request.url} to  ${newUrl}`)
                  return redirectToGateway(request, newUrl, state, ipfsPathValidator, runtime)
                }
              }
            }
          }
        }
      }
    },

    // browser.webRequest.onErrorOccurred
    // Fired when a request could not be processed due to an error on network level.
    // For example: TCP timeout, DNS lookup failure
    // NOTE: this is executed only if webRequest.ResourceType='main_frame'
    onErrorOccurred (request) {
      const state = getState()
      if (!state.active) return

      // Avoid duplicates in Chromium, which fires two events instead of one
      // https://github.com/ipfs-shipyard/ipfs-companion/issues/805
      if (errorInFlight.has(request.url)) return
      errorInFlight.set(request.url, request.requestId)

      // Skip additional requests produced by DNS fixup logic in Firefox
      // https://github.com/ipfs-shipyard/ipfs-companion/issues/804
      if (request.error === 'NS_ERROR_UNKNOWN_HOST' && request.url.includes('://www.')) {
        const urlBeforeFixup = request.url.replace('://www.', '://')
        if (errorInFlight.has(urlBeforeFixup)) return
      }

      // Check if error can be recovered via EthDNS
      if (isRecoverableViaEthDNS(request, state)) {
        const url = new URL(request.url)
        url.hostname = `${url.hostname}.link`
        url.protocol = 'https:' // force HTTPS, as HSTS may be missing on initial load
        const redirectUrl = url.toString()
        log(`onErrorOccurred: attempting to recover from DNS error (${request.error}) using EthDNS for ${request.url} → ${redirectUrl}`, request)
        return updateTabWithURL(request, redirectUrl, browser)
      }

      // Check if error can be recovered via DNSLink
      if (isRecoverableViaDNSLink(request, state, dnslinkResolver)) {
        const { hostname } = new URL(request.url)
        const dnslink = dnslinkResolver.readAndCacheDnslink(hostname)
        if (dnslink) {
          const redirectUrl = dnslinkResolver.dnslinkAtGateway(request.url, dnslink)
          log(`onErrorOccurred: attempting to recover from network error (${request.error}) using dnslink for ${request.url} → ${redirectUrl}`, request)
          // We are unable to redirect in onErrorOccurred, but we can update the tab
          return updateTabWithURL(request, redirectUrl, browser)
        }
      }

      // Cleanup after https://github.com/ipfs-shipyard/ipfs-companion/issues/436
      if (runtime.requiresXHRCORSfix && onHeadersReceivedRedirect.has(request.requestId)) {
        onHeadersReceivedRedirect.delete(request.requestId)
      }

      // Check if error can be recovered by opening same content-addresed path
      // using active gateway (public or local, depending on redirect state)
      if (isRecoverable(request, state, ipfsPathValidator)) {
        const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url)
        log(`onErrorOccurred: attempting to recover from network error (${request.error}) for ${request.url} → ${redirectUrl}`, request)
        // We are unable to redirect in onErrorOccurred, but we can update the tab
        return updateTabWithURL(request, redirectUrl, browser)
      }
    },

    // browser.webRequest.onCompleted
    // Fired when HTTP request is completed (successfully or with an error code)
    // NOTE: this is executed only if webRequest.ResourceType='main_frame'
    onCompleted (request) {
      const state = getState()
      if (!state.active) return
      if (request.statusCode === 200) return // finish if no error to recover from

      // Seamlessly fix canonical link when DNSLink breaks ipfs.io/blog/*
      /// https://github.com/ipfs/blog/issues/360
      if (request.type === 'main_frame' &&
          request.statusCode === 404 &&
          request.url.match(/\/\/[^/]+\/ipns\/ipfs\.io\/blog\//)) {
        log('onCompleted: fixing /ipns/ipfs.io/blog → /ipns/blog.ipfs.io')
        const fixedUrl = request.url.replace('/ipns/ipfs.io/blog', '/ipns/blog.ipfs.io')

        // Chromium bug: sometimes tabs.update does not work from onCompleted,
        // we run additional update after 1s just to be sure
        setTimeout(() => browser.tabs.update(request.tabId, { url: fixedUrl }), 1000)

        return browser.tabs.update(request.tabId, { url: fixedUrl })
      }

      if (isRecoverable(request, state, ipfsPathValidator)) {
        const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url)
        log(`onCompleted: attempting to recover from HTTP Error ${request.statusCode} for ${request.url} → ${redirectUrl}`, request)
        return updateTabWithURL(request, redirectUrl, browser)
      }
    }
  }
}

// Returns a string with URL at the active gateway (local or public)
function redirectToGateway (request, url, state, ipfsPathValidator, runtime) {
  const { resolveToPublicUrl, resolveToLocalUrl } = ipfsPathValidator
  let redirectUrl = state.localGwAvailable ? resolveToLocalUrl(url) : resolveToPublicUrl(url)

  // SUBRESOURCE ON HTTPS PAGE: THE WORKAROUND EXTRAVAGANZA
  // ------------------------------------------------------ \o/
  //
  // Firefox 74 does not mark *.localhost subdomains as Secure Context yet
  // (https://bugzilla.mozilla.org/show_bug.cgi?id=1220810#c23) so we can't
  // redirect there when we have IPFS resource embedded on HTTPS page (eg.
  // image loaded from a public gateway) because that would cause mixed-content
  // warning and subresource would fail to load.  Given the fact that
  // localhost/ipfs/* provided by go-ipfs 0.5+ returns a redirect to
  // *.ipfs.localhost subdomain we need to check requests for subresources, and
  // manually replace 'localhost' hostname with '127.0.0.1' (IP is hardcoded as
  // Secure Context in Firefox). The need for this workaround can be revisited
  // when Firefox closes mentioned bug.
  //
  // Chromium 80 seems to force HTTPS in the final URL (after all redirects) so
  // https://*.localhost fails TODO: needs additional research (could be a bug
  // in Chromium). For now we reuse the same workaround as Firefox.
  //
  if (state.localGwAvailable) {
    const { type, originUrl, initiator } = request
    // match request types for embedded subdresources, but skip ones coming from local gateway
    const parentUrl = originUrl || initiator // FF || Chromium
    if (type !== 'main_frame' && (parentUrl && !sameGateway(parentUrl, state.gwURL))) {
      // use raw IP to ensure subresource will be loaded from the path gateway
      // at 127.0.0.1, which is marked as Secure Context in all browsers
      const useLocalhostName = false
      redirectUrl = safeURL(redirectUrl, { useLocalhostName }).toString()
    }
    // Leverage native URI support in Brave for nice address bar.
    if (type === 'main_frame' && state.ipfsNodeType === braveNodeType && !sameGateway(request.url, state.gwURL)) {
      redirectUrl = ipfsUri(redirectUrl)
      // In Brave 1.20.54 a webRequest redirect pointing at ipfs:// URI
      // is not reflected in address bar - a http://*.localhost URL is displayed instead.
      // but tabs.update works, so we do that for the main request.
      if (redirectUrl !== url) { // futureproofing in case url from request becomes native
        log('redirectToGateway: upgrading address bar to native URI', redirectUrl)
        // manually set tab to native URI
        return runtime.browser.tabs.update(request.tabId, { url: redirectUrl })
      }
    }
  }

  // return a redirect only if URL changed
  if (redirectUrl && request.url !== redirectUrl) return { redirectUrl }
}

function isSafeToRedirect (request, runtime) {
  const { url, type, originUrl } = request
  // Do not redirect if URL includes opt-out hint
  if (url.includes(redirectOptOutHint)) {
    return false
  }
  // Do not redirect if parent URL in address bar includes opt-out hint
  // Note: this works only in Firefox, Chromium does not provide full originUrl, only hostname in request.initiator
  if (type !== 'main_frame' && originUrl && originUrl.includes(redirectOptOutHint)) {
    return false
  }

  // Ignore XHR requests for which redirect would fail due to CORS bug in Firefox
  // See: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
  if (runtime.requiresXHRCORSfix && request.type === 'xmlhttprequest' && !request.responseHeaders) {
    // Sidenote on XHR Origin: Firefox 60 uses request.originUrl, Chrome 63 uses request.initiator
    if (request.originUrl) {
      const sourceOrigin = new URL(request.originUrl).origin
      const targetOrigin = new URL(request.url).origin
      if (sourceOrigin !== targetOrigin) {
        log('Delaying redirect of CORS XHR until onHeadersReceived due to https://github.com/ipfs-shipyard/ipfs-companion/issues/436 :', request.url)
        onHeadersReceivedRedirect.add(request.requestId)
        return false
      }
    }
  }
  // Ignore requests for which redirect would fail due to Brave Shields rules
  // https://github.com/ipfs-shipyard/ipfs-companion/issues/962
  if (runtime.brave && request.type !== 'main_frame') {
    // log('Skippping redirect of IPFS subresource due to Brave Shields', request)
    return false
  }

  return true
}

// REDIRECT-BASED PROTOCOL HANDLERS
// This API is available only Firefox (protocol_handlers from manifest.json)
// Background: https://github.com/ipfs-shipyard/ipfs-companion/issues/164#issuecomment-282513891
// Notes on removal of web+ in Firefox 59: https://github.com/ipfs-shipyard/ipfs-companion/issues/164#issuecomment-355708883
// ===================================================================

// This is just a placeholder that we had to provide -- removed in normalizedRedirectingProtocolRequest()
// It has to match URL from manifest.json/protocol_handlers
const redirectingProtocolEndpoint = 'https://dweb.link/ipfs/?uri='

function redirectingProtocolRequest (request) {
  return request.url.startsWith(redirectingProtocolEndpoint)
}

function normalizedRedirectingProtocolRequest (request, pubGwUrl) {
  const oldPath = decodeURIComponent(new URL(request.url).hash)
  let path = oldPath
  // prefixed (Firefox < 59)
  path = path.replace(/^#web\+dweb:\//i, '/') // web+dweb:/ipfs/Qm → /ipfs/Qm
  path = path.replace(/^#web\+ipfs:\/\//i, '/ipfs/') // web+ipfs://Qm → /ipfs/Qm
  path = path.replace(/^#web\+ipns:\/\//i, '/ipns/') // web+ipns://Qm → /ipns/Qm
  // without prefix (Firefox >= 59)
  path = path.replace(/^#dweb:\//i, '/') // dweb:/ipfs/Qm → /ipfs/Qm
  path = path.replace(/^#ipfs:\/\//i, '/ipfs/') // ipfs://Qm → /ipfs/Qm
  path = path.replace(/^#ipns:\/\//i, '/ipns/') // ipns://Qm → /ipns/Qm
  // additional fixups of the final path
  path = fixupDnslinkPath(path) // /ipfs/example.com → /ipns/example.com
  if (oldPath !== path && isIPFS.path(path)) {
    return { redirectUrl: pathAtHttpGateway(path, pubGwUrl) }
  }
  return null
}

// idempotent /ipfs/example.com → /ipns/example.com
function fixupDnslinkPath (path) {
  if (!(path && path.startsWith('/ipfs/'))) return path
  const [, root] = path.match(/^\/ipfs\/([^/?#]+)/)
  if (root && !isIPFS.cid(root) && isFQDN(root)) {
    return path.replace(/^\/ipfs\//, '/ipns/')
  }
  return path
}

// SEARCH-HIJACK HANDLERS: UNIVERSAL FALLBACK FOR UNHANDLED PROTOCOLS
// (Used in Chrome and other browsers that do not provide better alternatives)
// Background: https://github.com/ipfs-shipyard/ipfs-companion/issues/164#issuecomment-328374052
// ===================================================================

const unhandledIpfsRE = /=(?:web%2B|)(ipfs(?=%3A%2F%2F)|ipns(?=%3A%2F%2F)|dweb(?=%3A%2Fip[f|n]s))%3A(?:%2F%2F|%2F)([^&]+)/

function mayContainUnhandledIpfsProtocol (request) {
  return request.type === 'main_frame' && request.url.includes('%3A%2F')
}

function unhandledIpfsPath (requestUrl) {
  const unhandled = requestUrl.match(unhandledIpfsRE)
  if (unhandled && unhandled.length > 1) {
    const unhandledProtocol = decodeURIComponent(unhandled[1])
    const unhandledPath = `/${decodeURIComponent(unhandled[2])}`
    return isIPFS.path(unhandledPath) ? unhandledPath : `/${unhandledProtocol}${unhandledPath}`
  }
  return null
}

function normalizedUnhandledIpfsProtocol (request, pubGwUrl) {
  let path = unhandledIpfsPath(request.url)
  path = fixupDnslinkPath(path) // /ipfs/example.com → /ipns/example.com
  if (isIPFS.path(path)) {
    // replace search query with a request to a public gateway
    // (will be redirected later, if needed)
    return { redirectUrl: pathAtHttpGateway(path, pubGwUrl) }
  }
}

// RECOVERY OF FAILED REQUESTS
// ===================================================================

// Recovery check for onErrorOccurred (request.error) and onCompleted (request.statusCode)
function isRecoverable (request, state, ipfsPathValidator) {
  // Note: we are unable to recover default public gateways without a local one
  const { error, statusCode, url } = request
  const { redirect, localGwAvailable, pubGwURL, pubSubdomainGwURL } = state
  return (state.recoverFailedHttpRequests &&
    request.type === 'main_frame' &&
    (recoverableNetworkErrors.has(error) ||
      recoverableHttpError(statusCode)) &&
    ipfsPathValidator.publicIpfsOrIpnsResource(url) &&
    ((redirect && localGwAvailable) ||
      (!sameGateway(url, pubGwURL) &&
       !sameGateway(url, pubSubdomainGwURL))))
}

// Recovery check for onErrorOccurred (request.error)
function isRecoverableViaDNSLink (request, state, dnslinkResolver) {
  const recoverableViaDnslink =
    state.recoverFailedHttpRequests &&
    request.type === 'main_frame' &&
    state.dnslinkPolicy &&
    recoverableNetworkErrors.has(request.error)
  return recoverableViaDnslink && dnslinkResolver.canLookupURL(request.url)
}

// Recovery check for onErrorOccurred (request.error)
function isRecoverableViaEthDNS (request, state) {
  return state.recoverFailedHttpRequests &&
    request.type === 'main_frame' &&
    recoverableNetworkErrors.has(request.error) &&
    new URL(request.url).hostname.endsWith('.eth')
}

// We can't redirect in onErrorOccurred/onCompleted
// Indead, we recover by opening URL in a new tab that replaces the failed one
// TODO: display an user-friendly prompt when the very first recovery is done
async function updateTabWithURL (request, redirectUrl, browser) {
  // Do nothing if the URL remains the same
  if (request.url === redirectUrl) return

  return browser.tabs.update(request.tabId, {
    active: true,
    url: redirectUrl
  })
}


text:
'use strict'

import browser from 'webextension-polyfill'
import debug from 'debug'
const log = debug('ipfs-companion:notifier')
log.error = debug('ipfs-companion:notifier:error')

export default function createNotifier (getState) {
  const { getMessage } = browser.i18n
  return async (titleKey, messageKey, messageParam) => {
    const title = browser.i18n.getMessage(titleKey) || titleKey
    let message
    if (messageKey.startsWith('notify_')) {
      message = messageParam ? getMessage(messageKey, messageParam) : getMessage(messageKey)
    } else {
      message = messageKey
    }
    log(`${title}: ${message}`)
    if (getState().displayNotifications && browser && browser.notifications.create) {
      try {
        return await browser.notifications.create({
          type: 'basic',
          iconUrl: browser.runtime.getURL('icons/ipfs-logo-on.svg'),
          title,
          message
        })
      } catch (err) {
        log.error('failed to create a notification', err)
      }
    }
  }
}

repo_name: ipfs/ipfs-companion

repo_language: JavaScript

mime_type: text/x-diff


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import { safeURL } from './options.js'

import debug from 'debug'
const log = debug('ipfs-companion:http-proxy')
log.error = debug('ipfs-companion:http-proxy:error')

// Preface:
//
// When go-ipfs runs on localhost, it exposes two types of gateway:
// 127.0.0.1:8080 - old school path gateway
// localhost:8080 - subdomain gateway supporting Origins like $cid.ipfs.localhost
// More: https://docs.ipfs.tech/how-to/address-ipfs-on-web/#subdomain-gateway
//
// In a web browser contexts we care about Origin per content root (CID)
// because entire web security model uses it as a basis for sandboxing and
// access controls:
// https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy

// registerSubdomainProxy is necessary wourkaround for supporting subdomains
// under 'localhost' (*.ipfs.localhost) because some operating systems do not
// resolve them to local IP and return NX error not found instead
//
// State in Q2 2020:
// - Chromium hardcodes `localhost` name to point at local IP and proxy is not
//   really necessary. The code is here (inactivE) in case we need it in the future.
// - Firefox requires proxy to avoid DNS lookup, but there is an open issue
//   that will remove that need at some point:
//   https://bugzilla.mozilla.org/show_bug.cgi?id=1220810
export async function registerSubdomainProxy (getState, runtime, notify) {
  // At the moment only firefox requires proxy registration
  if (!runtime.isFirefox) return

  try {
    const { active, useSubdomains, gwURLString } = getState()
    const enable = active && useSubdomains

    // HTTP Proxy feature is exposed on the gateway port
    // Just ensure we use localhost IP to remove any dependency on DNS
    const { hostname, port } = safeURL(gwURLString, { useLocalhostName: false })

    // Firefox uses own APIs for selective proxying
    if (runtime.isFirefox) {
      return await registerSubdomainProxyFirefox(enable, hostname, port)
    }

    // At this point we would asume Chromium, but its not needed atm
    // Uncomment below if ever needed (+ add 'proxy' permission to manifest.json)
    // return await registerSubdomainProxyChromium(enable, hostname, port)
  } catch (err) {
    // registerSubdomainProxy is just a failsafe, not necessary in most cases,
    // so we should not break init when it fails.
    // For now we just log error and exit as NOOP
    log.error('registerSubdomainProxy failed', err)
    // Show pop-up only the first time, during init() when notify is passed
    try {
      if (notify) notify('notify_addonIssueTitle', 'notify_addonIssueMsg')
    } catch (_) {
    }
  }
}

// storing listener for later
let onRequestProxyListener

// registerSubdomainProxyFirefox sets proxy using API available in Firefox
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/onRequest
async function registerSubdomainProxyFirefox (enable, hostname, port) {
  const { onRequest } = browser.proxy

  // always remove the old listener (host and port could change)
  const oldListener = onRequestProxyListener
  if (oldListener && onRequest.hasListener(oldListener)) {
    onRequest.removeListener(oldListener)
  }

  if (enable) {
    // create new listener with the latest host:port note: the listener is
    // handling requests made to all localhost ports (limitation of the API,
    // port is ignored) that is why we manually check port inside of the listener
    onRequestProxyListener = (request) => {
      if (new URL(request.url).port === port) {
        return { type: 'http', host: hostname, port }
      }
      return { type: 'direct' }
    }

    // register the listener
    onRequest.addListener(onRequestProxyListener, {
      urls: ['http://*.localhost/*'],
      incognito: false
    })
    log(`enabled ${hostname}:${port} as HTTP proxy for *.localhost`)
    return
  }

  // at this point we effectively disabled proxy
  log('disabled HTTP proxy for *.localhost')
}

/*
 * Chromium 80 does not need proxy, so below is not used.
 * Uncomment below if ever needed (+ add 'proxy' permission to manifest.json)

// Helpers for converting callback chrome.* API to promises
const cb = (resolve, reject) => (result) => {
  const err = chrome.runtime.lastError
  if (err) return reject(err)
  return resolve(result)
}
const get = async (opts) => new Promise((resolve, reject) => chrome.proxy.settings.get(opts, cb(resolve, reject)))
const set = async (opts) => new Promise((resolve, reject) => chrome.proxy.settings.set(opts, cb(resolve, reject)))
const clear = async (opts) => new Promise((resolve, reject) => chrome.proxy.settings.clear(opts, cb(resolve, reject)))

// registerSubdomainProxyChromium sets proxy using API available in Chromium
// https://developer.chrome.com/extensions/proxy
async function registerSubdomainProxyChromium (enable, hostname, port) {
  const scope = 'regular_only'

  // read current proxy settings
  const settings = await get({ incognito: false })

  // set or update, if enabled
  if (enable) {
    // PAC script enables selective routing to PROXY at host+port
    // here, PROXY is the same as HTTP API endpoint
    const pacConfig = {
      mode: 'pac_script',
      pacScript: {
        data: 'function FindProxyForURL(url, host) {\n' +
                `  if (shExpMatch(host, '*.localhost:${port}'))\n` +
                `    return 'PROXY ${hostname}:${port}';\n` +
                "  return 'DIRECT';\n" +
                '}'
      }
    }
    await set({ value: pacConfig, scope })
    log(`enabled ${hostname}:${port} as HTTP proxy for *.localhost`)
    // log('updated chrome.proxy.settings', await get({ incognito: false }))
    return
  }

  // else: remove any existing proxy settings
  if (settings && settings.levelOfControl === 'controlled_by_this_extension') {
    // remove any proxy settings ipfs-companion set up before
    await clear({ scope })
    log('disabled HTTP proxy for *.localhost')
  }
}
*/


text:
'use strict'
/* eslint-env browser, webextensions */

import debug from 'debug'

import browser from 'webextension-polyfill'

import { redirectOptOutHint } from './ipfs-request.js'
import { ipfsContentPath } from './ipfs-path.js'
const log = debug('ipfs-companion:import')
log.error = debug('ipfs-companion:import:error')

export const browserActionFilesCpImportCurrentTab = 'browserActionFilesCpImportCurrentTab'

export function createIpfsImportHandler (getState, getIpfs, ipfsPathValidator, runtime, copier) {
  const {
    resolveToPublicUrl,
    resolveToCid
  } = ipfsPathValidator
  const ipfsImportHandler = {
    getIpfsPathAndNativeAddress (cid) {
      const state = getState()
      const path = `/ipfs/${cid}`
      if (runtime.hasNativeProtocolHandler) {
        return { path, url: `ipfs://${cid}` }
      } else {
        // open at public GW (will be redirected to local elsewhere, if enabled)
        const url = new URL(path, state.pubGwURLString).toString()
        return { path, url }
      }
    },

    async openFilesAtGateway (mfsPath) {
      const ipfs = getIpfs()
      const { cid } = await ipfs.files.stat(mfsPath)
      const { url } = ipfsImportHandler.getIpfsPathAndNativeAddress(cid)
      await browser.tabs.create({ url })
    },

    async openFilesAtWebUI (mfsPath) {
      const state = getState()
      const url = `${state.webuiRootUrl}#/files${mfsPath}`
      await browser.tabs.create({ url })
    },

    async copyImportResultsToFiles (results, importDir) {
      const ipfs = getIpfs()
      // cp will fail if directory does not exist
      await ipfs.files.mkdir(importDir, { parents: true })
      // remove directory from files API import files
      const files = results.filter(file => (file.path !== ''))
      for (const file of files) {
        await ipfs.files.cp(`/ipfs/${file.cid}`, `${importDir}${file.path}`)
      }
      log(`created import dir at ${importDir}`)
    },

    async copyShareLink (files) {
      if (!files || !copier) return
      const root = files.find(file => file.path === '')
      if (!root) return
      let path
      if (files.length === 2) {
        // share path to a single file in a dir
        const file = files.find(file => file.path !== '')
        path = `/ipfs/${root.cid}/${file.path}`
      } else {
        // share wrapping dir
        path = `/ipfs/${root.cid}/`
      }
      const url = resolveToPublicUrl(path)
      await copier.copyTextToClipboard(url)
    },

    async preloadFilesAtPublicGateway (files) {
      const state = getState()
      if (!state.preloadAtPublicGateway) return
      for (const file of files) {
        if (file && file.cid) {
          const { path } = ipfsImportHandler.getIpfsPathAndNativeAddress(file.cid)
          const preloadUrl = resolveToPublicUrl(`${path}#${redirectOptOutHint}`)
          try {
            await fetch(preloadUrl, { method: 'HEAD' })
            log('successfully preloaded', file)
          } catch (err) {
            log.error('preload failed', err)
          }
        }
      }
    },

    async filesCpImportCurrentTab (browser) {
      try {
        const {
          copyShareLink,
          preloadFilesAtPublicGateway,
          openFilesAtWebUI
        } = this
        const currentTab = await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0])
        const importDir = formatImportDirectory(getState().importDir)
        const ipfs = getIpfs()
        const contentPath = ipfsContentPath(currentTab.url, { keepURIParams: false })
        const pathSegments = contentPath.split('/').filter(Boolean)
        const namedImport = typeof pathSegments[2] !== 'undefined'
        const resolvedCid = await resolveToCid(contentPath)

        // best-effort name based on last path segment, or IPNS root
        const name = namedImport
          ? pathSegments.slice(-1)
          : `${pathSegments[0] === 'ipns' ? pathSegments[1] : 'unnamed'}_${resolvedCid.slice(-4)}`

        // import to mfs
        await ipfs.files.mkdir(importDir, { parents: true })
        await ipfs.files.cp(`/ipfs/${resolvedCid}`, `${importDir}${name}`)
        await openFilesAtWebUI(importDir)

        // create fake ipfs.add results, so we can reuse code from quick-import feature
        const { cid } = await ipfs.files.stat(importDir, { hash: true })
        const results = [{ path: '', cid }, { path: name, cid: resolvedCid }]
        await copyShareLink(results)
        await preloadFilesAtPublicGateway(results)
      } catch (e) { log.error('unexpected error during filesCpImportCurrentTab', e) }
    }

  }
  return ipfsImportHandler
}

export function formatImportDirectory (path) {
  path = path.replace(/\/$|$/, '/')
  path = path.replace(/(\/)\/+/g, '$1')

  // needed to handle date symbols in the import directory
  const now = new Date()
  const dateSymbols = [/%Y/g, /%M/g, /%D/g, /%h/g, /%m/g, /%s/g]
  const symbolReplacements = [now.getFullYear(), now.getMonth() + 1, now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds()].map(n => String(n).padStart(2, '0'))
  dateSymbols.forEach((symbol, i) => { path = path.replace(symbol, symbolReplacements[i]) })
  return path
}


text:
'use strict'
/* eslint-env browser, webextensions */

import { brave } from './ipfs-client/brave.js'

// this is our kitchen sink for runtime detection

function getBrowserInfo (browser) {
  // browser.runtime.getBrowserInfo is not available in Chromium-based browsers
  if (browser && browser.runtime && browser.runtime.getBrowserInfo) {
    return browser.runtime.getBrowserInfo()
  }
  return Promise.resolve({})
}

function getPlatformInfo (browser) {
  if (browser && browser.runtime && browser.runtime.getPlatformInfo) {
    return browser.runtime.getPlatformInfo()
  }
  return Promise.resolve()
}

export default async function createRuntimeChecks (browser) {
  // browser
  const { name, version } = await getBrowserInfo(browser)
  const isFirefox = name && (name.includes('Firefox') || name.includes('Fennec'))
  const hasNativeProtocolHandler = !!(browser && browser.protocol && browser.protocol.registerStringProtocol) // TODO: chrome.ipfs support
  // platform
  const platformInfo = await getPlatformInfo(browser)
  const isAndroid = platformInfo ? platformInfo.os === 'android' : false
  return Object.freeze({
    browser,
    brave, // easy Boolean(runtime.brave)
    isFirefox,
    isAndroid,
    requiresXHRCORSfix: !!(isFirefox && version && version.startsWith('68')),
    hasNativeProtocolHandler
  })
}


text:
'use strict'
/* eslint-env browser, webextensions */

import debug from 'debug'

import browser from 'webextension-polyfill'
import toMultiaddr from 'uri-to-multiaddr'
import pMemoize from 'p-memoize'
import LRU from 'lru-cache'
import all from 'it-all'
import { optionDefaults, storeMissingOptions, migrateOptions, guiURLString, safeURL } from './options.js'
import { initState, offlinePeerCount } from './state.js'
import { createIpfsPathValidator, dropSlash, sameGateway, safeHostname } from './ipfs-path.js'
import createDnslinkResolver from './dnslink.js'
import { createRequestModifier } from './ipfs-request.js'
import { initIpfsClient, destroyIpfsClient, reloadIpfsClientOfflinePages } from './ipfs-client/index.js'
import { braveNodeType, useBraveEndpoint, releaseBraveEndpoint } from './ipfs-client/brave.js'
import { createIpfsImportHandler, formatImportDirectory, browserActionFilesCpImportCurrentTab } from './ipfs-import.js'
import createNotifier from './notifier.js'
import createCopier from './copier.js'
import createInspector from './inspector.js'
import createRuntimeChecks from './runtime-checks.js'
import { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuViewOnGateway, contextMenuCopyPermalink, contextMenuCopyCidAddress } from './context-menus.js'
import { registerSubdomainProxy } from './http-proxy.js'
import { runPendingOnInstallTasks } from './on-installed.js'
import { handleConsentFromState, startSession, endSession, trackView } from './telemetry.js'
const log = debug('ipfs-companion:main')
log.error = debug('ipfs-companion:main:error')

let browserActionPort // reuse instance for status updates between on/off toggles

// init happens on addon load in background/background.js
export default async function init () {
  // INIT
  // ===================================================================
  let ipfs // ipfs-api instance
  /** @type {import('../types.js').CompanionState} */
  let state // avoid redundant API reads by utilizing local cache of various states
  let dnslinkResolver
  let ipfsPathValidator
  let modifyRequest
  let notify
  let copier
  let inspector
  let runtime
  let contextMenus
  let apiStatusUpdateInterval
  let ipfsImportHandler
  const idleInSecs = 5 * 60
  const browserActionPortName = 'browser-action-port'

  try {
    log('init')
    await storeMissingOptions(await browser.storage.local.get(), optionDefaults, browser.storage.local)
    await migrateOptions(browser.storage.local, debug)
    const options = await browser.storage.local.get(optionDefaults)
    runtime = await createRuntimeChecks(browser)
    state = initState(options)
    notify = createNotifier(getState)
    // ensure consent is set properly on app init
    handleConsentFromState(state)

    if (state.active) {
      startSession()
      // It's ok for this to fail, node might be unavailable or mis-configured
      try {
        ipfs = await initIpfsClient(browser, state)
      } catch (err) {
        console.error('[ipfs-companion] Failed to init IPFS client', err)
        notify(
          'notify_startIpfsNodeErrorTitle',
          err.name === 'ValidationError' ? err.details[0].message : err.message
        )
      }
    }

    dnslinkResolver = createDnslinkResolver(getState)
    ipfsPathValidator = createIpfsPathValidator(getState, getIpfs, dnslinkResolver)
    copier = createCopier(notify, ipfsPathValidator)
    ipfsImportHandler = createIpfsImportHandler(getState, getIpfs, ipfsPathValidator, runtime, copier)
    inspector = createInspector(notify, ipfsPathValidator, getState)
    contextMenus = createContextMenus(getState, runtime, ipfsPathValidator, {
      onAddFromContext,
      onCopyCanonicalAddress: copier.copyCanonicalAddress,
      onCopyRawCid: copier.copyRawCid,
      onCopyAddressAtPublicGw: copier.copyAddressAtPublicGw
    })
    modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
    log('register all listeners')
    registerListeners()
    await registerSubdomainProxy(getState, runtime, notify)
    log('init done')
    setApiStatusUpdateInterval(options.ipfsApiPollMs)
    await runPendingOnInstallTasks()
  } catch (error) {
    log.error('Unable to initialize addon due to error', error)
    if (notify) notify('notify_addonIssueTitle', 'notify_addonIssueMsg')
    throw error
  }

  function getState () {
    return state
  }

  function getIpfs () {
    if (state.active && ipfs) return ipfs
    log.error('refused access to IPFS API client, check if extension is enabled')
    throw new Error('IPFS Companion: API client is disabled')
  }

  function registerListeners () {
    const onBeforeSendInfoSpec = ['blocking', 'requestHeaders']
    if (browser.webRequest.OnBeforeSendHeadersOptions && 'EXTRA_HEADERS' in browser.webRequest.OnBeforeSendHeadersOptions) {
      // Chrome 72+  requires 'extraHeaders' for accessing all headers
      // Note: we need this for code ensuring kubo-rpc-client can talk to API without setting CORS
      onBeforeSendInfoSpec.push('extraHeaders')
    }
    browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, { urls: ['<all_urls>'] }, onBeforeSendInfoSpec)
    browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, { urls: ['<all_urls>'] }, ['blocking'])
    browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, { urls: ['<all_urls>'] }, ['blocking', 'responseHeaders'])
    browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, { urls: ['<all_urls>'], types: ['main_frame'] })
    browser.webRequest.onCompleted.addListener(onCompleted, { urls: ['<all_urls>'], types: ['main_frame'] })
    browser.storage.onChanged.addListener(onStorageChange)
    browser.webNavigation.onCommitted.addListener(onNavigationCommitted)
    browser.webNavigation.onDOMContentLoaded.addListener(onDOMContentLoaded)
    browser.tabs.onActivated.addListener(onActivatedTab)
    if (browser.windows) {
      browser.windows.onFocusChanged.addListener(onWindowFocusChanged)
    }
    browser.runtime.onMessage.addListener(onRuntimeMessage)
    browser.runtime.onConnect.addListener(onRuntimeConnect)

    if (runtime.hasNativeProtocolHandler) {
      log('native protocol handler support detected, but IPFS handler is not implemented yet :-(')
    } else {
      log('no browser.protocol API, native protocol will not be registered')
    }
  }

  // HTTP Request Hooks
  // ===================================================================

  function onBeforeSendHeaders (request) {
    return modifyRequest.onBeforeSendHeaders(request)
  }

  function onBeforeRequest (request) {
    return modifyRequest.onBeforeRequest(request)
  }

  function onHeadersReceived (request) {
    return modifyRequest.onHeadersReceived(request)
  }

  function onErrorOccurred (request) {
    return modifyRequest.onErrorOccurred(request)
  }

  function onCompleted (request) {
    return modifyRequest.onCompleted(request)
  }

  // RUNTIME MESSAGES (one-off messaging)
  // ===================================================================
  // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/sendMessage

  function onRuntimeMessage (request, sender) {
    // console.log((sender.tab ? 'Message from a content script:' + sender.tab.url : 'Message from the extension'), request)
    if (request.pubGwUrlForIpfsOrIpnsPath) {
      const path = request.pubGwUrlForIpfsOrIpnsPath
      const { validIpfsOrIpns, resolveToPublicUrl } = ipfsPathValidator
      const result = validIpfsOrIpns(path) ? resolveToPublicUrl(path) : null
      return Promise.resolve({ pubGwUrlForIpfsOrIpnsPath: result })
    }
    if (request.telemetry) {
      return Promise.resolve(onTelemetryMessage(request.telemetry, sender))
    }
  }

  function onTelemetryMessage (request, sender) {
    if (request.trackView) {
      const { version } = browser.runtime.getManifest()
      return trackView(request.trackView, { version })
    }
  }

  // PORTS (connection-based messaging)
  // ===================================================================
  // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/connect
  // Make a connection between different contexts inside the add-on,
  // e.g. signalling between browser action popup and background page that works
  // in everywhere, even in private contexts (https://github.com/ipfs/ipfs-companion/issues/243)

  // Cache for async URL2CID resolution used by browser action
  // (resolution happens off-band so UI render is not blocked with sometimes expensive DHT traversal)
  const resolveCache = new LRU({ max: 10, ttl: 1000 * 30 })

  function onRuntimeConnect (port) {
    // console.log('onConnect', port)
    if (port.name === browserActionPortName) {
      browserActionPort = port
      browserActionPort.onMessage.addListener(handleMessageFromBrowserAction)
      browserActionPort.onDisconnect.addListener(() => { browserActionPort = null })
      sendStatusUpdateToBrowserAction()
    }
  }

  const BrowserActionMessageHandlers = {
    notification: (message) => notify(message.title, message.message),
    [browserActionFilesCpImportCurrentTab]: () => ipfsImportHandler.filesCpImportCurrentTab(browser),
    [contextMenuViewOnGateway]: inspector.viewOnGateway,
    [contextMenuCopyCanonicalAddress]: copier.copyCanonicalAddress,
    [contextMenuCopyCidAddress]: copier.copyCidAddress,
    [contextMenuCopyRawCid]: copier.copyRawCid,
    [contextMenuCopyAddressAtPublicGw]: copier.copyAddressAtPublicGw,
    [contextMenuCopyPermalink]: copier.copyPermalink
  }

  function handleMessageFromBrowserAction (message) {
    const handler = BrowserActionMessageHandlers[message && message.event]
    if (!handler) return console.warn('Unknown browser action message event', message)
    handler(message)
  }

  async function sendStatusUpdateToBrowserAction () {
    if (!browserActionPort) return
    const currentTab = await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0])
    const { version } = browser.runtime.getManifest()
    const info = {
      active: state.active,
      ipfsNodeType: state.ipfsNodeType,
      peerCount: state.peerCount,
      gwURLString: dropSlash(state.gwURLString),
      pubGwURLString: dropSlash(state.pubGwURLString),
      webuiRootUrl: dropSlash(state.webuiRootUrl), // TODO: fix js-ipfs - it fails with trailing slash
      importDir: state.importDir,
      openViaWebUI: state.openViaWebUI,
      apiURLString: dropSlash(state.apiURLString),
      redirect: state.redirect,
      enabledOn: state.enabledOn,
      disabledOn: state.disabledOn,
      newVersion: state.dismissedUpdate !== version ? version : null,
      currentTab
    }
    try {
      const v = await ipfs.version()
      if (v) {
        info.gatewayVersion = v.commit ? v.version + '/' + v.commit : v.version
      }
    } catch (error) {
      info.gatewayVersion = null
    }
    if (state.active && info.currentTab) {
      const url = info.currentTab.url
      info.isIpfsContext = ipfsPathValidator.isIpfsPageActionsContext(url)
      if (info.isIpfsContext) {
        info.currentTabPublicUrl = ipfsPathValidator.resolveToPublicUrl(url)
        info.currentTabContentPath = ipfsPathValidator.resolveToIpfsPath(url)
        if (resolveCache.has(url)) {
          const [immutableIpfsPath, permalink, cid] = resolveCache.get(url)
          info.currentTabImmutablePath = immutableIpfsPath
          info.currentTabPermalink = permalink
          info.currentTabCid = cid
        } else {
          // run async resolution in the next event loop so it does not block the UI
          setTimeout(async () => {
            resolveCache.set(url, [
              await ipfsPathValidator.resolveToImmutableIpfsPath(url),
              await ipfsPathValidator.resolveToPermalink(url),
              await ipfsPathValidator.resolveToCid(url)
            ])
            await sendStatusUpdateToBrowserAction()
          }, 0)
        }
      }
      info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(url)
      info.currentFqdn = info.currentDnslinkFqdn || safeHostname(url)
      info.currentTabIntegrationsOptOut = !state.activeIntegrations(info.currentFqdn)
      info.isRedirectContext = info.currentFqdn && ipfsPathValidator.isRedirectPageActionsContext(url)
    }
    // Still here?
    if (browserActionPort) {
      browserActionPort.postMessage({ statusUpdate: info })
    }
  }

  // Context Menu Importer
  // -------------------------------------------------------------------

  async function onAddFromContext (context, contextType, options) {
    const {
      copyImportResultsToFiles,
      copyShareLink,
      preloadFilesAtPublicGateway,
      openFilesAtGateway,
      openFilesAtWebUI
    } = ipfsImportHandler
    const importDir = formatImportDirectory(state.importDir)
    let data
    let results
    try {
      const dataSrc = await findValueForContext(context, contextType)
      if (contextType === 'selection') {
        // TODO: persist full pageUrl somewhere (eg. append at the end of the content but add toggle to disable it)
        data = {
          path: `${new URL(context.pageUrl).hostname}.txt`,
          content: dataSrc
        }
      } else {
        // Enchanced addFromURL
        // --------------------
        // Initially, this was a workaround due to https://github.com/ipfs/ipfs-companion/issues/227
        // but now we have additional rules about keeping file name, so we can't use valilla ipfs.addFromURL
        const fetchOptions = {
          cache: 'force-cache',
          referrer: context.pageUrl
        }
        // console.log('onAddFromContext.context', context)
        // console.log('onAddFromContext.fetchOptions', fetchOptions)
        const response = await fetch(dataSrc, fetchOptions)
        const blob = await response.blob()
        const url = new URL(response.url)
        // https://github.com/ipfs-shipyard/ipfs-companion/issues/599
        const filename = url.pathname === '/'
          ? url.hostname
          : url.pathname.replace(/[\\/]+$/, '').split('/').pop()
        data = {
          path: decodeURIComponent(filename),
          content: blob
        }
      }

      results = await all(ipfs.addAll([data], options))
      copyShareLink(results)
      preloadFilesAtPublicGateway(results)

      await copyImportResultsToFiles(results, importDir)
      if (!state.openViaWebUI || state.ipfsNodeType.startsWith('embedded')) {
        await openFilesAtGateway(importDir)
      } else {
        await openFilesAtWebUI(importDir)
      }
    } catch (error) {
      console.error('Error in import to IPFS context menu', error)
      if (error.message === 'NetworkError when attempting to fetch resource.') {
        notify('notify_importErrorTitle', 'notify_importTrackingProtectionErrorMsg')
        console.warn('IPFS import often fails because remote file can not be downloaded due to Tracking Protection. See details at: https://github.com/ipfs/ipfs-companion/issues/227')
        browser.tabs.create({
          url: 'https://github.com/ipfs/ipfs-companion/issues/227'
        })
      } else {
        notify('notify_importErrorTitle', 'notify_inlineErrorMsg', `${error.message}`)
      }
    }
  }

  // Page-specific Actions
  // -------------------------------------------------------------------

  async function onWindowFocusChanged (windowId) {
    // Note: On some Linux window managers, WINDOW_ID_NONE will always be sent
    // immediately preceding a switch from one browser window to another.
    if (windowId !== browser.windows.WINDOW_ID_NONE) {
      const currentTab = await browser.tabs.query({ active: true, windowId }).then(tabs => tabs[0])
      await contextMenus.update(currentTab.id)
    }
  }

  async function onActivatedTab (activeInfo) {
    await contextMenus.update(activeInfo.tabId)
  }

  async function onNavigationCommitted (details) {
    await contextMenus.update(details.tabId)
    await updatePageActionIndicator(details.tabId, details.url)
  }

  async function updatePageActionIndicator (tabId, url) {
    // Chrome does not permit for both pageAction and browserAction to be enabled at the same time
    // https://github.com/ipfs-shipyard/ipfs-companion/issues/398
    if (runtime.isFirefox && ipfsPathValidator.isIpfsPageActionsContext(url)) {
      if (sameGateway(url, state.gwURL) || sameGateway(url, state.apiURL)) {
        await browser.pageAction.setIcon({ tabId, path: '/icons/ipfs-logo-on.svg' })
        await browser.pageAction.setTitle({ tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtCustomGateway') })
      } else {
        await browser.pageAction.setIcon({ tabId, path: '/icons/ipfs-logo-off.svg' })
        await browser.pageAction.setTitle({ tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtPublicGateway') })
      }
      await browser.pageAction.show(tabId)
    }
  }

  async function onDOMContentLoaded (details) {
    if (!state.active) return // skip content script injection when off
    if (!details.url || !details.url.startsWith('http')) return // skip empty and special pages
    if (!state.activeIntegrations(details.url)) return // skip if opt-out exists
    // console.info(`[ipfs-companion] onDOMContentLoaded`, details)
    if (state.linkify) {
      log(`running linkfy experiment on ${details.url}`)
      try {
        await browser.tabs.executeScript(details.tabId, {
          file: '/dist/bundles/linkifyContentScript.bundle.js',
          matchAboutBlank: false,
          allFrames: true,
          runAt: 'document_idle'
        })
      } catch (error) {
        log.error(`Unable to linkify DOM at '${details.url}' due to`, error)
      }
    }
    // Ensure embedded js-ipfs in Brave uses correct API
    if (details.url.startsWith(state.webuiRootUrl)) {
      const apiMultiaddr = toMultiaddr(state.apiURLString)
      await browser.tabs.executeScript(details.tabId, {
        runAt: 'document_start',
        code: `if (!localStorage.getItem('ipfsApi')) {
        console.log('[ipfs-companion] Setting API to ${apiMultiaddr}');
        localStorage.setItem('ipfsApi', '${apiMultiaddr}');
        window.location.reload();
      }`
      })
    }
  }

  // API STATUS UPDATES
  // -------------------------------------------------------------------
  // API is polled for peer count every ipfsApiPollMs

  async function setApiStatusUpdateInterval (ipfsApiPollMs) {
    if (apiStatusUpdateInterval) {
      clearInterval(apiStatusUpdateInterval)
    }
    apiStatusUpdateInterval = setInterval(() => runIfNotIdle(apiStatusUpdate), ipfsApiPollMs)
    await apiStatusUpdate()
  }

  async function apiStatusUpdate () {
    // update peer count
    const oldPeerCount = state.peerCount
    state.peerCount = await getSwarmPeerCount()
    await Promise.all([
      updateAutomaticModeRedirectState(oldPeerCount, state.peerCount),
      updateBrowserActionBadge(),
      contextMenus.update(),
      sendStatusUpdateToBrowserAction()
    ])
  }

  async function getSwarmPeerCount () {
    if (!ipfs) return offlinePeerCount
    try {
      const peerInfos = await ipfs.swarm.peers({ timeout: 2500 })
      return peerInfos.length
    } catch (error) {
      console.error(`Error while ipfs.swarm.peers: ${error}`)
      return offlinePeerCount
    }
  }

  async function runIfNotIdle (action) {
    try {
      const state = await browser.idle.queryState(idleInSecs)
      if (state === 'active') {
        return action()
      }
    } catch (error) {
      console.error('Unable to read idle state, executing action without idle check', error)
      return action()
    }
  }

  // browserAction
  // -------------------------------------------------------------------

  async function updateBrowserActionBadge () {
    if (typeof browser.browserAction.setBadgeBackgroundColor === 'undefined') {
      // Firefox for Android does not have this UI, so we just skip it
      return
    }

    let badgeText, badgeColor, badgeIcon

    badgeText = ''
    if (state.peerCount > 0) {
      // All is good (online with peers)
      badgeColor = '#418B8E'
      badgeIcon = '/icons/ipfs-logo-on.svg'

      // prevent text overflow when peer count has more than 3 digits
      badgeText = (state.peerCount > 999) ? (Math.floor(state.peerCount / 1000).toString() + 'k') : state.peerCount.toString()
    } else if (state.peerCount === 0) {
      // API is online but no peers
      badgeColor = 'red'
      badgeIcon = '/icons/ipfs-logo-on.svg'
    } else {
      // API is offline
      badgeColor = '#8C8C8C'
      badgeIcon = '/icons/ipfs-logo-off.svg'
    }
    try {
      const oldColor = colorArraytoHex(await browser.browserAction.getBadgeBackgroundColor({}))
      if (badgeColor !== oldColor) {
        await browser.browserAction.setBadgeBackgroundColor({ color: badgeColor })
        await setBrowserActionIcon(badgeIcon)
      }
      const oldText = await browser.browserAction.getBadgeText({})
      if (oldText !== badgeText) await browser.browserAction.setBadgeText({ text: badgeText })
    } catch (error) {
      console.error('Unable to update browserAction badge due to error', error)
    }
  }

  // ColorArray [0,0,0,0] → Hex #000000
  // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/ColorArray
  function colorArraytoHex (colorArray) {
    if (!colorArray) return ''
    const colorAt = i => colorArray[i].toString(16).padStart(2, '0')
    const r = colorAt(0)
    const g = colorAt(1)
    const b = colorAt(2)
    return ('#' + r + g + b).toUpperCase()
  }

  async function setBrowserActionIcon (iconPath) {
    let iconDefinition = { path: iconPath }
    try {
      // Try SVG first -- Firefox supports it natively
      await browser.browserAction.setIcon(iconDefinition)
    } catch (error) {
      // Fallback!
      // Chromium does not support SVG [ticket below is 8 years old, I can't even..]
      // https://bugs.chromium.org/p/chromium/issues/detail?id=29683
      // Still, we want icon, so we precompute rasters of popular sizes and use them instead
      iconDefinition = await rasterIconDefinition(iconPath)
      await browser.browserAction.setIcon(iconDefinition)
    }
  }

  // OPTIONS
  // ===================================================================

  async function updateAutomaticModeRedirectState (oldPeerCount, newPeerCount) {
    // enable/disable gw redirect based on API going online or offline
    if (state.automaticMode && state.localGwAvailable) {
      if (oldPeerCount === offlinePeerCount && newPeerCount > offlinePeerCount && !state.redirect) {
        await browser.storage.local.set({ useCustomGateway: true })
        reloadIpfsClientOfflinePages(browser, ipfs, state)
      } else if (newPeerCount === offlinePeerCount && state.redirect) {
        await browser.storage.local.set({ useCustomGateway: false })
      }
    }
  }

  async function onStorageChange (changes, area) {
    let shouldReloadExtension = false
    let shouldRestartIpfsClient = false
    let shouldStopIpfsClient = false

    for (const key in changes) {
      const change = changes[key]
      if (change.oldValue === change.newValue) continue

      // debug info
      log(`storage key "${key}" changed: "${change.oldValue}" → "${change.newValue}"`)
      switch (key) {
        case 'active':
          state[key] = change.newValue
          await registerSubdomainProxy(getState, runtime)
          shouldRestartIpfsClient = true
          shouldStopIpfsClient = !state.active
          // Any time the extension switches active state, start or stop the current session.
          state.active ? startSession() : endSession()
          break
        case 'ipfsNodeType':
          if (change.oldValue !== braveNodeType && change.newValue === braveNodeType) {
            useBraveEndpoint(browser)
          } else if (change.oldValue === braveNodeType && change.newValue !== braveNodeType) {
            releaseBraveEndpoint(browser)
          }
          shouldRestartIpfsClient = true
          state[key] = change.newValue
          break
        case 'ipfsNodeConfig':
          shouldRestartIpfsClient = true
          state[key] = change.newValue
          break
        case 'ipfsApiUrl':
          state.apiURL = safeURL(change.newValue, { useLocalhostName: false }) // go-ipfs returns 403 if IP is beautified to 'localhost'
          state.apiURLString = state.apiURL.toString()
          shouldRestartIpfsClient = true
          break
        case 'ipfsApiPollMs':
          setApiStatusUpdateInterval(change.newValue)
          break
        case 'customGatewayUrl':
          state.gwURL = safeURL(change.newValue, { useLocalhostName: state.useSubdomains })
          state.gwURLString = state.gwURL.toString()
          break
        case 'publicGatewayUrl':
          state.pubGwURL = new URL(change.newValue)
          state.pubGwURLString = state.pubGwURL.toString()
          break
        case 'publicSubdomainGatewayUrl':
          state.pubSubdomainGwURL = new URL(change.newValue)
          state.pubSubdomainGwURLString = state.pubSubdomainGwURL.toString()
          break
        case 'useCustomGateway':
          state.redirect = change.newValue
          break
        case 'useSubdomains':
          state[key] = change.newValue
          await browser.storage.local.set({
            // We need to update the hostname in customGatewayUrl because:
            // 127.0.0.1 - path gateway
            // localhost - subdomain gateway
            // and we need to use the latter
            customGatewayUrl: guiURLString(state.gwURLString, {
              useLocalhostName: state.useSubdomains
            })
          })
          // Finally, update proxy settings based on the state
          await registerSubdomainProxy(getState, runtime)
          break
        case 'dnslinkPolicy':
          state.dnslinkPolicy = String(change.newValue) === 'false' ? false : change.newValue
          if (state.dnslinkPolicy === 'best-effort' && !state.detectIpfsPathHeader) {
            await browser.storage.local.set({ detectIpfsPathHeader: true })
          }
          break
        case 'logNamespaces':
          shouldReloadExtension = true
          state[key] = localStorage.debug = change.newValue
          break
        default:
          state[key] = change.newValue
          break
      }
    }
    // ensure consent is set properly on state changes
    handleConsentFromState(state)

    if ((state.active && shouldRestartIpfsClient) || shouldStopIpfsClient) {
      try {
        log('stoping ipfs client due to config changes', changes)
        await destroyIpfsClient(browser)
      } catch (err) {
        console.error('[ipfs-companion] Failed to destroy IPFS client', err)
        notify('notify_stopIpfsNodeErrorTitle', err.message)
      } finally {
        ipfs = null
      }

      if (!state.active) return

      try {
        log('starting ipfs client with the new config')
        ipfs = await initIpfsClient(browser, state)
      } catch (err) {
        console.error('[ipfs-companion] Failed to init IPFS client', err)
        notify(
          'notify_startIpfsNodeErrorTitle',
          err.name === 'ValidationError' ? err.details[0].message : err.message
        )
      }

      apiStatusUpdate()
    }
    if (shouldReloadExtension) {
      log('reloading extension due to config change')
      browser.tabs.reload() // async reload of options page to keep it alive
      await browser.runtime.reload()
    }
    // Post update to Browser Action (if exists) -- this gives UX a snappy feel
    await sendStatusUpdateToBrowserAction()
  }

  // Public API
  // (typically attached to a window variable to interact with this companion instance)
  const api = {
    get ipfs () {
      return ipfs
    },

    get state () {
      return state
    },

    get runtime () {
      return runtime
    },

    get dnslinkResolver () {
      return dnslinkResolver
    },

    get ipfsPathValidator () {
      return ipfsPathValidator
    },

    get notify () {
      return notify
    },

    get ipfsImportHandler () {
      return ipfsImportHandler
    },

    async destroy () {
      if (state) {
        state.active = false
        state.peerCount = -1 // api down
      }

      if (apiStatusUpdateInterval) {
        clearInterval(apiStatusUpdateInterval)
        apiStatusUpdateInterval = null
      }

      if (ipfs) {
        await destroyIpfsClient(browser)
        ipfs = null
      }
    }
  }

  return api
}

const rasterIconDefinition = pMemoize((svgPath) => {
  const pngPath = (size) => {
    // point at precomputed PNG file
    const baseName = /\/icons\/(.+)\.svg/.exec(svgPath)[1]
    return `/icons/png/${baseName}_${size}.png`
  }
  // icon sizes to cover ranges from:
  // - https://bugs.chromium.org/p/chromium/issues/detail?id=647182
  // - https://developer.chrome.com/extensions/manifest/icons
  const r19 = pngPath(19)
  const r38 = pngPath(38)
  const r128 = pngPath(128)
  // return computed values to be cached by p-memoize
  return { path: { 19: r19, 38: r38, 128: r128 } }
})


text:
'use strict'
/* eslint-env browser */

const stableChannels = new Set([
  'ipfs-firefox-addon@lidel.org', // firefox (for legacy reasons)
  'nibjojkomfdiaoajekhjakgkdhaomnch' // chromium (chrome web store)
])

const stableChannelFormUrl = 'https://docs.google.com/forms/d/e/1FAIpQLSfLF7uzaxRKiF4XpPL9_DvkdaQHoRnDihRTZ1uVL6ceQwIrtg/viewform'

export function getUninstallURL (browser) {
  // on uninstall feedback form shown only on stable channel
  return stableChannels.has(browser.runtime.id) ? stableChannelFormUrl : ''
}


text:
'use strict'
/* eslint-env browser, webextensions */
import pWaitFor from 'p-wait-for'

import debug from 'debug'

import * as external from './external.js'
import toUri from 'multiaddr-to-uri'
import { welcomePage, optionsPage, tickMs } from '../constants.js'
const log = debug('ipfs-companion:client:brave')
log.error = debug('ipfs-companion:client:brave:error')

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

// wrapper for chrome.ipfs.* that gets us closer to ergonomics of promise-based browser.*
export const 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

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

export async function destroy (browser) {
  log('shuting down node managed by Brave')
  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
// Here we use inlined empty byte array, which resolves instantly and does not
// introduce any delay in UI.
const braveIpfsUriTrigger = 'ipfs://bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss/'
const braveGatewayUrlTrigger = 'https://bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss.ipfs.dweb.link/'

// Settings screen in Brave where user can manage IPFS support
const braveSettingsPage = 'brave://settings/extensions'
// TODO: replace with brave://settings/ipfs after https://github.com/brave/brave-browser/issues/13655 lands in Brave Stable

// Diagnostic page for manually starting/stopping Brave's node
// const braveIpfsDiagnosticPage = 'brave://ipfs' // TODO: https://github.com/brave/brave-browser/issues/14500

// ipfsNodeType for this backend
export const braveNodeType = 'external:brave'

// 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.

export async function useBraveEndpoint (browser) {
  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]
  })
}

export async function releaseBraveEndpoint (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 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.
    await browser.tabs.create({ url: braveIpfsUriTrigger })

    // 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..')
      await waitFor(() => brave.getExecutableAvailable())

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

  if (method !== 'local') {
    // user picked remote gateway mode, turn Companion off for now
    // (this way clicking power button in browser action menu will re-run activation flow)
    await browser.storage.local.set({ active: false })
    // close tab with temporary trigger URI
    await closeIpfsTab(browser, braveIpfsUriTrigger)
    await closeIpfsTab(browser, braveGatewayUrlTrigger)
    // open settings
    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 waitFor(() => brave.launch())
  log('brave.launch() finished')

  // ensure Companion uses the endpoint provided by Brave
  await useBraveEndpoint(browser)

  // async UI cleanup, after other stuff
  setTimeout(() => activationUiCleanup(browser), tickMs)
}

// close tab in a way that works with ipfs://
async function closeIpfsTab (browser, tabUrl) {
  // fun bug: querying for { url: 'ipfs://..' } does not work,
  // but if we query for { } ipfs:// tabs are returned just fine,
  // so we do that and discard unwanted ones  ¯\_(ツ)_/¯
  // TODO: fix chrome.tabs.query when we care about upstreaming things to Chromium
  for (const tab of await browser.tabs.query({})) {
    if (tab.url === tabUrl) {
      await browser.tabs.remove(tab.id)
    }
  }
}

// Various tedious tasks that need to happen for nice UX:
// - wait for gateway to be up (indicates node finished booting)
// - close ephemeral activation tab
// - re-activate entry point (options or welcome page)
// - ignore unexpected failures (user could do something weird, close tab before time etc)
async function activationUiCleanup (browser) {
  try {
    // after useBraveEndpoint we can start polling for gateway to become online
    const { customGatewayUrl: braveGwUrl } = await browser.storage.local.get('customGatewayUrl')
    // wait 1m for gateway to be online (bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss)
    await waitFor(async () => {
      try {
        return await fetch(`${braveGwUrl}/ipfs/bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss`).then(response => response.ok)
      } catch (_) {
        return false
      }
    })
    log('[activation ui cleanup] Brave gateway is up, cleaning up')

    const welcomePageUrl = browser.runtime.getURL(welcomePage)
    const optionsPageUrl = browser.runtime.getURL(optionsPage)
    // we are unable to query ipfs:// directly due to reasons mentioned in 'closeIpfsTab'
    // so we make quick pass over all tabs and check welcome and options while at it.
    for (const tab of await browser.tabs.query({})) {
      try {
        // close tab with temporary trigger
        if (tab.url === braveIpfsUriTrigger || tab.url === braveGatewayUrlTrigger) {
          await browser.tabs.remove(tab.id)
        }
        // switch to welcome page if present (onboarding via fresh install)
        if (tab.url === welcomePageUrl) {
          await browser.tabs.reload(tab.id)
          await browser.tabs.update(tab.id, { active: true })
        }
        // switch to options page if present (onboarding via Preferences)
        if (tab.url === optionsPageUrl) {
          await browser.tabs.update(tab.id, { active: true })
        }
      } catch (e) {
        log.error('[activation ui cleanup] unexpected error, but safe to ignore', e)
        continue
      }
    }
    log('[activation ui cleanup] done')

    // (if ok or not, close temporary tab and switch to welcome page or open it if not existing
    // if ((await browser.tabs.get(tabId)).url.startsWith(braveIpfsUriTrigger)) {
  } catch (e) {
    // most likely tab is gone (closed by user, etc)
    log.error('[activation ui cleanup] failed to cleanup ephemeral UI tab', e)
  }
}


text:
'use strict'

import debug from 'debug'

import mergeOptions from 'merge-options'
import { create } from 'ipfs-core'
import { optionDefaults } from '../options.js'
const log = debug('ipfs-companion:client:embedded')
log.error = debug('ipfs-companion:client:embedded:error')

let node = null

export async function init (browser, opts) {
  log('init')
  const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig)
  const userOpts = JSON.parse(opts.ipfsNodeConfig)
  const ipfsOpts = mergeOptions(defaultOpts, userOpts, { start: true })
  const missing = (array) => (!Array.isArray(array) || !array.length)
  const { Addresses } = ipfsOpts.config
  if (missing(Addresses.Swarm)) {
    Addresses.Swarm = [
      '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star',
      '/dns4/wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star'
    ]
  }
  if (missing(ipfsOpts.Delegates)) {
    Addresses.Delegates = [
      '/dns4/node0.delegate.ipfs.io/tcp/443/https',
      '/dns4/node1.delegate.ipfs.io/tcp/443/https',
      '/dns4/node2.delegate.ipfs.io/tcp/443/https',
      '/dns4/node3.delegate.ipfs.io/tcp/443/https'
    ]
  }
  node = await create(ipfsOpts)
  return node
}

export async function destroy (browser) {
  log('destroy')
  if (!node) return

  await node.stop()
  node = null
}


text:
'use strict'

/* eslint-env browser, webextensions */

import debug from 'debug'

import * as external from './external.js'
import * as embedded from './embedded.js'
import * as brave from './brave.js'
import { precache } from '../precache.js'
import {
  prepareReloadExtensions, WebUiReloader, LocalGatewayReloader, InternalTabReloader
} from './reloaders/index.js'
const log = debug('ipfs-companion:client')
log.error = debug('ipfs-companion:client:error')

// ensure single client at all times, and no overlap between init and destroy
let client

export async function initIpfsClient (browser, opts) {
  log('init ipfs client')
  if (client) return // await destroyIpfsClient()
  let backend
  switch (opts.ipfsNodeType) {
    case 'embedded:chromesockets':
      // TODO: remove this one-time migration after in second half of 2021
      setTimeout(async () => {
        log('converting embedded:chromesockets to native external:brave')
        opts.ipfsNodeType = 'external:brave'
        await browser.storage.local.set({
          ipfsNodeType: 'external:brave',
          ipfsNodeConfig: '{}' // remove chrome-apps config
        })
        await browser.tabs.create({ url: 'https://docs.ipfs.tech/how-to/companion-node-types/#native' })
      }, 0)
      // Halt client init
      throw new Error('Embedded + chrome.sockets is deprecated. Switching to Native IPFS in Brave.')
    case 'embedded':
      backend = embedded
      break
    case 'external:brave':
      backend = brave
      break
    case 'external':
      backend = external
      break
    default:
      throw new Error(`Unsupported ipfsNodeType: ${opts.ipfsNodeType}`)
  }
  const instance = await backend.init(browser, opts)
  _reloadIpfsClientDependents(browser, instance, opts) // async (API is present)
  client = backend
  return instance
}

export async function destroyIpfsClient (browser) {
  log('destroy ipfs client')
  if (!client) return
  try {
    await client.destroy(browser)
    await _reloadIpfsClientDependents(browser) // sync (API stopped working)
  } finally {
    client = null
  }
}

/**
 * Reloads pages dependant on ipfs to be online
 *
 * @typedef {embedded|brave|external} Browser
 * @param {Browser} browser
 * @param {import('kubo-rpc-client').default} instance
 * @param {Object} opts
 * @param {Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]=} reloadExtensions
 * @returns {void}
 */
async function _reloadIpfsClientDependents (
  browser, instance, opts, reloadExtensions = [WebUiReloader, LocalGatewayReloader, InternalTabReloader]) {
  // online || offline
  if (browser.tabs && browser.tabs.query) {
    const tabs = await browser.tabs.query({})
    if (tabs) {
      try {
        const reloadExtensionInstances = await prepareReloadExtensions(reloadExtensions, browser, log)
        // the reload process is async, fire and forget.
        reloadExtensionInstances.forEach(ext => ext.reload(tabs))
      } catch (e) {
        log('Failed to trigger reloaders')
      }
    }
  }

  // online only
  if (client && instance && opts) {
    // add important data to local ipfs repo for instant load
    setTimeout(() => precache(instance, opts), 5000)
  }
}

/**
 * Reloads local gateway pages dependant on ipfs to be online
 *
 * @typedef {embedded|brave|external} Browser
 * @param {Browser} browser
 * @param {import('kubo-rpc-client').default} instance
 * @param {Object} opts
 * @returns {void}
 */
export function reloadIpfsClientOfflinePages (browser, instance, opts) {
  _reloadIpfsClientDependents(browser, instance, opts, [LocalGatewayReloader])
}


text:
'use strict'
/* eslint-env browser */

import debug from 'debug'

import { create } from 'kubo-rpc-client'
const log = debug('ipfs-companion:client:external')
log.error = debug('ipfs-companion:client:external:error')

export async function init (browser, opts) {
  log(`init with IPFS API at ${opts.apiURLString}`)
  const clientConfig = opts.apiURLString
  const api = await create(clientConfig)
  return api
}

export async function destroy (browser) {
  log('destroy')
}


text:
import ReloaderBase from './reloaderBase.js'

export default class WebUiReloader extends ReloaderBase {
  /**
   * Performs url validation for the tab. If tab is a WebUI tab.
   *
   * @param {Object} tab
   * @returns {boolean}
   */
  validation ({ url }) {
    const bundled = !url.startsWith('http') && url.includes('/webui/index.html#/')
    const ipns = url.includes('/webui.ipfs.io/#/')
    return bundled || ipns
  }

  /**
   * Returns message when reloading the tab.
   *
   * @param {Object} tab
   * @param {string} tab.url
   * @returns {string} message.
   */
  message ({ url }) {
    return `reloading webui at ${url}`
  }
}


text:
import InternalTabReloader from './internalTabReloader.js'
import LocalGatewayReloader from './localGatewayReloader.js'
import WebUiReloader from './webUiReloader.js'

/**
 * Prepares extension by creating an instance and awaiting for init.
 *
 * @param {Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]} extensions
 * @param {Browser} browserInstance
 * @param {Logger} loggerInstance
 * @returns {Promise<Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]>}
 */
function prepareReloadExtensions (extensions, browserInstance, loggerInstance) {
  const reloadExtensions = Array.isArray(extensions) ? extensions : [extensions]
  return Promise.all(reloadExtensions
    .map(async Ext => {
      try {
        const ext = new Ext(browserInstance, loggerInstance)
        await ext.init()
        return ext
      } catch (e) {
        loggerInstance(`Extension Instance Failed to Initialize with error: ${e}. Extension: ${Ext}`)
      }
    })
  )
}

export {
  InternalTabReloader,
  LocalGatewayReloader,
  WebUiReloader,
  prepareReloadExtensions
}


text:
export default class ReloaderBase {
  /**
   * Constructor for reloader base class.
   *
   * @param {Browser} browser
   * @param {Logger} log
   */
  constructor (browser, log) {
    if (!browser || !log) {
      throw new Error('Instances of browser and logger are needed!')
    }
    this._browserInstance = browser
    this._log = log
  };

  /**
   * Initializes the instance.
   */
  init () {
    this._log('Initialized without additional config.')
  }

  /**
   * To be implemented in child class.
   */
  validation () {
    throw new Error('Validation: Method Not Implemented')
  }

  /**
   * To be implemented in child class.
   */
  message () {
    throw new Error('Message: Method Not Implemented')
  }

  /**
   * Handles reload for all tabs.
   * params {Array<tabs>}
   */
  reload (tabs) {
    tabs
      .filter(tab => this.validation(tab))
      .forEach(tab => {
        this._log(this.message(tab))
        this._browserInstance.tabs.reload(tab.id)
      })
  }
}


text:
import ReloaderBase from './reloaderBase.js'
import isIPFS from 'is-ipfs'

export default class LocalGatewayReloader extends ReloaderBase {
  /**
   * Performs url validation for the tab. If tab is loaded via local gateway.
   *
   * @param {Object} tab
   * @param {string} tab.url
   * @param {string} tab.url
   * @returns {boolean}
   */
  validation ({ url, title }) {
    /**
     * Check if the url is the local gateway url and if the title is contained within the url then it was not loaded.
     * - This assumes that the title of most pages on the web will be set and hence when not reachable, the browser
     *   will set title to the url/host (both chrome and brave) and 'problem loading page' for firefox.
     * - There is probability that this might be true in case the <title> tag is omitted, but worst case it only reloads
     *   those pages.
     * - The benefit we get from this approach is the static nature of just observing the tabs in their current state
     *   which reduces the overhead of injecting content scripts to track urls that were loaded after the connection
     *   was offline, it may also need extra permissions to inject code on error pages.
     */
    return (isIPFS.url(url) || isIPFS.subdomain(url)) &&
      (url.includes(title) || title.toLowerCase() === 'problem loading page')
  }

  /**
   * Returns message when reloading the tab.
   *
   * @param {Object} tab
   * @param {string} tab.url
   * @returns {string} message.
   */
  message ({ url }) {
    return `reloading local gateway at ${url}`
  }
}


text:
import ReloaderBase from './reloaderBase.js'

export default class InternalTabReloader extends ReloaderBase {
  /**
   * Setting up the extension origin.
   */
  init () {
    this.extensionOrigin = this._browserInstance.runtime.getURL('/')
    this._log('InternalTabReloader Ready for use.')
  }

  /**
   * Performs url validation for the tab. If tab is a WebUI tab.
   *
   * @param {Object} tab
   * @param {string} tab.url
   * @returns {boolean}
   */
  validation ({ url }) {
    return url.startsWith(this.extensionOrigin)
  }

  /**
   * Returns message when reloading the tab.
   *
   * @param {Object} tab
   * @param {string} tab.url
   * @returns {string} message.
   */
  message ({ url }) {
    return `reloading internal extension page at ${url}`
  }
}