Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions public/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@
"placeholder": "Enter a URL (http://user:password@127.0.0.1:5001) or a Multiaddr (/ip4/127.0.0.1/tcp/5001)"
},
"publicGatewayForm": {
"placeholder": "Enter a URL (https://ipfs.io)"
"placeholder": "Enter an HTTP URL (https://path-gw.example.com)"
},
"localGatewayForm": {
"placeholder": "Enter a URL (http://localhost:8080)"
"placeholder": "Enter an HTTP URL (http://localhost:8080)"
},
"publicSubdomainGatewayForm": {
"placeholder": "Enter a URL (https://dweb.link)"
"placeholder": "Enter an HTTP URL (https://subdomain-gw.example.net)"
},
"ipfsCheckForm": {
"label": "Retrieval Check Service URL",
Expand Down Expand Up @@ -90,7 +90,8 @@
"pinStatus": "Pin Status",
"publicKey": "Public key",
"publicGateway": "Public Gateway",
"localGateway": "Local Gateway",
"publicSubdomainGateway": "Public Subdomain Gateway",
"localGateway": "Local HTTP Gateway",
"rateIn": "Rate in",
"rateOut": "Rate out",
"repo": "Repo",
Expand Down
8 changes: 7 additions & 1 deletion public/locales/en/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"individualFilesOnly": "Only available for individual files",
"noPDFSupport": "Your browser does not support PDFs. Please download the PDF to view it:",
"downloadPDF": "Download PDF",
"openWithLocalGateway": "Try opening it instead with your <1>local gateway</1>.",
"openWithPublicGateway": "Try opening it instead with your <1>public gateway</1>.",
"openWithLocalAndPublicGateway": "Try opening it instead with your <1>local gateway</1> or <3>public gateway</3>.",
"cantBePreviewed": "Sorry, this file can’t be previewed",
Expand All @@ -28,7 +29,12 @@
},
"shareModal": {
"title": "Share files",
"description": "Copy the link below and share it with your friends."
"description": "Copy the link below and share it with your friends.",
"descriptionLocal": "Use this link to open in apps running on this machine.",
"descriptionNative": "This native IPFS address opens in apps and browsers that support IPFS, like the <1>IPFS Companion</1> extension.",
"useLocalLink": "Local HTTP link for other apps on this machine",
"useSubdomains": "Use localhost subdomains for web apps",
"linkError": "Could not generate the link. Check that your IPFS node is running."
},
"renameModal": {
"titleFile": "Rename file",
Expand Down
35 changes: 33 additions & 2 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,39 @@
},
"apiDescription": "<0>If your node is configured with a <1>custom Kubo RPC API address</1>, including a port other than the default 5001, enter it here.</0>",
"localGatewayDescription": "<0>If you access the WebUI through a reverse proxy, Docker, or a different host, enter the gateway URL your browser can reach. Leave empty to use the first <1>gateway address</1> from your Kubo config.</0>",
"publicSubdomainGatewayDescription": "<0>Select a default <1>Subdomain Gateway</1> for generating shareable links.</0>",
"publicPathGatewayDescription": "<0>Select a fallback <1>Path Gateway</1> for generating shareable links for CIDs that exceed the 63-character DNS limit.</0>",
"shareLink": {
"title": "Sharing IPFS Links",
"intro": "This controls the link you copy with Share Link or Publish to IPNS. Native addresses are the default. The gateway options below copy a regular HTTP link instead, each for a specific need, so pick one only when you know you want it.",
"pathVsSubdomain": "Gateway links come in two shapes: <0>path</0> and <1>subdomain</1>. A path link puts everything from that gateway on one origin, the boundary your browser uses for cache, cookies, and saved logins, so one page can see what another left behind. A subdomain link gives each item its own origin, so your browser keeps them apart. Use a subdomain link for opening or hosting web pages, and a path link for non-browser apps.",
"recommendedBadge": "Recommended",
"gatewayGroupTitle": "HTTP gateway links",
"gatewayGroupNote": "These open in any HTTP client, but depend on a specific URL being reachable.",
"usesGateway": "Produces links like",
"subdomainNote": "Each item gets its own origin. Learn <0>how browsers keep sites apart</0>.",
"publicGatewayHelp": "New to public gateways? Find one with the <0>Public Gateway Checker</0>, or <1>run your own</1>.",
"publicGatewayEmptyHint": "To use this, enter a gateway address below.",
"native": {
"label": "Native (ipfs:// and ipns:// addresses)",
"description": "These addresses don't depend on any HTTP server, so they keep working even when a public gateway is down. Best for sharing with people peer-to-peer.",
"companionNote": "To open them, you and the people you share with can install the <0>IPFS Companion</0> browser extension. Everyone then loads content directly from each other, instead of depending on someone else's HTTP server."
},
"localPath": {
"label": "Local gateway, path link",
"description": "Opens content straight from your node on this computer. Good for local apps that aren't a browser. Does not work for sharing with other people."
},
"localSubdomain": {
"label": "Local gateway, subdomain link",
"description": "Like the option above, but for web apps on this computer that need origin isolation for security. Does not work for sharing with other people. Needs a gateway on localhost, not a raw IP."
},
"publicPath": {
"label": "Public gateway, path link",
"description": "A link anyone on the internet can open through a public gateway. Without origin isolation, don't use it for sensitive web apps."
},
"publicSubdomain": {
"label": "Public gateway, subdomain link",
"description": "Like the option above, but the safer choice for opening or hosting web pages and apps that require origin isolation."
}
},
"retrievalDiagnosticService": {
"title": "Retrieval Diagnostic Service",
"description": "Configure the URL of the <0>ipfs-check</0> service used for <1>retrieval diagnostics</1>. This service checks if content can be successfully fetched from your node and other nodes hosting a specific CID, helping you troubleshoot sharing issues."
Expand Down
2 changes: 1 addition & 1 deletion public/locales/en/welcome.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"header": "What is IPFS?",
"paragraph1": "<0><0>A hypermedia distribution protocol</0> that incorporates ideas from Kademlia, BitTorrent, Git, and more</0>",
"paragraph2": "<0><0>A peer-to-peer file transfer network</0> with a completely decentralized architecture and no central point of failure, censorship, or control</0>",
"paragraph3": "<0><0>An on-ramp to tomorrow's web</0>traditional browsers can access IPFS files through gateways like <2>https://dweb.link</2> or directly using the <4>IPFS Companion</4> extension</0>",
"paragraph3": "<0><0>An on-ramp to tomorrow's web</0>: traditional browsers can access IPFS content through an HTTP gateway, or directly with the <2>IPFS Companion</2> extension</0>",
"paragraph4": "<0><0>A next-gen CDN</0> — just add a file to your node to make it available to the world with cache-friendly content-hash addressing and BitTorrent-style bandwidth distribution</0>",
"paragraph5": "<0><0>A developer toolset</0> for building <2>completely distributed apps and services</2>, backed by a robust open-source community</0>"
},
Expand Down
85 changes: 21 additions & 64 deletions src/bundles/config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import memoize from 'p-memoize'
import { multiaddrToUri as toUri } from '@multiformats/multiaddr-to-uri'
import { createAsyncResourceBundle, createSelector } from 'redux-bundler'
import { contextBridge } from '../helpers/context-bridge'

const LOCAL_HOSTNAMES = ['127.0.0.1', '[::1]', '0.0.0.0', '[::]']

const bundle = createAsyncResourceBundle({
name: 'config',
staleAfter: 60000,
Expand All @@ -23,36 +20,15 @@ const bundle = createAsyncResourceBundle({

const config = JSON.parse(conf)

// An explicit Local Gateway URL is the gateway the browser is meant to reach
// for everything (reverse proxy, Docker, non-default host or port), so trust
// it for the reachability-probed availableGateway too. Without this, the
// 127.0.0.1 probe below fails for remote users and previews, thumbnails and
// IPNS links fall back to the public gateway instead of the user's own node.
// availableGateway drives previews, thumbnails, IPNS links and the Explore
// link: the user's Local Gateway URL override, or the gateway from the Kubo
// config, chosen without any reachability probing. selectAvailableGatewayUrl
// only falls back to a public gateway (through selectGatewayUrl) when no
// local gateway is configured at all.
// https://github.com/ipfs/ipfs-webui/issues/2458
const localGateway = store.selectLocalGateway()
if (localGateway) {
store.doSetAvailableGateway(localGateway)
return conf
}

const publicGateway = store.selectPublicGateway()
const url = getURLFromAddress('Gateway', config) || publicGateway

// Normalize local hostnames to localhost
// to leverage subdomain gateway, if present
// https://github.com/ipfs-shipyard/ipfs-webui/issues/1490
const gw = new URL(url)
if (LOCAL_HOSTNAMES.includes(gw.hostname)) {
gw.hostname = 'localhost'
const localUrl = gw.toString().replace(/\/+$/, '') // no trailing slashes
if (await checkIfSubdomainGatewayUrlIsAccessible(localUrl)) {
store.doSetAvailableGateway(localUrl)
return conf
}
}

if (!await checkIfGatewayUrlIsAccessible(url)) {
store.doSetAvailableGateway(publicGateway)
const gateway = store.selectLocalGateway() || getURLFromAddress('Gateway', config)
if (gateway) {
store.doSetAvailableGateway(gateway)
}

// stringy json for quick compares
Expand All @@ -75,18 +51,26 @@ bundle.reactIsSameOriginToBridge = createSelector(
}
)

bundle.selectGatewayUrl = createSelector(
bundle.selectLocalGatewayUrl = createSelector(
'selectConfigObject',
'selectPublicGateway',
'selectLocalGateway',
(config, publicGateway, localGateway) => {
// Priority: 1) User-configured local gateway, 2) Kubo config, 3) Public gateway
const url = localGateway || getURLFromAddress('Gateway', config) || publicGateway
(config, localGateway) => {
// Priority: 1) User-configured local gateway, 2) Kubo config. Never the
// public gateway: consumers use this where "local" is a promise (share
// links and content links labeled as same-machine).
const url = localGateway || getURLFromAddress('Gateway', config) || ''
// Normalize: remove trailing slashes to avoid double slashes when constructing paths
return url.replace(/\/+$/, '')
}
)

bundle.selectGatewayUrl = createSelector(
'selectLocalGatewayUrl',
'selectPublicGateway',
(localGatewayUrl, publicGateway) =>
localGatewayUrl || publicGateway.replace(/\/+$/, '')
)

bundle.selectAvailableGatewayUrl = createSelector(
'selectAvailableGateway',
'selectGatewayUrl',
Expand Down Expand Up @@ -127,31 +111,4 @@ function getURLFromAddress (name, config) {
}
}

const checkIfGatewayUrlIsAccessible = memoize(async (url) => {
try {
const { status } = await fetch(
`${url}/ipfs/bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss`
)
return status === 200
} catch (e) {
console.error(`Unable to use the gateway at ${url}. The public gateway will be used as a fallback`, e)
return false
}
})

// Separate test is necessary to see if subdomain mode is possible,
// because some browser+OS combinations won't resolve them:
// https://github.com/ipfs/kubo/issues/7527
const checkIfSubdomainGatewayUrlIsAccessible = memoize(async (url) => {
try {
url = new URL(url)
url.hostname = `bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss.ipfs.${url.hostname}`
const { status } = await fetch(url.toString())
return status === 200
} catch (e) {
console.error(`Unable to use the subdomain gateway at ${url}. Regular gateway will be used as a fallback`, e)
return false
}
})

export default bundle
32 changes: 28 additions & 4 deletions src/bundles/files/actions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable require-yield */

import { join, dirname, basename } from 'path'
import { getDownloadLink, getShareableLink, getCarLink } from '../../lib/files.js'
import { getDownloadLink, getCarLink, resolveShareCid } from '../../lib/files.js'
import { buildShareLink, getFilenameQuery, getLocalLinks } from '../../lib/share-link.js'
import countDirs from '../../lib/count-dirs.js'
import memoize from 'p-memoize'
import all from 'it-all'
Expand Down Expand Up @@ -153,8 +154,10 @@ const getPinCIDs = (ipfs) => map(getRawPins(ipfs), (pin) => pin.cid)
* @typedef {Object} ConfigSelectors
* @property {function():string} selectApiUrl
* @property {function():string} selectGatewayUrl
* @property {function():string} selectLocalGatewayUrl
* @property {function():string} selectPublicGateway
* @property {function():string} selectPublicSubdomainGateway
* @property {function():string} selectEffectiveShareLinkType
*
* @typedef {Object} UnkonwActions
* @property {function(string):Promise<unknown>} doUpdateHash
Expand Down Expand Up @@ -598,14 +601,35 @@ const actions = () => ({

doFilesShareLink: (/** @type {FileStat[]} */ files) => perform(ACTIONS.SHARE_LINK, async (ipfs, { store }) => {
// ensureMFS deliberately omitted here, see https://github.com/ipfs/ipfs-webui/issues/1744 for context.
const cid = await resolveShareCid(files, ipfs)
const filename = getFilenameQuery(files)
// Local-only gateway (user override or Kubo config): a link labeled local
// must never point at the public gateway.
const localGatewayUrl = store.selectLocalGatewayUrl()
const publicGateway = store.selectPublicGateway()
const publicSubdomainGateway = store.selectPublicSubdomainGateway()
const { link: shareableLink, cid } = await getShareableLink(files, publicGateway, publicSubdomainGateway, ipfs)

// Trigger background provide operation with the CID from getShareableLink
// The effective type already falls back to native when the chosen type's
// gateway is not configured, so buildShareLink always returns a link here.
const type = store.selectEffectiveShareLinkType()
const link = buildShareLink({
type,
namespace: 'ipfs',
pathId: cid.toString(),
subdomainLabel: cid.toV1().toString(),
filename,
localGatewayUrl,
publicGateway,
publicSubdomainGateway
})

// Local variants for the in-modal override, offered when the chosen type is
// native or public so users can still grab a same-machine link.
const { localLink, subdomainLocalLink } = getLocalLinks(cid, filename, localGatewayUrl)

dispatchAsyncProvide(cid, ipfs)

return shareableLink
return { link, type, localLink, subdomainLocalLink }
}),

/**
Expand Down
9 changes: 8 additions & 1 deletion src/bundles/files/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ export type FileDownload = {
filename: string
}
export type DownloadLink = Perform<'FILES_DOWNLOADLINK', Error, FileDownload, void>
export type ShareLinks = {
link: string
type: string
localLink: string
subdomainLocalLink: string
}
export type ShareLink = Perform<'FILES_SHARE_LINK', Error, ShareLinks, void>

export type Message =
| { type: 'FILES_CLEAR_ALL' }
Expand All @@ -91,7 +98,7 @@ export type Message =
| AddByPath
| BulkCidImport
| DownloadLink
| Perform<'FILES_SHARE_LINK', Error, string, void>
| ShareLink
| Perform<'FILES_COPY', Error, void, void>
| Perform<'FILES_PIN_ADD', Error, Pin[], void>
| Perform<'FILES_PIN_REMOVE', Error, Pin[], void>
Expand Down
Loading
Loading