Skip to content

Commit bbd8a31

Browse files
authored
feat: choose share link type, add native uris (#2482)
* feat: add local gateway URL options to share modal allow copying local gateway links for use in external apps, with optional localhost subdomain mode for web apps - actions.js: doFilesShareLink returns local and subdomain links - ShareModal: checkbox to toggle local link, nested checkbox for localhost subdomains, QR hidden in local mode - Modals: passes new link variants to ShareModal - en/files.json: added translation keys for new UI elements * feat: honor gateway override in local links The share modal's local links flow through selectGatewayUrl, so the Local Gateway URL override added in #2486 reaches them. Subdomain links are offered only for domain gateways, since subdomain gateways cannot resolve on a bare IP such as 127.0.0.1. - files.js: add getLocalLinks, deriving both links from the gateway url (host, port, scheme); gate the subdomain link to domain hosts and to CIDs that fit a 63-char DNS label; share filename and base32 logic with getShareableLink - actions.js: build the local links via getLocalLinks - share-modal: clear the subdomain choice when the local link is unchecked, so it cannot silently reapply - protocol.ts: type the share-link result as ShareLinks, not string * fix: loopback share links and error handling Refine the local gateway share links so they resolve from other apps and never leave the modal hanging when generation fails. - files.js: a single loopback check in getLocalLinks drives both links, so the path link uses 127.0.0.1 (no DNS) and the subdomain link uses localhost (origins need a hostname); domain gateways and non-loopback IPs are left as-is - modals: catch a failed link generation and show an error instead of a stuck "Generating..." - en/files.json: add the error string; mark the plain link as HTTP * feat: native share links by default Share Link and Publish to IPNS copy native ipfs:// and ipns:// URIs by default, with opt-in HTTP gateway links for cases that need them. - add a Settings "Sharing IPFS Links" section to choose the link type: native, local path/subdomain, or public path/subdomain - public gateways default to empty and are opt-in; a public option stays disabled until its URL is set, and clearing it reverts to native - native URIs use canonical CIDv1: base32 for /ipfs, base36 libp2p-key for /ipns; the same choice drives Publish to IPNS - local links normalize loopback per type: 127.0.0.1 for path, localhost for subdomain - remove online gateway reachability probes; previews, thumbnails, IPLD explorer and IPNS links load from the local gateway (override or Kubo config), never a third-party gateway unless configured - consolidate the localhost to 127.0.0.1 subresource fix across previews, thumbnails, downloads, CAR links and pinning icons - Explore links honor the configured gateways; a local gateway change refreshes previews at once and reloads the explorer on the next visit * fix: local links must not use public gateway Share modal and content links labeled as local could point at the public path gateway when the Kubo config exposed no usable gateway address. Local links draw from the local gateway only (user override or Kubo config), with related fixes and cleanups: - loopback host rewrites apply to http only, so an https://localhost gateway keeps its certificate-valid hostname - a link type without its gateway resolves to native before building, removing per-caller fallbacks in share and publish flows - open-in-new-tab links (file preview, IPNS keys) honor the local subdomain choice from Settings for per-CID origin isolation - pinning service icons resolve against the current gateway at read time and are omitted when no gateway is configured; previews and thumbnails skip gateway embeds in the same case - shared toLoopbackIpUrl and GatewayForm replace per-site localhost rewrites and three near-identical gateway forms - re-saving an unchanged Local Gateway URL no longer schedules an Explore page reload * fix: keep existing public gateway defaults keep the defaults users already have (ipfs.io path gateway, dweb.link subdomain gateway, share links on dweb.link) so this PR can merge without changing any default behavior. defaulting to native ipfs:// sharing is left for a follow-up: DEFAULT_SHARE_LINK_TYPE and the DEFAULT_*_GATEWAY constants are the only knobs to flip. - a gateway cleared in Settings stays cleared across reloads; only a never-written setting gets the default - selecting a public link type persists the gateway it points at, so an explicit opt-in keeps its gateway if the defaults change later
1 parent 5cdb301 commit bbd8a31

37 files changed

Lines changed: 1699 additions & 704 deletions

public/locales/en/app.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@
4646
"placeholder": "Enter a URL (http://user:password@127.0.0.1:5001) or a Multiaddr (/ip4/127.0.0.1/tcp/5001)"
4747
},
4848
"publicGatewayForm": {
49-
"placeholder": "Enter a URL (https://ipfs.io)"
49+
"placeholder": "Enter an HTTP URL (https://path-gw.example.com)"
5050
},
5151
"localGatewayForm": {
52-
"placeholder": "Enter a URL (http://localhost:8080)"
52+
"placeholder": "Enter an HTTP URL (http://localhost:8080)"
5353
},
5454
"publicSubdomainGatewayForm": {
55-
"placeholder": "Enter a URL (https://dweb.link)"
55+
"placeholder": "Enter an HTTP URL (https://subdomain-gw.example.net)"
5656
},
5757
"ipfsCheckForm": {
5858
"label": "Retrieval Check Service URL",
@@ -90,7 +90,8 @@
9090
"pinStatus": "Pin Status",
9191
"publicKey": "Public key",
9292
"publicGateway": "Public Gateway",
93-
"localGateway": "Local Gateway",
93+
"publicSubdomainGateway": "Public Subdomain Gateway",
94+
"localGateway": "Local HTTP Gateway",
9495
"rateIn": "Rate in",
9596
"rateOut": "Rate out",
9697
"repo": "Repo",

public/locales/en/files.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"individualFilesOnly": "Only available for individual files",
88
"noPDFSupport": "Your browser does not support PDFs. Please download the PDF to view it:",
99
"downloadPDF": "Download PDF",
10+
"openWithLocalGateway": "Try opening it instead with your <1>local gateway</1>.",
1011
"openWithPublicGateway": "Try opening it instead with your <1>public gateway</1>.",
1112
"openWithLocalAndPublicGateway": "Try opening it instead with your <1>local gateway</1> or <3>public gateway</3>.",
1213
"cantBePreviewed": "Sorry, this file can’t be previewed",
@@ -28,7 +29,12 @@
2829
},
2930
"shareModal": {
3031
"title": "Share files",
31-
"description": "Copy the link below and share it with your friends."
32+
"description": "Copy the link below and share it with your friends.",
33+
"descriptionLocal": "Use this link to open in apps running on this machine.",
34+
"descriptionNative": "This native IPFS address opens in apps and browsers that support IPFS, like the <1>IPFS Companion</1> extension.",
35+
"useLocalLink": "Local HTTP link for other apps on this machine",
36+
"useSubdomains": "Use localhost subdomains for web apps",
37+
"linkError": "Could not generate the link. Check that your IPFS node is running."
3238
},
3339
"renameModal": {
3440
"titleFile": "Rename file",

public/locales/en/settings.json

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,39 @@
2424
},
2525
"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>",
2626
"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>",
27-
"publicSubdomainGatewayDescription": "<0>Select a default <1>Subdomain Gateway</1> for generating shareable links.</0>",
28-
"publicPathGatewayDescription": "<0>Select a fallback <1>Path Gateway</1> for generating shareable links for CIDs that exceed the 63-character DNS limit.</0>",
27+
"shareLink": {
28+
"title": "Sharing IPFS Links",
29+
"intro": "This controls the link you copy with Share Link or Publish to IPNS. Each option fits a specific need, so pick the one that matches how the link will be opened.",
30+
"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.",
31+
"recommendedBadge": "Recommended",
32+
"gatewayGroupTitle": "HTTP gateway links",
33+
"gatewayGroupNote": "These open in any HTTP client, but depend on a specific URL being reachable.",
34+
"usesGateway": "Produces links like",
35+
"subdomainNote": "Each item gets its own origin. Learn <0>how browsers keep sites apart</0>.",
36+
"publicGatewayHelp": "New to public gateways? Find one with the <0>Public Gateway Checker</0>, or <1>run your own</1>.",
37+
"publicGatewayEmptyHint": "To use this, enter a gateway address below.",
38+
"native": {
39+
"label": "Native (ipfs:// and ipns:// addresses)",
40+
"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.",
41+
"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."
42+
},
43+
"localPath": {
44+
"label": "Local gateway, path link",
45+
"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."
46+
},
47+
"localSubdomain": {
48+
"label": "Local gateway, subdomain link",
49+
"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."
50+
},
51+
"publicPath": {
52+
"label": "Public gateway, path link",
53+
"description": "A link anyone on the internet can open through a public gateway. Without origin isolation, don't use it for sensitive web apps."
54+
},
55+
"publicSubdomain": {
56+
"label": "Public gateway, subdomain link",
57+
"description": "Like the option above, but the safer choice for opening or hosting web pages and apps that require origin isolation."
58+
}
59+
},
2960
"retrievalDiagnosticService": {
3061
"title": "Retrieval Diagnostic Service",
3162
"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."

public/locales/en/welcome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"header": "What is IPFS?",
1515
"paragraph1": "<0><0>A hypermedia distribution protocol</0> that incorporates ideas from Kademlia, BitTorrent, Git, and more</0>",
1616
"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>",
17-
"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>",
17+
"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>",
1818
"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>",
1919
"paragraph5": "<0><0>A developer toolset</0> for building <2>completely distributed apps and services</2>, backed by a robust open-source community</0>"
2020
},

src/bundles/config.js

Lines changed: 21 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import memoize from 'p-memoize'
21
import { multiaddrToUri as toUri } from '@multiformats/multiaddr-to-uri'
32
import { createAsyncResourceBundle, createSelector } from 'redux-bundler'
43
import { contextBridge } from '../helpers/context-bridge'
54

6-
const LOCAL_HOSTNAMES = ['127.0.0.1', '[::1]', '0.0.0.0', '[::]']
7-
85
const bundle = createAsyncResourceBundle({
96
name: 'config',
107
staleAfter: 60000,
@@ -23,36 +20,15 @@ const bundle = createAsyncResourceBundle({
2320

2421
const config = JSON.parse(conf)
2522

26-
// An explicit Local Gateway URL is the gateway the browser is meant to reach
27-
// for everything (reverse proxy, Docker, non-default host or port), so trust
28-
// it for the reachability-probed availableGateway too. Without this, the
29-
// 127.0.0.1 probe below fails for remote users and previews, thumbnails and
30-
// IPNS links fall back to the public gateway instead of the user's own node.
23+
// availableGateway drives previews, thumbnails, IPNS links and the Explore
24+
// link: the user's Local Gateway URL override, or the gateway from the Kubo
25+
// config, chosen without any reachability probing. selectAvailableGatewayUrl
26+
// only falls back to a public gateway (through selectGatewayUrl) when no
27+
// local gateway is configured at all.
3128
// https://github.com/ipfs/ipfs-webui/issues/2458
32-
const localGateway = store.selectLocalGateway()
33-
if (localGateway) {
34-
store.doSetAvailableGateway(localGateway)
35-
return conf
36-
}
37-
38-
const publicGateway = store.selectPublicGateway()
39-
const url = getURLFromAddress('Gateway', config) || publicGateway
40-
41-
// Normalize local hostnames to localhost
42-
// to leverage subdomain gateway, if present
43-
// https://github.com/ipfs-shipyard/ipfs-webui/issues/1490
44-
const gw = new URL(url)
45-
if (LOCAL_HOSTNAMES.includes(gw.hostname)) {
46-
gw.hostname = 'localhost'
47-
const localUrl = gw.toString().replace(/\/+$/, '') // no trailing slashes
48-
if (await checkIfSubdomainGatewayUrlIsAccessible(localUrl)) {
49-
store.doSetAvailableGateway(localUrl)
50-
return conf
51-
}
52-
}
53-
54-
if (!await checkIfGatewayUrlIsAccessible(url)) {
55-
store.doSetAvailableGateway(publicGateway)
29+
const gateway = store.selectLocalGateway() || getURLFromAddress('Gateway', config)
30+
if (gateway) {
31+
store.doSetAvailableGateway(gateway)
5632
}
5733

5834
// stringy json for quick compares
@@ -75,18 +51,26 @@ bundle.reactIsSameOriginToBridge = createSelector(
7551
}
7652
)
7753

78-
bundle.selectGatewayUrl = createSelector(
54+
bundle.selectLocalGatewayUrl = createSelector(
7955
'selectConfigObject',
80-
'selectPublicGateway',
8156
'selectLocalGateway',
82-
(config, publicGateway, localGateway) => {
83-
// Priority: 1) User-configured local gateway, 2) Kubo config, 3) Public gateway
84-
const url = localGateway || getURLFromAddress('Gateway', config) || publicGateway
57+
(config, localGateway) => {
58+
// Priority: 1) User-configured local gateway, 2) Kubo config. Never the
59+
// public gateway: consumers use this where "local" is a promise (share
60+
// links and content links labeled as same-machine).
61+
const url = localGateway || getURLFromAddress('Gateway', config) || ''
8562
// Normalize: remove trailing slashes to avoid double slashes when constructing paths
8663
return url.replace(/\/+$/, '')
8764
}
8865
)
8966

67+
bundle.selectGatewayUrl = createSelector(
68+
'selectLocalGatewayUrl',
69+
'selectPublicGateway',
70+
(localGatewayUrl, publicGateway) =>
71+
localGatewayUrl || publicGateway.replace(/\/+$/, '')
72+
)
73+
9074
bundle.selectAvailableGatewayUrl = createSelector(
9175
'selectAvailableGateway',
9276
'selectGatewayUrl',
@@ -127,31 +111,4 @@ function getURLFromAddress (name, config) {
127111
}
128112
}
129113

130-
const checkIfGatewayUrlIsAccessible = memoize(async (url) => {
131-
try {
132-
const { status } = await fetch(
133-
`${url}/ipfs/bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss`
134-
)
135-
return status === 200
136-
} catch (e) {
137-
console.error(`Unable to use the gateway at ${url}. The public gateway will be used as a fallback`, e)
138-
return false
139-
}
140-
})
141-
142-
// Separate test is necessary to see if subdomain mode is possible,
143-
// because some browser+OS combinations won't resolve them:
144-
// https://github.com/ipfs/kubo/issues/7527
145-
const checkIfSubdomainGatewayUrlIsAccessible = memoize(async (url) => {
146-
try {
147-
url = new URL(url)
148-
url.hostname = `bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss.ipfs.${url.hostname}`
149-
const { status } = await fetch(url.toString())
150-
return status === 200
151-
} catch (e) {
152-
console.error(`Unable to use the subdomain gateway at ${url}. Regular gateway will be used as a fallback`, e)
153-
return false
154-
}
155-
})
156-
157114
export default bundle

src/bundles/files/actions.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/* eslint-disable require-yield */
22

33
import { join, dirname, basename } from 'path'
4-
import { getDownloadLink, getShareableLink, getCarLink } from '../../lib/files.js'
4+
import { getDownloadLink, getCarLink, resolveShareCid } from '../../lib/files.js'
5+
import { buildShareLink, getFilenameQuery, getLocalLinks } from '../../lib/share-link.js'
56
import countDirs from '../../lib/count-dirs.js'
67
import memoize from 'p-memoize'
78
import all from 'it-all'
@@ -153,8 +154,10 @@ const getPinCIDs = (ipfs) => map(getRawPins(ipfs), (pin) => pin.cid)
153154
* @typedef {Object} ConfigSelectors
154155
* @property {function():string} selectApiUrl
155156
* @property {function():string} selectGatewayUrl
157+
* @property {function():string} selectLocalGatewayUrl
156158
* @property {function():string} selectPublicGateway
157159
* @property {function():string} selectPublicSubdomainGateway
160+
* @property {function():string} selectEffectiveShareLinkType
158161
*
159162
* @typedef {Object} UnkonwActions
160163
* @property {function(string):Promise<unknown>} doUpdateHash
@@ -598,14 +601,35 @@ const actions = () => ({
598601

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

605-
// Trigger background provide operation with the CID from getShareableLink
612+
// The effective type already falls back to native when the chosen type's
613+
// gateway is not configured, so buildShareLink always returns a link here.
614+
const type = store.selectEffectiveShareLinkType()
615+
const link = buildShareLink({
616+
type,
617+
namespace: 'ipfs',
618+
pathId: cid.toString(),
619+
subdomainLabel: cid.toV1().toString(),
620+
filename,
621+
localGatewayUrl,
622+
publicGateway,
623+
publicSubdomainGateway
624+
})
625+
626+
// Local variants for the in-modal override, offered when the chosen type is
627+
// native or public so users can still grab a same-machine link.
628+
const { localLink, subdomainLocalLink } = getLocalLinks(cid, filename, localGatewayUrl)
629+
606630
dispatchAsyncProvide(cid, ipfs)
607631

608-
return shareableLink
632+
return { link, type, localLink, subdomainLocalLink }
609633
}),
610634

611635
/**

src/bundles/files/protocol.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ export type FileDownload = {
7878
filename: string
7979
}
8080
export type DownloadLink = Perform<'FILES_DOWNLOADLINK', Error, FileDownload, void>
81+
export type ShareLinks = {
82+
link: string
83+
type: string
84+
localLink: string
85+
subdomainLocalLink: string
86+
}
87+
export type ShareLink = Perform<'FILES_SHARE_LINK', Error, ShareLinks, void>
8188

8289
export type Message =
8390
| { type: 'FILES_CLEAR_ALL' }
@@ -91,7 +98,7 @@ export type Message =
9198
| AddByPath
9299
| BulkCidImport
93100
| DownloadLink
94-
| Perform<'FILES_SHARE_LINK', Error, string, void>
101+
| ShareLink
95102
| Perform<'FILES_COPY', Error, void, void>
96103
| Perform<'FILES_PIN_ADD', Error, Pin[], void>
97104
| Perform<'FILES_PIN_REMOVE', Error, Pin[], void>

0 commit comments

Comments
 (0)