diff --git a/.gitignore b/.gitignore index a609aa0fa..6cca7dbc8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build npm-debug.log yarn-error.log crowdin.yml +.connect-deps* .*~ add-on/dist add-on/webui/ diff --git a/add-on/_locales/en/messages.json b/add-on/_locales/en/messages.json index 02152da13..53122f7ce 100644 --- a/add-on/_locales/en/messages.json +++ b/add-on/_locales/en/messages.json @@ -279,6 +279,14 @@ "message": "Redirect requests for IPFS resources to the Custom gateway", "description": "An option description on the Preferences screen (option_useCustomGateway_description)" }, + "option_useSubdomainProxy_title": { + "message": "Use Subdomain Proxy", + "description": "An option title on the Preferences screen (option_useSubdomainProxy_title)" + }, + "option_useSubdomainProxy_description": { + "message": "Use Custom Gateway as HTTP Proxy to enable Origin isolation per content root at *.ipfs.localhost", + "description": "An option description on the Preferences screen (option_useSubdomainProxy_description)" + }, "option_dnslinkRedirect_title": { "message": "Load websites from Custom Gateway", "description": "An option title on the Preferences screen (option_dnslinkRedirect_title)" diff --git a/add-on/_locales/nl/messages.json b/add-on/_locales/nl/messages.json index f3d8ee959..2f04b3e0f 100644 --- a/add-on/_locales/nl/messages.json +++ b/add-on/_locales/nl/messages.json @@ -268,7 +268,7 @@ "description": "An option description on the Preferences screen (option_customGatewayUrl_description)" }, "option_customGatewayUrl_warning": { - "message": "IPFS content will be blocked from loading on HTTPS websites unless your gateway URL starts with “http://127.0.0.1”, “http://[::1]” or “https://”", + "message": "IPFS content will be blocked from loading on HTTPS websites unless your gateway URL starts with “http://localhost”, “http://127.0.0.1”, “http://[::1]” or “https://”", "description": "A warning on the Preferences screen, displayed when URL does not belong to Secure Context (option_customGatewayUrl_warning)" }, "option_useCustomGateway_title": { diff --git a/add-on/manifest.common.json b/add-on/manifest.common.json index bd5abd409..8aa8f9f80 100644 --- a/add-on/manifest.common.json +++ b/add-on/manifest.common.json @@ -20,6 +20,7 @@ "unlimitedStorage", "contextMenus", "clipboardWrite", + "proxy", "webNavigation", "webRequest", "webRequestBlocking" diff --git a/add-on/src/lib/http-proxy.js b/add-on/src/lib/http-proxy.js new file mode 100644 index 000000000..ec8c12889 --- /dev/null +++ b/add-on/src/lib/http-proxy.js @@ -0,0 +1,112 @@ +'use strict' +/* eslint-env browser, webextensions */ + +const browser = require('webextension-polyfill') +const { safeURL } = require('./options') + +const debug = require('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-beta.ipfs.io/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 +async function registerSubdomainProxy (getState, runtime) { + const { useSubdomainProxy: enable, gwURLString } = getState() + + // HTTP Proxy feature is exposed on the gateway port + // Just ensure we use localhost IP to remove any dependency on DNS + const proxy = safeURL(gwURLString, { useLocalhostName: false }) + + // Firefox uses own APIs for selective proxying + if (runtime.isFirefox) { + return registerSubdomainProxyFirefox(enable, proxy.hostname, proxy.port) + } + + // at this point we asume Chromium + return registerSubdomainProxyChromium(enable, proxy.host) +} + +// storing listener for later +var 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, host, 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 + onRequestProxyListener = (request) => ({ type: 'http', host, port }) + + // register the listener + onRequest.addListener(onRequestProxyListener, { + urls: ['http://*.localhost/*'], + incognito: false + }) + log(`enabled ${host}:${port} as HTTP proxy for *.localhost`) + return + } + + // at this point we effectively disabled proxy + log('disabled HTTP proxy for *.localhost') +} + +// registerSubdomainProxyChromium sets proxy using API available in Chromium +// https://developer.chrome.com/extensions/proxy +async function registerSubdomainProxyChromium (enable, proxyHost) { + const get = async (opts) => new Promise(resolve => chrome.proxy.settings.get(opts, resolve)) + const set = async (opts) => new Promise(resolve => chrome.proxy.settings.set(opts, resolve)) + const clear = async (opts) => new Promise(resolve => chrome.proxy.settings.clear(opts, resolve)) + 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'))\n" + + ` return 'PROXY ${proxyHost}';\n` + + " return 'DIRECT';\n" + + '}' + } + } + set({ value: pacConfig, scope }) + log(`enabled ${proxyHost} 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') + } +} + +module.exports.registerSubdomainProxy = registerSubdomainProxy diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index 334ad25cb..fe8f3e97e 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -8,9 +8,9 @@ log.error = debug('ipfs-companion:main:error') const browser = require('webextension-polyfill') const toMultiaddr = require('uri-to-multiaddr') const pMemoize = require('p-memoize') -const { optionDefaults, storeMissingOptions, migrateOptions } = require('./options') +const { optionDefaults, storeMissingOptions, migrateOptions, guiURLString } = require('./options') const { initState, offlinePeerCount } = require('./state') -const { createIpfsPathValidator } = require('./ipfs-path') +const { createIpfsPathValidator, sameGateway } = require('./ipfs-path') const createDnslinkResolver = require('./dnslink') const { createRequestModifier } = require('./ipfs-request') const { initIpfsClient, destroyIpfsClient } = require('./ipfs-client') @@ -22,6 +22,7 @@ const createInspector = require('./inspector') const { createRuntimeChecks } = require('./runtime-checks') const { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuViewOnGateway } = require('./context-menus') const createIpfsProxy = require('./ipfs-proxy') +const { registerSubdomainProxy } = require('./http-proxy') const { showPendingLandingPages } = require('./on-installed') // init happens on addon load in background/background.js @@ -83,6 +84,7 @@ module.exports = async function init () { ipfsProxyContentScript = await registerIpfsProxyContentScript() log('register all listeners') registerListeners() + await registerSubdomainProxy(getState, runtime) await setApiStatusUpdateInterval(options.ipfsApiPollMs) log('init done') await showPendingLandingPages() @@ -353,7 +355,7 @@ module.exports = async function init () { // 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 (url.startsWith(state.gwURLString) || url.startsWith(state.apiURLString)) { + if (sameGateway(url, state.gwURL) || sameGateway(url, state.apiURL)) { await browser.pageAction.setIcon({ tabId: tabId, path: '/icons/ipfs-logo-on.svg' }) await browser.pageAction.setTitle({ tabId: tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtCustomGateway') }) } else { @@ -619,7 +621,8 @@ module.exports = async function init () { case 'customGatewayUrl': state.gwURL = new URL(change.newValue) state.gwURLString = state.gwURL.toString() - state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/` + // TODO: for now we load webui from API port, should we remove this? + // state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/` break case 'publicGatewayUrl': state.pubGwURL = new URL(change.newValue) @@ -632,8 +635,26 @@ module.exports = async function init () { case 'useCustomGateway': state.redirect = change.newValue break + case 'useSubdomainProxy': + state[key] = change.newValue + // More work is needed, as this key decides how requests are routed + // to the gateway: + await browser.storage.local.set({ + // We need to update the hostname in customGatewayUrl: + // 127.0.0.1 - path gateway + // localhost - subdomain gateway + customGatewayUrl: guiURLString( + state.gwURLString, { + useLocalhostName: state.useSubdomainProxy + } + ) + }) + // Finally, update proxy settings based on the state + await registerSubdomainProxy(getState, runtime) + break case 'ipfsProxy': state[key] = change.newValue + // This is window.ipfs proxy, requires update of the content script: ipfsProxyContentScript = await registerIpfsProxyContentScript() break case 'dnslinkPolicy': @@ -642,16 +663,12 @@ module.exports = async function init () { await browser.storage.local.set({ detectIpfsPathHeader: true }) } break - case 'recoverFailedHttpRequests': - state[key] = change.newValue - break case 'logNamespaces': shouldReloadExtension = true state[key] = localStorage.debug = change.newValue break + case 'recoverFailedHttpRequests': case 'importDir': - state[key] = change.newValue - break case 'linkify': case 'catchUnhandledProtocols': case 'displayNotifications': diff --git a/add-on/src/lib/ipfs-path.js b/add-on/src/lib/ipfs-path.js index 7aa623758..0850f071b 100644 --- a/add-on/src/lib/ipfs-path.js +++ b/add-on/src/lib/ipfs-path.js @@ -1,13 +1,13 @@ 'use strict' /* eslint-env browser */ -const IsIpfs = require('is-ipfs') +const isIPFS = require('is-ipfs') const isFQDN = require('is-fqdn') function normalizedIpfsPath (urlOrPath) { let result = urlOrPath // Convert CID-in-subdomain URL to /ipns// path - if (IsIpfs.subdomain(urlOrPath)) { + if (isIPFS.subdomain(urlOrPath)) { result = subdomainToIpfsPath(urlOrPath) } // Drop everything before the IPFS path @@ -16,7 +16,7 @@ function normalizedIpfsPath (urlOrPath) { // https://github.com/ipfs/ipfs-companion/issues/303 result = decodeURIComponent(result) // Return a valid IPFS path or null otherwise - return IsIpfs.path(result) ? result : null + return isIPFS.path(result) ? result : null } exports.normalizedIpfsPath = normalizedIpfsPath @@ -24,14 +24,20 @@ function subdomainToIpfsPath (url) { if (typeof url === 'string') { url = new URL(url) } - const match = url.toString().match(IsIpfs.subdomainPattern) - if (!match) throw new Error('no match for IsIpfs.subdomainPattern') + const { id, ns } = subdomainPatternMatch(url) + return `/${ns}/${id}${url.pathname}${url.search}${url.hash}` +} - // TODO: support CID split with commas - const cid = match[1] - // TODO: support .ip(f|n)s. being at deeper levels - const protocol = match[2] - return `/${protocol}/${cid}${url.pathname}${url.search}${url.hash}` +// TODO: add tests +function subdomainPatternMatch (url) { + if (typeof url === 'string') { + url = new URL(url) + } + const match = url.toString().match(isIPFS.subdomainPattern) + if (!match || match.length < 3) return false + const id = match[1] + const ns = match[2] + return { id, ns } } function pathAtHttpGateway (path, gatewayUrl) { @@ -40,17 +46,16 @@ function pathAtHttpGateway (path, gatewayUrl) { } exports.pathAtHttpGateway = pathAtHttpGateway -function redirectSubdomainGateway (url, subdomainGateway) { +function swapSubdomainGateway (url, subdomainGwURL) { if (typeof url === 'string') { url = new URL(url) } - const match = url.toString().match(IsIpfs.subdomainPattern) - if (!match) throw new Error('no match for IsIpfs.subdomainPattern') - const cid = match[1] - const protocol = match[2] - return trimDoubleSlashes(`${subdomainGateway.protocol}//${cid}.${protocol}.${subdomainGateway.hostname}${url.pathname}${url.search}${url.hash}`) + const { id, ns } = subdomainPatternMatch(url) + if (!id || !ns) throw new Error('no matching isIPFS.*subdomainPattern') + return new URL(trimDoubleSlashes( + `${subdomainGwURL.protocol}//${id}.${ns}.${subdomainGwURL.hostname}${url.pathname}${url.search}${url.hash}` + )).toString() } -exports.redirectSubdomainGateway = redirectSubdomainGateway function trimDoubleSlashes (urlString) { return urlString.replace(/([^:]\/)\/+/g, '$1') @@ -63,13 +68,40 @@ function trimHashAndSearch (urlString) { } exports.trimHashAndSearch = trimHashAndSearch +// returns true if URL belongs to the gateway +function sameGateway (url, gwUrl) { + if (typeof url === 'string') { + url = new URL(url) + } + if (typeof gwUrl === 'string') { + gwUrl = new URL(gwUrl) + } + const gws = [gwUrl.hostname] + + // localhost gateway has more than one hostname + if (gwUrl.hostname === 'localhost') { + gws.push(`127.0.0.1:${gwUrl.port}`) + } + if (gwUrl.hostname === '127.0.0.1' || url.hostname === '[::1]') { + gws.push(`localhost:${gwUrl.port}`) + } + + for (const gwName of gws) { + // match against the end to include subdomain gateways + if (url.hostname.endsWith(gwName)) return true + } + return false +} +exports.sameGateway = sameGateway + function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { const ipfsPathValidator = { // Test if URL is a Public IPFS resource // (pass validIpfsOrIpnsUrl(url) and not at the local gateway or API) publicIpfsOrIpnsResource (url) { // exclude custom gateway and api, otherwise we have infinite loops - if (!url.startsWith(getState().gwURLString) && !url.startsWith(getState().apiURLString)) { + const { gwURL, apiURL } = getState() + if (!sameGateway(url, gwURL) && !sameGateway(url, apiURL)) { return validIpfsOrIpnsUrl(url, dnslinkResolver) } return false @@ -87,41 +119,62 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { return validIpfsOrIpnsPath(path, dnslinkResolver) }, // Test if URL is a subdomain gateway resource - // TODO: add test if URL is a public subdomain resource ipfsOrIpnsSubdomain (url) { - return IsIpfs.subdomain(url) + return isIPFS.subdomain(url) }, // Test if actions such as 'copy URL', 'pin/unpin' should be enabled for the URL isIpfsPageActionsContext (url) { - return Boolean(url && !url.startsWith(getState().apiURLString) && ( - IsIpfs.url(url) || - IsIpfs.subdomain(url) || - dnslinkResolver.cachedDnslink(new URL(url).hostname) + const { apiURLString } = getState() + const { hostname } = new URL(url) + return Boolean(url && !url.startsWith(apiURLString) && ( + isIPFS.url(url) || + isIPFS.subdomain(url) || + dnslinkResolver.cachedDnslink(hostname) )) }, // Test if actions such as 'per site redirect toggle' should be enabled for the URL isRedirectPageActionsContext (url) { - const state = getState() - return state.ipfsNodeType !== 'embedded' && // hide with embedded node - (IsIpfs.ipnsUrl(url) || // show on /ipns/ + const { ipfsNodeType, gwURL, apiURL } = getState() + return ipfsNodeType !== 'embedded' && // hide with embedded node + (isIPFS.ipnsUrl(url) || // show on /ipns/ (url.startsWith('http') && // hide on non-HTTP pages - !url.startsWith(state.gwURLString) && // hide on /ipfs/* - !url.startsWith(state.apiURLString))) // hide on api port + !sameGateway(url, gwURL) && // hide on /ipfs/* and *.ipfs. + !sameGateway(url, apiURL))) // hide on api port }, // Resolve URL or path to HTTP URL: - // - IPFS paths are attached to HTTP Gateway root + // - IPFS paths are attached to HTTP Path Gateway root + // - IPFS subdomains are attached to HTTP Subdomain Gateway root // - URL of DNSLinked websites are returned as-is // The purpose of this resolver is to always return a meaningful, publicly // accessible URL that can be accessed without the need of IPFS client. resolveToPublicUrl (urlOrPath, optionalGatewayUrl) { const input = urlOrPath - // CID-in-subdomain is good as-is - if (IsIpfs.subdomain(input)) return input + const { pubSubdomainGwURL, pubGwURLString } = getState() + // SUBDOMAINS + // Detect *.localhost and other subdomain gateways + if (isIPFS.subdomain(input)) { + // Switch Origin to prefered public subdomain gateway (default: dweb.link) + const subdomainUrl = swapSubdomainGateway(input, pubSubdomainGwURL) + // DNSLink in subdomains does not make sense outside of *.localhost + // and is usually not supported due to limitation of TLS wildcart certs. + // Instead, we resolve it to the canonical FQDN Origin + if (isIPFS.dnslinkSubdomain(subdomainUrl)) { + const url = new URL(subdomainUrl) + // remove gateway suffix to get original FQDN + url.hostname = url.hostname.replace(`.ipns.${pubSubdomainGwURL.hostname}`, '') + // Confirm DNSLink record is present and its not a false-positive + const dnslink = dnslinkResolver.readAndCacheDnslink(url.hostname) + if (dnslink) return url.toString() + } + return subdomainUrl + } + // PATHS // IPFS Paths should be attached to the public gateway const ipfsPath = normalizedIpfsPath(input) - const gateway = optionalGatewayUrl || getState().pubGwURLString + const gateway = optionalGatewayUrl || pubGwURLString + // TODO: make below return links to dweb.link if (ipfsPath) return pathAtHttpGateway(ipfsPath, gateway) // Return original URL (eg. DNSLink domains) or null if not an URL return input.startsWith('http') ? input : null @@ -132,10 +185,10 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { // subdomain URL resolveToPublicSubdomainUrl (url, optionalGatewayUrl) { // if non-subdomain return as-is - if (!IsIpfs.subdomain(url)) return url + if (!isIPFS.subdomain(url)) return url const gateway = optionalGatewayUrl || getState().pubSubdomainGwURL - return redirectSubdomainGateway(url, gateway) + return swapSubdomainGateway(url, gateway) }, // Resolve URL or path to IPFS Path: @@ -172,7 +225,7 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { // Fail fast if no IPFS Path if (!path) return null // Resolve /ipns/ → /ipfs/ - if (IsIpfs.ipnsPath(path)) { + if (isIPFS.ipnsPath(path)) { const labels = path.split('/') // We resolve /ipns/ as value in DNSLink cache may be out of date const ipnsRoot = `/ipns/${labels[2]}` @@ -240,7 +293,7 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { if (rawPath !== safePath) { const result = await ipfsPathValidator.resolveToCid(safePath) // return in format of ipfs.resolve() - return IsIpfs.cid(result) ? `/ipfs/${result}` : result + return isIPFS.cid(result) ? `/ipfs/${result}` : result } } } @@ -252,7 +305,7 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { getIpfs().resolve(rawPath, { recursive: true, dhtt: '5s', dhtrc: 1 }) ) - const directCid = IsIpfs.ipfsPath(result) ? result.split('/')[2] : result + const directCid = isIPFS.ipfsPath(result) ? result.split('/')[2] : result return directCid } } @@ -263,7 +316,7 @@ exports.createIpfsPathValidator = createIpfsPathValidator function validIpfsOrIpnsUrl (url, dnsLink) { // `/ipfs/` is easy to validate, we just check if CID is correct - if (IsIpfs.ipfsUrl(url)) { + if (isIPFS.ipfsUrl(url)) { return true } // `/ipns/` requires multiple stages/branches (can be FQDN with dnslink or CID) @@ -276,7 +329,7 @@ function validIpfsOrIpnsUrl (url, dnsLink) { function validIpfsOrIpnsPath (path, dnsLink) { // `/ipfs/` is easy to validate, we just check if CID is correct - if (IsIpfs.ipfsPath(path)) { + if (isIPFS.ipfsPath(path)) { return true } // `/ipns/` requires multiple stages/branches (can be FQDN with dnslink or CID) @@ -288,12 +341,12 @@ function validIpfsOrIpnsPath (path, dnsLink) { } function validIpnsPath (path, dnsLink) { - if (IsIpfs.ipnsPath(path)) { + if (isIPFS.ipnsPath(path)) { // we may have false-positives here, so we do additional checks below - const ipnsRoot = path.match(/^\/ipns\/([^/]+)/)[1] + const ipnsRoot = path.match(/^\/ipns\/([^/?#]+)/)[1] // console.log('==> IPNS root', ipnsRoot) // first check if root is a regular CID - if (IsIpfs.cid(ipnsRoot)) { + if (isIPFS.cid(ipnsRoot)) { // console.log('==> IPNS is a valid CID', ipnsRoot) return true } diff --git a/add-on/src/lib/ipfs-request.js b/add-on/src/lib/ipfs-request.js index 26f34a528..dac8f34b4 100644 --- a/add-on/src/lib/ipfs-request.js +++ b/add-on/src/lib/ipfs-request.js @@ -497,6 +497,7 @@ function isSafeToRedirect (request, runtime) { return false } + // TODO: remove below, enable redirect if subdomain proxy is enabled // For now we do not redirect if cid-in-subdomain is used // as it would break origin-based security perimeter if (IsIpfs.subdomain(request.url)) { diff --git a/add-on/src/lib/options.js b/add-on/src/lib/options.js index dc8ee2b00..8e7be5c7f 100644 --- a/add-on/src/lib/options.js +++ b/add-on/src/lib/options.js @@ -13,6 +13,7 @@ exports.optionDefaults = Object.freeze({ publicGatewayUrl: 'https://ipfs.io', publicSubdomainGatewayUrl: 'https://dweb.link', useCustomGateway: true, + useSubdomainProxy: true, noIntegrationsHostnames: [], automaticMode: true, linkify: false, @@ -36,7 +37,7 @@ exports.optionDefaults = Object.freeze({ function buildCustomGatewayUrl () { // TODO: make more robust (sync with buildDefaultIpfsNodeConfig) const port = DEFAULT_TO_EMBEDDED_GATEWAY ? 9091 : 8080 - return `http://127.0.0.1:${port}` + return `http://localhost:${port}` } function buildIpfsApiUrl () { @@ -79,19 +80,31 @@ exports.storeMissingOptions = async (read, defaults, storage) => { return changes } -function normalizeGatewayURL (url) { +// safeURL produces URL object with optional normalizations +function safeURL (url, opts) { + opts = opts || { useLocalhostName: true } if (typeof url === 'string') { url = new URL(url) } - // https://github.com/ipfs/ipfs-companion/issues/328 - if (url.hostname.toLowerCase() === 'localhost') { + // "localhost" gateway normalization matters because: + // - 127.0.0.1 is a path gateway + // - localhost is a subdomain gateway + // https://github.com/ipfs-shipyard/ipfs-companion/issues/328#issuecomment-537383212 + if (opts.useLocalhostName && localhostIpUrl(url)) { + url.hostname = 'localhost' + } + if (!opts.useLocalhostName && localhostNameUrl(url)) { url.hostname = '127.0.0.1' } - // Return string without trailing slash - return url.toString().replace(/\/$/, '') + return url +} + +// Return string without trailing slash +function guiURLString (url, opts) { + return safeURL(url, opts).toString().replace(/\/$/, '') } -exports.normalizeGatewayURL = normalizeGatewayURL -exports.safeURL = (url) => new URL(normalizeGatewayURL(url)) +exports.safeURL = safeURL +exports.guiURLString = guiURLString // convert JS array to multiline textarea function hostArrayCleanup (array) { @@ -110,6 +123,19 @@ function hostTextToArray (text) { exports.hostArrayToText = hostArrayToText exports.hostTextToArray = hostTextToArray +function localhostIpUrl (url) { + if (typeof url === 'string') { + url = new URL(url) + } + return url.hostname === '127.0.0.1' || url.hostname === '[::1]' +} +function localhostNameUrl (url) { + if (typeof url === 'string') { + url = new URL(url) + } + return url.hostname.toLowerCase() === 'localhost' +} + exports.migrateOptions = async (storage) => { // <= v2.4.4 // DNSLINK: convert old on/off 'dnslink' flag to text-based 'dnslinkPolicy' @@ -139,4 +165,14 @@ exports.migrateOptions = async (storage) => { await storage.set({ noIntegrationsHostnames: noRedirectHostnames }) await storage.remove('noRedirectHostnames') } + // ~v2.11: subdomain proxy at *.ipfs.localhost + // migrate old default 127.0.0.1 to localhost hostname + const { customGatewayUrl: gwUrl } = await storage.get('customGatewayUrl') + if (gwUrl && (localhostIpUrl(gwUrl) || localhostNameUrl(gwUrl))) { + const { useSubdomainProxy } = await storage.get('useSubdomainProxy') + const newUrl = guiURLString(gwUrl, { useLocalhostName: useSubdomainProxy }) + if (gwUrl !== newUrl) { + await storage.set({ customGatewayUrl: newUrl }) + } + } } diff --git a/add-on/src/lib/state.js b/add-on/src/lib/state.js index 59d703305..32084f69f 100644 --- a/add-on/src/lib/state.js +++ b/add-on/src/lib/state.js @@ -22,15 +22,30 @@ function initState (options, overrides) { delete state.publicSubdomainGatewayUrl state.redirect = options.useCustomGateway delete state.useCustomGateway - state.apiURL = safeURL(options.ipfsApiUrl) + state.apiURL = safeURL(options.ipfsApiUrl, { useLocalhostName: false }) // go-ipfs returns 403 if IP is beautified to 'localhost' state.apiURLString = state.apiURL.toString() delete state.ipfsApiUrl - state.gwURL = safeURL(options.customGatewayUrl) + state.gwURL = safeURL(options.customGatewayUrl, { useLocalhostName: state.useSubdomainProxy }) state.gwURLString = state.gwURL.toString() delete state.customGatewayUrl state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy state.webuiCid = webuiCid - state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/` + + // TODO: unify the way webui is opened + // - https://github.com/ipfs-shipyard/ipfs-companion/pull/737 + // - https://github.com/ipfs-shipyard/ipfs-companion/pull/738 + // Context: previously, we loaded webui from gateway port + // (`${state.gwURLString}ipfs/${state.webuiCid}/`) because API port + // has hardcoded list of whitelisted webui versions. + // To enable API access from webui loaded from Gateway port Companion + // removed Origin header to avoid CORS, now we move away from that + // complexity and for now just load version whitelisted on API port. + // In the future, we want to load webui from $webuiCid.ipfs.localhost + // and whitelist API access from that specific hostname + // by appending it to API.HTTPHeaders.Access-Control-Allow-Origin list + // When that is possible, we can remove Origin manipulation (see PR #737 for PoC) + state.webuiRootUrl = `${state.apiURLString}webui/` + // attach helper functions state.activeIntegrations = (url) => { if (!state.active) return false diff --git a/add-on/src/options/forms/api-form.js b/add-on/src/options/forms/api-form.js index 780ad2862..68c008e90 100644 --- a/add-on/src/options/forms/api-form.js +++ b/add-on/src/options/forms/api-form.js @@ -3,11 +3,11 @@ const browser = require('webextension-polyfill') const html = require('choo/html') -const { normalizeGatewayURL } = require('../../lib/options') +const { guiURLString } = require('../../lib/options') const switchToggle = require('../../pages/components/switch-toggle') function apiForm ({ ipfsApiUrl, ipfsApiPollMs, automaticMode, onOptionChange }) { - const onIpfsApiUrlChange = onOptionChange('ipfsApiUrl', normalizeGatewayURL) + const onIpfsApiUrlChange = onOptionChange('ipfsApiUrl', (url) => guiURLString(url, { useLocalhostName: false })) const onIpfsApiPollMsChange = onOptionChange('ipfsApiPollMs') const onAutomaticModeChange = onOptionChange('automaticMode') diff --git a/add-on/src/options/forms/gateways-form.js b/add-on/src/options/forms/gateways-form.js index 1d6133f94..dbeec8cb1 100644 --- a/add-on/src/options/forms/gateways-form.js +++ b/add-on/src/options/forms/gateways-form.js @@ -4,25 +4,28 @@ const browser = require('webextension-polyfill') const html = require('choo/html') const switchToggle = require('../../pages/components/switch-toggle') -const { normalizeGatewayURL, hostTextToArray, hostArrayToText } = require('../../lib/options') +const { guiURLString, hostTextToArray, hostArrayToText } = require('../../lib/options') // Warn about mixed content issues when changing the gateway +// to something other than HTTP or localhost // https://github.com/ipfs-shipyard/ipfs-companion/issues/648 -const secureContextUrl = /^https:\/\/|^http:\/\/127.0.0.1|^http:\/\/\[::1\]/ +const secureContextUrl = /^https:\/\/|^http:\/\/localhost|^http:\/\/127.0.0.1|^http:\/\/\[::1\]/ function gatewaysForm ({ ipfsNodeType, customGatewayUrl, useCustomGateway, + useSubdomainProxy, noIntegrationsHostnames, publicGatewayUrl, publicSubdomainGatewayUrl, onOptionChange }) { - const onCustomGatewayUrlChange = onOptionChange('customGatewayUrl', normalizeGatewayURL) + const onCustomGatewayUrlChange = onOptionChange('customGatewayUrl', (url) => guiURLString(url, { useLocalhostName: useSubdomainProxy })) const onUseCustomGatewayChange = onOptionChange('useCustomGateway') - const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', normalizeGatewayURL) - const onPublicSubdomainGatewayUrlChange = onOptionChange('publicSubdomainGatewayUrl', normalizeGatewayURL) + const onUseSubdomainProxyChange = onOptionChange('useSubdomainProxy') + const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', guiURLString) + const onPublicSubdomainGatewayUrlChange = onOptionChange('publicSubdomainGatewayUrl', guiURLString) const onNoIntegrationsHostnamesChange = onOptionChange('noIntegrationsHostnames', hostTextToArray) const mixedContentWarning = !secureContextUrl.test(customGatewayUrl) const supportRedirectToCustomGateway = ipfsNodeType !== 'embedded' @@ -108,6 +111,22 @@ function gatewaysForm ({
${switchToggle({ id: 'useCustomGateway', checked: useCustomGateway, onchange: onUseCustomGatewayChange })}
` : null} + ${supportRedirectToCustomGateway ? html` +
+ +
${switchToggle({ id: 'useSubdomainProxy', checked: useSubdomainProxy, onchange: onUseSubdomainProxyChange })}
+
+ ` : null} ${supportRedirectToCustomGateway ? html`
` : null} + ` diff --git a/add-on/src/options/page.js b/add-on/src/options/page.js index c616327f9..e3173fae0 100644 --- a/add-on/src/options/page.js +++ b/add-on/src/options/page.js @@ -67,6 +67,7 @@ module.exports = function optionsPage (state, emit) { ipfsNodeType: state.options.ipfsNodeType, customGatewayUrl: state.options.customGatewayUrl, useCustomGateway: state.options.useCustomGateway, + useSubdomainProxy: state.options.useSubdomainProxy, publicGatewayUrl: state.options.publicGatewayUrl, publicSubdomainGatewayUrl: state.options.publicSubdomainGatewayUrl, noIntegrationsHostnames: state.options.noIntegrationsHostnames, diff --git a/add-on/src/popup/browser-action/context-actions.js b/add-on/src/popup/browser-action/context-actions.js index 3af22fdad..4673d9e7d 100644 --- a/add-on/src/popup/browser-action/context-actions.js +++ b/add-on/src/popup/browser-action/context-actions.js @@ -5,6 +5,7 @@ const browser = require('webextension-polyfill') const html = require('choo/html') const navItem = require('./nav-item') const navHeader = require('./nav-header') +const { sameGateway } = require('../../lib/ipfs-path') const { contextMenuViewOnGateway, contextMenuCopyAddressAtPublicGw, @@ -38,11 +39,16 @@ function contextActions ({ }) { const activeCidResolver = active && isIpfsOnline && isApiAvailable const activePinControls = active && isIpfsOnline && isApiAvailable - const activeViewOnGateway = currentTab && !(currentTab.url.startsWith(pubGwURLString) || currentTab.url.startsWith(gwURLString)) + const activeViewOnGateway = (currentTab) => { + if (!currentTab) return false + const { url } = currentTab + return !(sameGateway(url, gwURLString) || sameGateway(url, pubGwURLString)) + } + const renderIpfsContextItems = () => { if (!isIpfsContext) return return html`
- ${activeViewOnGateway ? navItem({ + ${activeViewOnGateway(currentTab) ? navItem({ text: browser.i18n.getMessage(contextMenuViewOnGateway), onClick: () => onViewOnGateway(contextMenuViewOnGateway) }) : null} @@ -105,4 +111,5 @@ function activeTabActions (state) {
` } + module.exports.activeTabActions = activeTabActions diff --git a/package.json b/package.json index 2de9d4f60..f1df55654 100644 --- a/package.json +++ b/package.json @@ -110,13 +110,13 @@ "sinon-chrome": "3.0.1", "standard": "14.3.1", "tar": "5.0.5", - "terser": "4.4.2", - "terser-webpack-plugin": "2.2.2", + "terser": "4.6.7", + "terser-webpack-plugin": "2.3.5", "transform-loader": "0.2.4", "web-ext": "4.1.0", - "webpack": "4.41.2", - "webpack-bundle-analyzer": "3.6.0", - "webpack-cli": "3.3.10", + "webpack": "4.42.0", + "webpack-bundle-analyzer": "3.6.1", + "webpack-cli": "3.3.11", "webpack-merge": "4.2.2" }, "dependencies": { @@ -140,7 +140,7 @@ "ipfs-postmsg-proxy": "3.1.1", "ipfsx": "0.17.0", "is-fqdn": "1.0.1", - "is-ipfs": "0.6.1", + "is-ipfs": "https://github.com/ipfs/is-ipfs/tarball/f1823cc0b12e97ac1f202ba7c06663b5b9b5d265/is-ipfs.tar.gz", "is-svg": "4.2.0", "it-to-stream": "0.1.1", "lru-cache": "5.1.1", @@ -156,7 +156,7 @@ "pull-file-reader": "1.0.2", "readable-stream": "3.4.0", "tachyons": "4.11.1", - "tar-stream": "2.1.0", + "tar-stream": "2.1.2", "timers-browserify-full": "0.0.1", "uri-to-multiaddr": "3.0.1", "webextension-polyfill": "0.5.0", diff --git a/test/functional/lib/dnslink.test.js b/test/functional/lib/dnslink.test.js index 464fa4258..22c8c2d48 100644 --- a/test/functional/lib/dnslink.test.js +++ b/test/functional/lib/dnslink.test.js @@ -81,8 +81,10 @@ describe('dnslinkResolver (dnslinkPolicy=detectIpfsPathHeader)', function () { const dnslinkResolver = createDnslinkResolver(getExternalNodeState) dnslinkResolver.setDnslink(url.hostname, '/ipfs/bafybeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge') expectNoDnsTxtRecordLookup(url.hostname, dnslinkResolver) + // note: locahost will redirect to subdomain if its go-ipfs >0.5, + // so companion does not need to handle that expect(dnslinkResolver.dnslinkRedirect(url.toString()).redirectUrl) - .to.equal('http://127.0.0.1:8080/ipns/dnslinksite4.io/foo/barl?a=b#c=d') + .to.equal('http://localhost:8080/ipns/dnslinksite4.io/foo/barl?a=b#c=d') }) it('[embedded node] should return redirect to public gateway if dnslink is present in cache', function () { const url = new URL('https://dnslinksite4.io/foo/barl?a=b#c=d') @@ -163,8 +165,10 @@ describe('dnslinkResolver (dnslinkPolicy=enabled)', function () { const url = new URL('https://dnslinksite4.io/foo/barl?a=b#c=d') const dnslinkResolver = createDnslinkResolver(getExternalNodeState) spoofDnsTxtRecord(url.hostname, dnslinkResolver, dnslinkValue) + // note: locahost will redirect to subdomain if its go-ipfs >0.5, + // so companion does not need to handle that expect(dnslinkResolver.dnslinkRedirect(url.toString()).redirectUrl) - .to.equal('http://127.0.0.1:8080/ipns/dnslinksite4.io/foo/barl?a=b#c=d') + .to.equal('http://localhost:8080/ipns/dnslinksite4.io/foo/barl?a=b#c=d') }) it('[embedded node] should return redirect to public gateway if DNS TXT record is present and path does not belong to a gateway', function () { const url = new URL('https://dnslinksite4.io/foo/barl?a=b#c=d') diff --git a/yarn.lock b/yarn.lock index c2453911e..519b2dd94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1540,6 +1540,11 @@ acorn-walk@^6.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== +acorn-walk@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" + integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ== + acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" @@ -1565,7 +1570,7 @@ acorn@^7.0.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.0.0.tgz#26b8d1cd9a9b700350b71c0905546f64d1284e7a" integrity sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ== -acorn@^7.1.0: +acorn@^7.1.0, acorn@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== @@ -1706,6 +1711,16 @@ ajv@^6.1.0, ajv@^6.5.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.0: + version "6.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" + integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ansi-align@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" @@ -2199,6 +2214,13 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" +base-x@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.8.tgz#1e1106c2537f0162e8b52474a557ebb09000018d" + integrity sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA== + dependencies: + safe-buffer "^5.0.1" + base32-encode@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/base32-encode/-/base32-encode-1.1.1.tgz#d022d86aca0002a751bbe1bf20eb4a9b1cef4e95" @@ -2379,6 +2401,15 @@ bl@^4.0.0: dependencies: readable-stream "^3.4.0" +bl@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a" + integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + blakejs@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.1.0.tgz#69df92ef953aa88ca51a32df6ab1c54a155fc7a5" @@ -2719,6 +2750,14 @@ buffer@^5.4.3: base64-js "^1.0.2" ieee754 "^1.1.4" +buffer@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.5.0.tgz#9c3caa3d623c33dd1c7ef584b89b88bf9c9bc1ce" + integrity sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + buffers@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" @@ -5286,6 +5325,11 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + fast-fifo@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.0.0.tgz#9bc72e6860347bb045a876d1c5c0af11e9b984e7" @@ -5473,13 +5517,13 @@ find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-cache-dir@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.1.0.tgz#9935894999debef4cf9f677fdf646d002c4cdecb" - integrity sha512-zw+EFiNBNPgI2NTrKkDd1xd7q0cs6wr/iWnr/oUkI0yF9K9GqQ+riIt4aiyFaaqpaWbxPrJXHI+QvmNUQbX+0Q== +find-cache-dir@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" + integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== dependencies: commondir "^1.0.1" - make-dir "^3.0.0" + make-dir "^3.0.2" pkg-dir "^4.1.0" find-root@^1.0.0: @@ -6692,7 +6736,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7811,16 +7855,15 @@ is-ip@^3.1.0: dependencies: ip-regex "^4.0.0" -is-ipfs@0.6.1, is-ipfs@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/is-ipfs/-/is-ipfs-0.6.1.tgz#c85069c73275dc6a60673c791a9be731e2b4bfc4" - integrity sha512-WhqQylam6pODS2RyqT/u0PR5KWtBZNCgPjgargFOVQjzw/3+6d0midXenzU65klM4LH13IUiCC6ObhDUdXZ7Nw== +"is-ipfs@https://github.com/ipfs/is-ipfs/tarball/f1823cc0b12e97ac1f202ba7c06663b5b9b5d265/is-ipfs.tar.gz": + version "0.6.3" + resolved "https://github.com/ipfs/is-ipfs/tarball/f1823cc0b12e97ac1f202ba7c06663b5b9b5d265/is-ipfs.tar.gz#df65d251b5e137fd16eeb6488ab67ca693c74548" dependencies: bs58 "^4.0.1" cids "~0.7.0" - mafmt "^6.0.7" - multiaddr "^6.0.4" - multibase "~0.6.0" + mafmt "^7.0.0" + multiaddr "^7.2.1" + multibase "~0.7.0" multihashes "~0.4.13" is-ipfs@~0.4.2: @@ -7833,6 +7876,18 @@ is-ipfs@~0.4.2: multibase "~0.6.0" multihashes "~0.4.13" +is-ipfs@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/is-ipfs/-/is-ipfs-0.6.1.tgz#c85069c73275dc6a60673c791a9be731e2b4bfc4" + integrity sha512-WhqQylam6pODS2RyqT/u0PR5KWtBZNCgPjgargFOVQjzw/3+6d0midXenzU65klM4LH13IUiCC6ObhDUdXZ7Nw== + dependencies: + bs58 "^4.0.1" + cids "~0.7.0" + mafmt "^6.0.7" + multiaddr "^6.0.4" + multibase "~0.6.0" + multihashes "~0.4.13" + is-mergeable-object@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-mergeable-object/-/is-mergeable-object-1.1.1.tgz#faaa3ed1cfce87d6f7d2f5885e92cc30af3e2ebf" @@ -8222,13 +8277,13 @@ jed@1.1.1: resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4" integrity sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ= -jest-worker@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" - integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== +jest-worker@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.1.0.tgz#75d038bad6fdf58eba0d2ec1835856c497e3907a" + integrity sha512-ZHhHtlxOWSxCoNOKHGbiLzXnl42ga9CxDr27H36Qn+15pQZd3R/F24jrmjDelw9j/iHUIWMWs08/u2QN50HHOg== dependencies: merge-stream "^2.0.0" - supports-color "^6.1.0" + supports-color "^7.0.0" jetpack-id@1.0.0: version "1.0.0" @@ -9608,6 +9663,13 @@ mafmt@^6.0.10: dependencies: multiaddr "^6.1.0" +mafmt@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/mafmt/-/mafmt-7.1.0.tgz#4126f6d0eded070ace7dbbb6fb04977412d380b5" + integrity sha512-vpeo9S+hepT3k2h5iFxzEHvvR0GPBx9uKaErmnRzYNcaKb03DgOArjEMlgG4a9LcuZZ89a3I8xbeto487n26eA== + dependencies: + multiaddr "^7.3.0" + magic-string@^0.23.2: version "0.23.2" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.23.2.tgz#204d7c3ea36c7d940209fcc54c39b9f243f13369" @@ -9637,6 +9699,13 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" +make-dir@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" + integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w== + dependencies: + semver "^6.0.0" + mamacro@^0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" @@ -10207,7 +10276,7 @@ multiaddr-to-uri@^4.0.1: dependencies: multiaddr "^6.0.3" -multiaddr@6.1.0, multiaddr@^4.0.0, multiaddr@^5.0.0, multiaddr@^6.0.3, multiaddr@^6.0.4, multiaddr@^6.0.6, multiaddr@^6.1.0, multiaddr@^6.1.1: +multiaddr@6.1.0, multiaddr@^4.0.0, multiaddr@^5.0.0, multiaddr@^6.0.3, multiaddr@^6.0.4, multiaddr@^6.0.6, multiaddr@^6.1.0, multiaddr@^6.1.1, multiaddr@^7.2.1, multiaddr@^7.3.0: version "6.1.0" resolved "https://registry.yarnpkg.com/multiaddr/-/multiaddr-6.1.0.tgz#1f93afce58a33db5cc32a5917d8a14105d94330e" integrity sha512-+XTP3OzG2m6JVcjxA9QBmGDr0Vk8WwnohC/fCC3puXb5qJqfJwLVJLEtdTc6vK7ri/hw+Nn4wyT4LkZaPnvGfQ== @@ -10239,6 +10308,14 @@ multibase@~0.6.0: dependencies: base-x "3.0.4" +multibase@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/multibase/-/multibase-0.7.0.tgz#1adfc1c50abe05eefeb5091ac0c2728d6b84581b" + integrity sha512-TW8q03O0f6PNFTQDvh3xxH03c8CjGaaYrjkl9UQPG6rz53TQzzxJVCIWVjzcbN/Q5Y53Zd0IBQBMVktVgNx4Fg== + dependencies: + base-x "^3.0.8" + buffer "^5.5.0" + multicast-dns@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.0.tgz#7aa49a7efba931a346011aa02e7d1c314a65ac77" @@ -11256,6 +11333,13 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" + integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -12983,12 +13067,12 @@ schema-utils@^2.5.0: ajv "^6.10.2" ajv-keywords "^3.4.1" -schema-utils@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.1.tgz#eb78f0b945c7bcfa2082b3565e8db3548011dc4f" - integrity sha512-0WXHDs1VDJyo+Zqs9TKLKyD/h7yDpHUhEFsM2CzkICFdoX1av+GBq/J2xRTFfsQO5kBfhZzANf2VcIm84jqDbg== +schema-utils@^2.6.4: + version "2.6.5" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.5.tgz#c758f0a7e624263073d396e29cd40aa101152d8a" + integrity sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ== dependencies: - ajv "^6.10.2" + ajv "^6.12.0" ajv-keywords "^3.4.1" scroll-to-anchor@^1.0.0: @@ -13085,15 +13169,10 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" -serialize-javascript@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" - integrity sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA== - -serialize-javascript@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.1.tgz#952907a04a3e3a75af7f73d92d15e233862048b2" - integrity sha512-MPLPRpD4FNqWq9tTIjYG5LesFouDhdyH0EPY3gVK4DRD5+g4aDqdNSzLIwceulo3Yj+PL1bPh6laE5+H6LTcrQ== +serialize-javascript@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" + integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== serve-static@1.14.1: version "1.14.1" @@ -14042,7 +14121,7 @@ supports-color@^5.3.0, supports-color@^5.5.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== @@ -14086,12 +14165,12 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tar-stream@2.1.0, tar-stream@^2.0.0, tar-stream@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" - integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== +tar-stream@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325" + integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q== dependencies: - bl "^3.0.0" + bl "^4.0.1" end-of-stream "^1.4.1" fs-constants "^1.0.0" inherits "^2.0.3" @@ -14110,6 +14189,17 @@ tar-stream@^1.5.0, tar-stream@^1.5.2: to-buffer "^1.1.1" xtend "^4.0.0" +tar-stream@^2.0.0, tar-stream@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" + integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== + dependencies: + bl "^3.0.0" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar@5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/tar/-/tar-5.0.5.tgz#03fcdb7105bc8ea3ce6c86642b9c942495b04f93" @@ -14196,39 +14286,40 @@ term-size@^2.1.0: resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw== -terser-webpack-plugin@2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.2.2.tgz#2a6e00237125564a455ad69b22e08ee59420473a" - integrity sha512-/CHMNswPMAwuD2kd++qys8UmBRmsshPSzHw4BlDwurPtK9YjeK93OV89YWkJulHk972cs07K/7Z92V6PNjWF8A== +terser-webpack-plugin@2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.5.tgz#5ad971acce5c517440ba873ea4f09687de2f4a81" + integrity sha512-WlWksUoq+E4+JlJ+h+U+QUzXpcsMSSNXkDy9lBVkSqDn1w23Gg29L/ary9GeJVYCGiNJJX7LnVc4bwL1N3/g1w== dependencies: cacache "^13.0.1" - find-cache-dir "^3.1.0" - jest-worker "^24.9.0" - schema-utils "^2.6.1" - serialize-javascript "^2.1.1" + find-cache-dir "^3.2.0" + jest-worker "^25.1.0" + p-limit "^2.2.2" + schema-utils "^2.6.4" + serialize-javascript "^2.1.2" source-map "^0.6.1" - terser "^4.4.2" + terser "^4.4.3" webpack-sources "^1.4.3" -terser-webpack-plugin@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" - integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== +terser-webpack-plugin@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" + integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== dependencies: cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^1.7.0" + serialize-javascript "^2.1.2" source-map "^0.6.1" terser "^4.1.2" webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser@4.4.2, terser@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.4.2.tgz#448fffad0245f4c8a277ce89788b458bfd7706e8" - integrity sha512-Uufrsvhj9O1ikwgITGsZ5EZS6qPokUOkCegS7fYOdGTv+OA90vndUbU6PEjr5ePqHfNUbGyMO7xyIZv2MhsALQ== +terser@4.6.7, terser@^4.4.3: + version "4.6.7" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.7.tgz#478d7f9394ec1907f0e488c5f6a6a9a2bad55e72" + integrity sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -15026,13 +15117,13 @@ webidl-conversions@^5.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== -webpack-bundle-analyzer@3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.6.0.tgz#39b3a8f829ca044682bc6f9e011c95deb554aefd" - integrity sha512-orUfvVYEfBMDXgEKAKVvab5iQ2wXneIEorGNsyuOyVYpjYrI7CUOhhXNDd3huMwQ3vNNWWlGP+hzflMFYNzi2g== +webpack-bundle-analyzer@3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.6.1.tgz#bdb637c2304424f2fbff9a950c7be42a839ae73b" + integrity sha512-Nfd8HDwfSx1xBwC+P8QMGvHAOITxNBSvu/J/mCJvOwv+G4VWkU7zir9SSenTtyCi0LnVtmsc7G5SZo1uV+bxRw== dependencies: - acorn "^6.0.7" - acorn-walk "^6.1.1" + acorn "^7.1.1" + acorn-walk "^7.1.1" bfj "^6.1.1" chalk "^2.4.1" commander "^2.18.0" @@ -15045,10 +15136,10 @@ webpack-bundle-analyzer@3.6.0: opener "^1.5.1" ws "^6.0.0" -webpack-cli@3.3.10: - version "3.3.10" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.10.tgz#17b279267e9b4fb549023fae170da8e6e766da13" - integrity sha512-u1dgND9+MXaEt74sJR4PR7qkPxXUSQ0RXYq8x1L6Jg1MYVEmGPrH6Ah6C4arD4r0J1P5HKjRqpab36k0eIzPqg== +webpack-cli@3.3.11: + version "3.3.11" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.11.tgz#3bf21889bf597b5d82c38f215135a411edfdc631" + integrity sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g== dependencies: chalk "2.4.2" cross-spawn "6.0.5" @@ -15077,10 +15168,10 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@4.41.2: - version "4.41.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.2.tgz#c34ec76daa3a8468c9b61a50336d8e3303dce74e" - integrity sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A== +webpack@4.42.0: + version "4.42.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.42.0.tgz#b901635dd6179391d90740a63c93f76f39883eb8" + integrity sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w== dependencies: "@webassemblyjs/ast" "1.8.5" "@webassemblyjs/helper-module-context" "1.8.5" @@ -15102,7 +15193,7 @@ webpack@4.41.2: node-libs-browser "^2.2.1" schema-utils "^1.0.0" tapable "^1.1.3" - terser-webpack-plugin "^1.4.1" + terser-webpack-plugin "^1.4.3" watchpack "^1.6.0" webpack-sources "^1.4.1"