@cryptotaxi247 / kubo / commits / 4291d6b23

fix: add InlineDNSLink flag to PublicGateways config (#9328)

https://github.com/ipfs/kubo/issues/9243 Co-authored-by: Marcin Rataj <lidel@lidel.org>

Henrique Dias committed Oct 11, 2022 at 15:45 UTC 4291d6b2369b8f1dc3cd5c191cd4f3b099df1560
5 files changed +91 -23
config/gateway.go
+7
@@ -1,5 +1,7 @@
1 package config
2
3 +const DefaultInlineDNSLink = false
4 +
5 type GatewaySpec struct {
6 // Paths is explicit list of path prefixes that should be handled by
7 // this gateway. Example: `["/ipfs", "/ipns", "/api"]`
@@ -18,6 +20,11 @@ type GatewaySpec struct {
20 // NoDNSLink configures this gateway to _not_ resolve DNSLink for the FQDN
21 // provided in `Host` HTTP header.
22 NoDNSLink bool
23 +
24 + // InlineDNSLink configures this gateway to always inline DNSLink names
25 + // (FQDN) into a single DNS label in order to interop with wildcard TLS certs
26 + // and Origin per CID isolation provided by rules like https://publicsuffix.org
27 + InlineDNSLink Flag
28 }
29
30 // Gateway contains options for the HTTP gateway server.
core/corehttp/hostname.go
+9 -5
@@ -84,7 +84,8 @@ func HostnameOption() ServeOption {
84 if gw.UseSubdomains {
85 // Yes, redirect if applicable
86 // Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link
87 - newURL, err := toSubdomainURL(host, r.URL.Path, r, coreAPI)
87 + useInlinedDNSLink := gw.InlineDNSLink.WithDefault(config.DefaultInlineDNSLink)
88 + newURL, err := toSubdomainURL(host, r.URL.Path, r, useInlinedDNSLink, coreAPI)
89 if err != nil {
90 http.Error(w, err.Error(), http.StatusBadRequest)
91 return
@@ -132,6 +133,9 @@ func HostnameOption() ServeOption {
133 // Assemble original path prefix.
134 pathPrefix := "/" + ns + "/" + rootID
135
136 + // Retrieve whether or not we should inline DNSLink.
137 + useInlinedDNSLink := gw.InlineDNSLink.WithDefault(config.DefaultInlineDNSLink)
138 +
139 // Does this gateway _handle_ subdomains AND this path?
140 if !(gw.UseSubdomains && hasPrefix(pathPrefix, gw.Paths...)) {
141 // If not, resource does not exist, return 404
@@ -149,7 +153,7 @@ func HostnameOption() ServeOption {
153 }
154 if !strings.HasPrefix(r.Host, dnsCID) {
155 dnsPrefix := "/" + ns + "/" + dnsCID
152 - newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, coreAPI)
156 + newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, useInlinedDNSLink, coreAPI)
157 if err != nil {
158 http.Error(w, err.Error(), http.StatusBadRequest)
159 return
@@ -165,7 +169,7 @@ func HostnameOption() ServeOption {
169 // Do we need to fix multicodec in PeerID represented as CIDv1?
170 if isPeerIDNamespace(ns) {
171 if rootCID.Type() != cid.Libp2pKey {
168 - newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, coreAPI)
172 + newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, useInlinedDNSLink, coreAPI)
173 if err != nil {
174 http.Error(w, err.Error(), http.StatusBadRequest)
175 return
@@ -451,7 +455,7 @@ func toDNSLinkFQDN(dnsLabel string) (fqdn string) {
455 }
456
457 // Converts a hostname/path to a subdomain-based URL, if applicable.
454 -func toSubdomainURL(hostname, path string, r *http.Request, ipfs iface.CoreAPI) (redirURL string, err error) {
458 +func toSubdomainURL(hostname, path string, r *http.Request, inlineDNSLink bool, ipfs iface.CoreAPI) (redirURL string, err error) {
459 var scheme, ns, rootID, rest string
460
461 query := r.URL.RawQuery
@@ -554,7 +558,7 @@ func toSubdomainURL(hostname, path string, r *http.Request, ipfs iface.CoreAPI)
558 // can be loaded from a subdomain gateway with a wildcard TLS cert if
559 // represented as a single DNS label:
560 // https://my-v--long-example-com.ipns.dweb.link
557 - if isHTTPS && ns == "ipns" && strings.Contains(rootID, ".") {
561 + if (inlineDNSLink || isHTTPS) && ns == "ipns" && strings.Contains(rootID, ".") {
562 if isDNSLinkName(r.Context(), ipfs, rootID) {
563 // my.v-long.example.com → my-v--long-example-com
564 dnsLabel, err := toDNSLinkDNSLabel(rootID)
core/corehttp/hostname_test.go
+20 -16
@@ -36,35 +36,39 @@ func TestToSubdomainURL(t *testing.T) {
36
37 for _, test := range []struct {
38 // in:
39 - request *http.Request
40 - gwHostname string
41 - path string
39 + request *http.Request
40 + gwHostname string
41 + inlineDNSLink bool
42 + path string
43 // out:
44 url string
45 err error
46 }{
47 // DNSLink
47 - {httpRequest, "localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
48 + {httpRequest, "localhost", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
49 // Hostname with port
49 - {httpRequest, "localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
50 + {httpRequest, "localhost:8080", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
51 // CIDv0 → CIDv1base32
51 - {httpRequest, "localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
52 + {httpRequest, "localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
53 // CIDv1 with long sha512
53 - {httpRequest, "localhost", "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
54 + {httpRequest, "localhost", false, "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
55 // PeerID as CIDv1 needs to have libp2p-key multicodec
55 - {httpRequest, "localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
56 - {httpRequest, "localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
56 + {httpRequest, "localhost", false, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
57 + {httpRequest, "localhost", false, "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
58 // PeerID: ed25519+identity multihash → CIDv1Base36
58 - {httpRequest, "localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
59 - {httpRequest, "sub.localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
59 + {httpRequest, "localhost", false, "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
60 + {httpRequest, "sub.localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
61 // HTTPS requires DNSLink name to fit in a single DNS label – see "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
61 - {httpRequest, "dweb.link", "/ipns/dnslink.long-name.example.com", "http://dnslink.long-name.example.com.ipns.dweb.link/", nil},
62 - {httpsRequest, "dweb.link", "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
63 - {httpsProxiedRequest, "dweb.link", "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
62 + {httpRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "http://dnslink.long-name.example.com.ipns.dweb.link/", nil},
63 + {httpsRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
64 + {httpsProxiedRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
65 + // HTTP requests can also be converted to fit into a single DNS label - https://github.com/ipfs/kubo/issues/9243
66 + {httpRequest, "localhost", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.localhost/", nil},
67 + {httpRequest, "dweb.link", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.dweb.link/", nil},
68 } {
65 - url, err := toSubdomainURL(test.gwHostname, test.path, test.request, coreAPI)
69 + url, err := toSubdomainURL(test.gwHostname, test.path, test.request, test.inlineDNSLink, coreAPI)
70 if url != test.url || !equalError(err, test.err) {
67 - t.Errorf("(%s, %s) returned (%s, %v), expected (%s, %v)", test.gwHostname, test.path, url, err, test.url, test.err)
71 + t.Errorf("(%s, %v, %s) returned (%s, %v), expected (%s, %v)", test.gwHostname, test.inlineDNSLink, test.path, url, err, test.url, test.err)
72 }
73 }
74 }
docs/config.md
+23 -2
@@ -59,6 +59,7 @@ config file at runtime.
59 - [`Gateway.PublicGateways: Paths`](#gatewaypublicgateways-paths)
60 - [`Gateway.PublicGateways: UseSubdomains`](#gatewaypublicgateways-usesubdomains)
61 - [`Gateway.PublicGateways: NoDNSLink`](#gatewaypublicgateways-nodnslink)
62 + - [`Gateway.PublicGateways: InlineDNSLink`](#gatewaypublicgateways-inlinednslink)
63 - [Implicit defaults of `Gateway.PublicGateways`](#implicit-defaults-of-gatewaypublicgateways)
64 - [`Gateway` recipes](#gateway-recipes)
65 - [`Identity`](#identity)
@@ -149,7 +150,7 @@ config file at runtime.
150 - [`Swarm.Transports.Network.QUIC`](#swarmtransportsnetworkquic)
151 - [`Swarm.Transports.Network.Relay`](#swarmtransportsnetworkrelay)
152 - [`Swarm.Transports.Network.WebTransport`](#swarmtransportsnetworkwebtransport)
152 - - [`How to enable WebTransport`](#how-to-enable-webtransport)
153 + - [How to enable WebTransport](#how-to-enable-webtransport)
154 - [`Swarm.Transports.Security`](#swarmtransportssecurity)
155 - [`Swarm.Transports.Security.TLS`](#swarmtransportssecuritytls)
156 - [`Swarm.Transports.Security.SECIO`](#swarmtransportssecuritysecio)
@@ -767,6 +768,26 @@ Default: `false` (DNSLink lookup enabled by default for every defined hostname)
768
769 Type: `bool`
770
771 +#### `Gateway.PublicGateways: InlineDNSLink`
772 +
773 +An optional flag to explicitly configure whether subdomain gateway's redirects
774 +(enabled by `UseSubdomains: true`) should always inline a DNSLink name (FQDN)
775 +into a single DNS label:
776 +
777 +```
778 +//example.com/ipns/example.net → HTTP 301 → //example-net.ipns.example.com
779 +```
780 +
781 +DNSLink name inlining allows for HTTPS on public subdomain gateways with single
782 +label wildcard TLS certs (also enabled when passing `X-Forwarded-Proto: https`),
783 +and provides disjoint Origin per root CID when special rules like
784 +https://publicsuffix.org, or a custom localhost logic in browsers like Brave
785 +has to be applied.
786 +
787 +Default: `false`
788 +
789 +Type: `flag`
790 +
791 #### Implicit defaults of `Gateway.PublicGateways`
792
793 Default entries for `localhost` hostname and loopback IPs are always present.
@@ -1964,7 +1985,7 @@ Default: Disabled
1985 Type: `flag`
1986
1987
1967 -#### How to enable WebTransport
1988 +##### How to enable WebTransport
1989
1990 Thoses steps are temporary and wont be needed once we make it enabled by default.
1991
test/sharness/t0114-gateway-subdomains.sh
+32
@@ -323,6 +323,38 @@ test_localhost_gateway_response_should_contain \
323 "http://api.localhost:$GWAY_PORT/api/v0/refs?arg=$DIR_CID&r=true" \
324 "Ref"
325
326 +## ============================================================================
327 +## Test DNSLink inlining on HTTP gateways
328 +## ============================================================================
329 +
330 +# set explicit subdomain gateway config for the hostname
331 +ipfs config --json Gateway.PublicGateways '{
332 + "localhost": {
333 + "UseSubdomains": true,
334 + "InlineDNSLink": true,
335 + "Paths": ["/ipfs", "/ipns", "/api"]
336 + },
337 + "example.com": {
338 + "UseSubdomains": true,
339 + "InlineDNSLink": true,
340 + "Paths": ["/ipfs", "/ipns", "/api"]
341 + }
342 +}' || exit 1
343 +# restart daemon to apply config changes
344 +test_kill_ipfs_daemon
345 +test_launch_ipfs_daemon_without_network
346 +
347 +test_localhost_gateway_response_should_contain \
348 + "request for localhost/ipns/{fqdn} redirects to DNSLink in subdomain with DNS inlining" \
349 + "http://localhost:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
350 + "Location: http://en-wikipedia--on--ipfs-org.ipns.localhost:$GWAY_PORT/wiki"
351 +
352 +test_hostname_gateway_response_should_contain \
353 + "request for example.com/ipns/{fqdn} redirects to DNSLink in subdomain with DNS inlining" \
354 + "example.com" \
355 + "http://127.0.0.1:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
356 + "Location: http://en-wikipedia--on--ipfs-org.ipns.example.com/wiki"
357 +
358 ## ============================================================================
359 ## Test subdomain-based requests with a custom hostname config
360 ## (origin per content root at http://*.example.com)