mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 10:27:46 +08:00
fix: add InlineDNSLink flag to PublicGateways config (#9328)
https://github.com/ipfs/kubo/issues/9243 Co-authored-by: Marcin Rataj <lidel@lidel.org>
This commit is contained in:
parent
052d797ca7
commit
4291d6b236
@ -1,5 +1,7 @@
|
||||
package config
|
||||
|
||||
const DefaultInlineDNSLink = false
|
||||
|
||||
type GatewaySpec struct {
|
||||
// Paths is explicit list of path prefixes that should be handled by
|
||||
// this gateway. Example: `["/ipfs", "/ipns", "/api"]`
|
||||
@ -18,6 +20,11 @@ type GatewaySpec struct {
|
||||
// NoDNSLink configures this gateway to _not_ resolve DNSLink for the FQDN
|
||||
// provided in `Host` HTTP header.
|
||||
NoDNSLink bool
|
||||
|
||||
// InlineDNSLink configures this gateway to always inline DNSLink names
|
||||
// (FQDN) into a single DNS label in order to interop with wildcard TLS certs
|
||||
// and Origin per CID isolation provided by rules like https://publicsuffix.org
|
||||
InlineDNSLink Flag
|
||||
}
|
||||
|
||||
// Gateway contains options for the HTTP gateway server.
|
||||
|
||||
@ -84,7 +84,8 @@ func HostnameOption() ServeOption {
|
||||
if gw.UseSubdomains {
|
||||
// Yes, redirect if applicable
|
||||
// Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link
|
||||
newURL, err := toSubdomainURL(host, r.URL.Path, r, coreAPI)
|
||||
useInlinedDNSLink := gw.InlineDNSLink.WithDefault(config.DefaultInlineDNSLink)
|
||||
newURL, err := toSubdomainURL(host, r.URL.Path, r, useInlinedDNSLink, coreAPI)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@ -132,6 +133,9 @@ func HostnameOption() ServeOption {
|
||||
// Assemble original path prefix.
|
||||
pathPrefix := "/" + ns + "/" + rootID
|
||||
|
||||
// Retrieve whether or not we should inline DNSLink.
|
||||
useInlinedDNSLink := gw.InlineDNSLink.WithDefault(config.DefaultInlineDNSLink)
|
||||
|
||||
// Does this gateway _handle_ subdomains AND this path?
|
||||
if !(gw.UseSubdomains && hasPrefix(pathPrefix, gw.Paths...)) {
|
||||
// If not, resource does not exist, return 404
|
||||
@ -149,7 +153,7 @@ func HostnameOption() ServeOption {
|
||||
}
|
||||
if !strings.HasPrefix(r.Host, dnsCID) {
|
||||
dnsPrefix := "/" + ns + "/" + dnsCID
|
||||
newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, coreAPI)
|
||||
newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, useInlinedDNSLink, coreAPI)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@ -165,7 +169,7 @@ func HostnameOption() ServeOption {
|
||||
// Do we need to fix multicodec in PeerID represented as CIDv1?
|
||||
if isPeerIDNamespace(ns) {
|
||||
if rootCID.Type() != cid.Libp2pKey {
|
||||
newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, coreAPI)
|
||||
newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, useInlinedDNSLink, coreAPI)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@ -451,7 +455,7 @@ func toDNSLinkFQDN(dnsLabel string) (fqdn string) {
|
||||
}
|
||||
|
||||
// Converts a hostname/path to a subdomain-based URL, if applicable.
|
||||
func toSubdomainURL(hostname, path string, r *http.Request, ipfs iface.CoreAPI) (redirURL string, err error) {
|
||||
func toSubdomainURL(hostname, path string, r *http.Request, inlineDNSLink bool, ipfs iface.CoreAPI) (redirURL string, err error) {
|
||||
var scheme, ns, rootID, rest string
|
||||
|
||||
query := r.URL.RawQuery
|
||||
@ -554,7 +558,7 @@ func toSubdomainURL(hostname, path string, r *http.Request, ipfs iface.CoreAPI)
|
||||
// can be loaded from a subdomain gateway with a wildcard TLS cert if
|
||||
// represented as a single DNS label:
|
||||
// https://my-v--long-example-com.ipns.dweb.link
|
||||
if isHTTPS && ns == "ipns" && strings.Contains(rootID, ".") {
|
||||
if (inlineDNSLink || isHTTPS) && ns == "ipns" && strings.Contains(rootID, ".") {
|
||||
if isDNSLinkName(r.Context(), ipfs, rootID) {
|
||||
// my.v-long.example.com → my-v--long-example-com
|
||||
dnsLabel, err := toDNSLinkDNSLabel(rootID)
|
||||
|
||||
@ -36,35 +36,39 @@ func TestToSubdomainURL(t *testing.T) {
|
||||
|
||||
for _, test := range []struct {
|
||||
// in:
|
||||
request *http.Request
|
||||
gwHostname string
|
||||
path string
|
||||
request *http.Request
|
||||
gwHostname string
|
||||
inlineDNSLink bool
|
||||
path string
|
||||
// out:
|
||||
url string
|
||||
err error
|
||||
}{
|
||||
// DNSLink
|
||||
{httpRequest, "localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
|
||||
{httpRequest, "localhost", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
|
||||
// Hostname with port
|
||||
{httpRequest, "localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
|
||||
{httpRequest, "localhost:8080", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
|
||||
// CIDv0 → CIDv1base32
|
||||
{httpRequest, "localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
|
||||
{httpRequest, "localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
|
||||
// CIDv1 with long sha512
|
||||
{httpRequest, "localhost", "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
|
||||
{httpRequest, "localhost", false, "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
|
||||
// PeerID as CIDv1 needs to have libp2p-key multicodec
|
||||
{httpRequest, "localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
|
||||
{httpRequest, "localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
|
||||
{httpRequest, "localhost", false, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
|
||||
{httpRequest, "localhost", false, "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
|
||||
// PeerID: ed25519+identity multihash → CIDv1Base36
|
||||
{httpRequest, "localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
|
||||
{httpRequest, "sub.localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
|
||||
{httpRequest, "localhost", false, "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
|
||||
{httpRequest, "sub.localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
|
||||
// HTTPS requires DNSLink name to fit in a single DNS label – see "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
|
||||
{httpRequest, "dweb.link", "/ipns/dnslink.long-name.example.com", "http://dnslink.long-name.example.com.ipns.dweb.link/", nil},
|
||||
{httpsRequest, "dweb.link", "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
|
||||
{httpsProxiedRequest, "dweb.link", "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
|
||||
{httpRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "http://dnslink.long-name.example.com.ipns.dweb.link/", nil},
|
||||
{httpsRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
|
||||
{httpsProxiedRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
|
||||
// HTTP requests can also be converted to fit into a single DNS label - https://github.com/ipfs/kubo/issues/9243
|
||||
{httpRequest, "localhost", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.localhost/", nil},
|
||||
{httpRequest, "dweb.link", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.dweb.link/", nil},
|
||||
} {
|
||||
url, err := toSubdomainURL(test.gwHostname, test.path, test.request, coreAPI)
|
||||
url, err := toSubdomainURL(test.gwHostname, test.path, test.request, test.inlineDNSLink, coreAPI)
|
||||
if url != test.url || !equalError(err, test.err) {
|
||||
t.Errorf("(%s, %s) returned (%s, %v), expected (%s, %v)", test.gwHostname, test.path, url, err, test.url, test.err)
|
||||
t.Errorf("(%s, %v, %s) returned (%s, %v), expected (%s, %v)", test.gwHostname, test.inlineDNSLink, test.path, url, err, test.url, test.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +59,7 @@ config file at runtime.
|
||||
- [`Gateway.PublicGateways: Paths`](#gatewaypublicgateways-paths)
|
||||
- [`Gateway.PublicGateways: UseSubdomains`](#gatewaypublicgateways-usesubdomains)
|
||||
- [`Gateway.PublicGateways: NoDNSLink`](#gatewaypublicgateways-nodnslink)
|
||||
- [`Gateway.PublicGateways: InlineDNSLink`](#gatewaypublicgateways-inlinednslink)
|
||||
- [Implicit defaults of `Gateway.PublicGateways`](#implicit-defaults-of-gatewaypublicgateways)
|
||||
- [`Gateway` recipes](#gateway-recipes)
|
||||
- [`Identity`](#identity)
|
||||
@ -149,7 +150,7 @@ config file at runtime.
|
||||
- [`Swarm.Transports.Network.QUIC`](#swarmtransportsnetworkquic)
|
||||
- [`Swarm.Transports.Network.Relay`](#swarmtransportsnetworkrelay)
|
||||
- [`Swarm.Transports.Network.WebTransport`](#swarmtransportsnetworkwebtransport)
|
||||
- [`How to enable WebTransport`](#how-to-enable-webtransport)
|
||||
- [How to enable WebTransport](#how-to-enable-webtransport)
|
||||
- [`Swarm.Transports.Security`](#swarmtransportssecurity)
|
||||
- [`Swarm.Transports.Security.TLS`](#swarmtransportssecuritytls)
|
||||
- [`Swarm.Transports.Security.SECIO`](#swarmtransportssecuritysecio)
|
||||
@ -767,6 +768,26 @@ Default: `false` (DNSLink lookup enabled by default for every defined hostname)
|
||||
|
||||
Type: `bool`
|
||||
|
||||
#### `Gateway.PublicGateways: InlineDNSLink`
|
||||
|
||||
An optional flag to explicitly configure whether subdomain gateway's redirects
|
||||
(enabled by `UseSubdomains: true`) should always inline a DNSLink name (FQDN)
|
||||
into a single DNS label:
|
||||
|
||||
```
|
||||
//example.com/ipns/example.net → HTTP 301 → //example-net.ipns.example.com
|
||||
```
|
||||
|
||||
DNSLink name inlining allows for HTTPS on public subdomain gateways with single
|
||||
label wildcard TLS certs (also enabled when passing `X-Forwarded-Proto: https`),
|
||||
and provides disjoint Origin per root CID when special rules like
|
||||
https://publicsuffix.org, or a custom localhost logic in browsers like Brave
|
||||
has to be applied.
|
||||
|
||||
Default: `false`
|
||||
|
||||
Type: `flag`
|
||||
|
||||
#### Implicit defaults of `Gateway.PublicGateways`
|
||||
|
||||
Default entries for `localhost` hostname and loopback IPs are always present.
|
||||
@ -1964,7 +1985,7 @@ Default: Disabled
|
||||
Type: `flag`
|
||||
|
||||
|
||||
#### How to enable WebTransport
|
||||
##### How to enable WebTransport
|
||||
|
||||
Thoses steps are temporary and wont be needed once we make it enabled by default.
|
||||
|
||||
|
||||
@ -323,6 +323,38 @@ test_localhost_gateway_response_should_contain \
|
||||
"http://api.localhost:$GWAY_PORT/api/v0/refs?arg=$DIR_CID&r=true" \
|
||||
"Ref"
|
||||
|
||||
## ============================================================================
|
||||
## Test DNSLink inlining on HTTP gateways
|
||||
## ============================================================================
|
||||
|
||||
# set explicit subdomain gateway config for the hostname
|
||||
ipfs config --json Gateway.PublicGateways '{
|
||||
"localhost": {
|
||||
"UseSubdomains": true,
|
||||
"InlineDNSLink": true,
|
||||
"Paths": ["/ipfs", "/ipns", "/api"]
|
||||
},
|
||||
"example.com": {
|
||||
"UseSubdomains": true,
|
||||
"InlineDNSLink": true,
|
||||
"Paths": ["/ipfs", "/ipns", "/api"]
|
||||
}
|
||||
}' || exit 1
|
||||
# restart daemon to apply config changes
|
||||
test_kill_ipfs_daemon
|
||||
test_launch_ipfs_daemon_without_network
|
||||
|
||||
test_localhost_gateway_response_should_contain \
|
||||
"request for localhost/ipns/{fqdn} redirects to DNSLink in subdomain with DNS inlining" \
|
||||
"http://localhost:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
|
||||
"Location: http://en-wikipedia--on--ipfs-org.ipns.localhost:$GWAY_PORT/wiki"
|
||||
|
||||
test_hostname_gateway_response_should_contain \
|
||||
"request for example.com/ipns/{fqdn} redirects to DNSLink in subdomain with DNS inlining" \
|
||||
"example.com" \
|
||||
"http://127.0.0.1:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
|
||||
"Location: http://en-wikipedia--on--ipfs-org.ipns.example.com/wiki"
|
||||
|
||||
## ============================================================================
|
||||
## Test subdomain-based requests with a custom hostname config
|
||||
## (origin per content root at http://*.example.com)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user