From 09178aa717689a0ef9fd2042ad355320a16ffb35 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Fri, 1 Jan 2021 21:39:42 +0100 Subject: [PATCH 1/3] feat(gw): support inlined DNSLink names with TLS Problem statement and rationale for doing this can be found under "Option C" at: https://github.com/ipfs/in-web-browsers/issues/169 TLDR is: `https://dweb.link/ipns/my.v-long.example.com` 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` --- core/corehttp/hostname.go | 125 ++++++++++++++++++---- core/corehttp/hostname_test.go | 111 ++++++++++++++++--- docs/config.md | 3 +- test/sharness/t0114-gateway-subdomains.sh | 20 +++- 4 files changed, 218 insertions(+), 41 deletions(-) diff --git a/core/corehttp/hostname.go b/core/corehttp/hostname.go index 4a531a4d371..7e2d07821c7 100644 --- a/core/corehttp/hostname.go +++ b/core/corehttp/hostname.go @@ -91,7 +91,7 @@ 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) + newURL, err := toSubdomainURL(host, r.URL.Path, r, coreAPI) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -126,7 +126,7 @@ func HostnameOption() ServeOption { // Not a whitelisted path // Try DNSLink, if it was not explicitly disabled for the hostname - if !gw.NoDNSLink && isDNSLinkRequest(r.Context(), coreAPI, host) { + if !gw.NoDNSLink && isDNSLinkName(r.Context(), coreAPI, host) { // rewrite path and handle as DNSLink r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path childMux.ServeHTTP(w, withHostnameContext(r, host)) @@ -139,8 +139,10 @@ func HostnameOption() ServeOption { } // HTTP Host check: is this one of our subdomain-based "known gateways"? - // Example: {cid}.ipfs.localhost, {cid}.ipfs.dweb.link - if gw, hostname, ns, rootID, ok := knownSubdomainDetails(host, knownGateways); ok { + // IPFS details extracted from the host: {rootID}.{ns}.{gwHostname} + // /ipfs/ example: {cid}.ipfs.localhost:8080, {cid}.ipfs.dweb.link + // /ipns/ example: {libp2p-key}.ipns.localhost:8080, {inlined-dnslink-fqdn}.ipns.dweb.link + if gw, gwHostname, ns, rootID, ok := knownSubdomainDetails(host, knownGateways); ok { // Looks like we're using a known gateway in subdomain mode. // Assemble original path prefix. @@ -156,14 +158,14 @@ func HostnameOption() ServeOption { // Check if rootID is a valid CID if rootCID, err := cid.Decode(rootID); err == nil { // Do we need to redirect root CID to a canonical DNS representation? - dnsCID, err := toDNSPrefix(rootID, rootCID) + dnsCID, err := toDNSLabel(rootID, rootCID) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if !strings.HasPrefix(r.Host, dnsCID) { dnsPrefix := "/" + ns + "/" + dnsCID - newURL, err := toSubdomainURL(hostname, dnsPrefix+r.URL.Path, r) + newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, coreAPI) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -179,7 +181,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(hostname, pathPrefix+r.URL.Path, r) + newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, coreAPI) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -191,13 +193,36 @@ func HostnameOption() ServeOption { } } } + } else { // rootID is not a CID.. + + // Check if rootID is a single DNS label with an inlined + // DNSLink FQDN a single DNS label. We support this so + // loading DNSLink names over TLS "just works" on public + // HTTP gateways. + // + // Rationale for doing this can be found under "Option C" + // at: https://github.com/ipfs/in-web-browsers/issues/169 + // + // TLDR is: + // https://dweb.link/ipns/my.v-long.example.com + // 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 ns == "ipns" && !strings.Contains(rootID, ".") { + // my-v--long-example-com → my.v-long.example.com + dnslinkFQDN := toDNSLinkFQDN(rootID) + if isDNSLinkName(r.Context(), coreAPI, dnslinkFQDN) { + // update path prefix to use real FQDN with DNSLink + pathPrefix = "/ipns/" + dnslinkFQDN + } + } } // Rewrite the path to not use subdomains r.URL.Path = pathPrefix + r.URL.Path // Serve path request - childMux.ServeHTTP(w, withHostnameContext(r, hostname)) + childMux.ServeHTTP(w, withHostnameContext(r, gwHostname)) return } // We don't have a known gateway. Fallback on DNSLink lookup @@ -206,7 +231,7 @@ func HostnameOption() ServeOption { // 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)? // 2. does Host header include a fully qualified domain name (FQDN)? // 3. does DNSLink record exist in DNS? - if !cfg.Gateway.NoDNSLink && isDNSLinkRequest(r.Context(), coreAPI, host) { + if !cfg.Gateway.NoDNSLink && isDNSLinkName(r.Context(), coreAPI, host) { // rewrite path and handle as DNSLink r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path childMux.ServeHTTP(w, withHostnameContext(r, host)) @@ -305,9 +330,10 @@ func isKnownHostname(hostname string, knownGateways gatewayHosts) (gw *config.Ga } // Parses Host header and looks for a known gateway matching subdomain host. -// If found, returns GatewaySpec and subdomain components. +// If found, returns GatewaySpec and subdomain components extracted from Host +// header: {rootID}.{ns}.{gwHostname} // Note: hostname is host + optional port -func knownSubdomainDetails(hostname string, knownGateways gatewayHosts) (gw *config.GatewaySpec, knownHostname, ns, rootID string, ok bool) { +func knownSubdomainDetails(hostname string, knownGateways gatewayHosts) (gw *config.GatewaySpec, gwHostname, ns, rootID string, ok bool) { labels := strings.Split(hostname, ".") // Look for FQDN of a known gateway hostname. // Example: given "dist.ipfs.io.ipns.dweb.link": @@ -336,9 +362,8 @@ func knownSubdomainDetails(hostname string, knownGateways gatewayHosts) (gw *con return nil, "", "", "", false } -// isDNSLinkRequest returns bool that indicates if request -// should return data from content path listed in DNSLink record (if exists) -func isDNSLinkRequest(ctx context.Context, ipfs iface.CoreAPI, host string) bool { +// isDNSLinkName returns bool if a valid DNS TXT record exist for provided host +func isDNSLinkName(ctx context.Context, ipfs iface.CoreAPI, host string) bool { fqdn := stripPort(host) if len(fqdn) == 0 && !isd.IsDomain(fqdn) { return false @@ -368,8 +393,8 @@ func isPeerIDNamespace(ns string) bool { } } -// Converts an identifier to DNS-safe representation that fits in 63 characters -func toDNSPrefix(rootID string, rootCID cid.Cid) (prefix string, err error) { +// Converts a CID to DNS-safe representation that fits in 63 characters +func toDNSLabel(rootID string, rootCID cid.Cid) (dnsCID string, err error) { // Return as-is if things fit if len(rootID) <= dnsLabelMaxLength { return rootID, nil @@ -388,12 +413,45 @@ func toDNSPrefix(rootID string, rootCID cid.Cid) (prefix string, err error) { return "", fmt.Errorf("CID incompatible with DNS label length limit of 63: %s", rootID) } +// Returns true if HTTP request involves TLS certificate. +// See https://github.com/ipfs/in-web-browsers/issues/169 to uderstand how it +// impacts DNSLink websites on public gateways. +func isHTTPSRequest(r *http.Request) bool { + // X-Forwarded-Proto if added by a reverse proxy + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto + xproto := r.Header.Get("X-Forwarded-Proto") + // Is request a native TLS (not used atm, but future-proofing) + // or a proxied HTTPS (eg. go-ipfs behind nginx at a public gw)? + return r.URL.Scheme == "https" || xproto == "https" +} + +// Converts a FQDN to DNS-safe representation that fits in 63 characters: +// my.v-long.example.com → my-v--long-example-com +func toDNSLinkDNSLabel(fqdn string) (dnsLabel string, err error) { + dnsLabel = strings.ReplaceAll(fqdn, "-", "--") + dnsLabel = strings.ReplaceAll(dnsLabel, ".", "-") + if len(dnsLabel) > dnsLabelMaxLength { + return "", fmt.Errorf("DNSLink representation incompatible with DNS label length limit of 63: %s", dnsLabel) + } + return dnsLabel, nil +} + +// Converts a DNS-safe representation of DNSLink FQDN to real FQDN: +// my-v--long-example-com → my.v-long.example.com +func toDNSLinkFQDN(dnsLabel string) (fqdn string) { + fqdn = strings.ReplaceAll(dnsLabel, "--", "@") // @ placeholder is unused in DNS labels + fqdn = strings.ReplaceAll(fqdn, "-", ".") + fqdn = strings.ReplaceAll(fqdn, "@", "-") + return fqdn +} + // Converts a hostname/path to a subdomain-based URL, if applicable. -func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, err error) { +func toSubdomainURL(hostname, path string, r *http.Request, ipfs iface.CoreAPI) (redirURL string, err error) { var scheme, ns, rootID, rest string query := r.URL.RawQuery parts := strings.SplitN(path, "/", 4) + isHTTPS := isHTTPSRequest(r) safeRedirectURL := func(in string) (out string, err error) { safeURI, err := url.ParseRequestURI(in) if err != nil { @@ -402,10 +460,7 @@ func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, er return safeURI.String(), nil } - // Support X-Forwarded-Proto if added by a reverse proxy - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto - xproto := r.Header.Get("X-Forwarded-Proto") - if xproto == "https" { + if isHTTPS { scheme = "https:" } else { scheme = "http:" @@ -475,10 +530,36 @@ func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, er } // 2. Make sure CID fits in a DNS label, adjust encoding if needed // (https://github.com/ipfs/go-ipfs/issues/7318) - rootID, err = toDNSPrefix(rootID, rootCID) + rootID, err = toDNSLabel(rootID, rootCID) if err != nil { return "", err } + } else { // rootID is not a CID + + // Check if rootID is a FQDN with DNSLink and convert it to TLS-safe + // representation that fits in a single DNS label. We support this so + // loading DNSLink names over TLS "just works" on public HTTP gateways + // that pass 'https' in X-Forwarded-Proto to go-ipfs. + // + // Rationale can be found under "Option C" + // at: https://github.com/ipfs/in-web-browsers/issues/169 + // + // TLDR is: + // /ipns/my.v-long.example.com + // 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 isDNSLinkName(r.Context(), ipfs, rootID) { + // my.v-long.example.com → my-v--long-example-com + dnsLabel, err := toDNSLinkDNSLabel(rootID) + if err != nil { + return "", err + } + // update path prefix to use real FQDN with DNSLink + rootID = dnsLabel + } + } } return safeRedirectURL(fmt.Sprintf( diff --git a/core/corehttp/hostname_test.go b/core/corehttp/hostname_test.go index 3a316ede5f1..f469a7720cd 100644 --- a/core/corehttp/hostname_test.go +++ b/core/corehttp/hostname_test.go @@ -2,41 +2,121 @@ package corehttp import ( "errors" + "net/http" "net/http/httptest" "testing" cid "github.com/ipfs/go-cid" config "github.com/ipfs/go-ipfs-config" + files "github.com/ipfs/go-ipfs-files" + coreapi "github.com/ipfs/go-ipfs/core/coreapi" + path "github.com/ipfs/go-path" ) func TestToSubdomainURL(t *testing.T) { - r := httptest.NewRequest("GET", "http://request-stub.example.com", nil) + ns := mockNamesys{} + n, err := newNodeWithMockNamesys(ns) + if err != nil { + t.Fatal(err) + } + coreAPI, err := coreapi.NewCoreAPI(n) + if err != nil { + t.Fatal(err) + } + testCID, err := coreAPI.Unixfs().Add(n.Context(), files.NewBytesFile([]byte("fnord"))) + if err != nil { + t.Fatal(err) + } + ns["/ipns/dnslink.long-name.example.com"] = path.FromString(testCID.String()) + ns["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.FromString(testCID.String()) + httpRequest := httptest.NewRequest("GET", "http://127.0.0.1:8080", nil) + httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil) + httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil) + httpsProxiedRequest.Header.Set("X-Forwarded-Proto", "https") + for _, test := range []struct { // in: - hostname string - path string + request *http.Request + gwHostname string + path string // out: url string err error }{ // DNSLink - {"localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil}, + {httpRequest, "localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil}, // Hostname with port - {"localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil}, + {httpRequest, "localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil}, // CIDv0 → CIDv1base32 - {"localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil}, + {httpRequest, "localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil}, // CIDv1 with long sha512 - {"localhost", "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")}, + {httpRequest, "localhost", "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")}, // PeerID as CIDv1 needs to have libp2p-key multicodec - {"localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil}, - {"localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil}, + {httpRequest, "localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil}, + {httpRequest, "localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil}, // PeerID: ed25519+identity multihash → CIDv1Base36 - {"localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil}, - {"sub.localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil}, + {httpRequest, "localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil}, + {httpRequest, "sub.localhost", "/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}, } { - url, err := toSubdomainURL(test.hostname, test.path, r) + url, err := toSubdomainURL(test.gwHostname, test.path, test.request, coreAPI) if url != test.url || !equalError(err, test.err) { - t.Errorf("(%s, %s) returned (%s, %v), expected (%s, %v)", test.hostname, test.path, url, err, test.url, test.err) + t.Errorf("(%s, %s) returned (%s, %v), expected (%s, %v)", test.gwHostname, test.path, url, err, test.url, test.err) + } + } +} + +func TestToDNSLinkDNSLabel(t *testing.T) { + for _, test := range []struct { + in string + out string + err error + }{ + {"dnslink.long-name.example.com", "dnslink-long--name-example-com", nil}, + {"dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com", "", errors.New("DNSLink representation incompatible with DNS label length limit of 63: dnslink-too--long-f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o-example-com")}, + } { + out, err := toDNSLinkDNSLabel(test.in) + if out != test.out || !equalError(err, test.err) { + t.Errorf("(%s) returned (%s, %v), expected (%s, %v)", test.in, out, err, test.out, test.err) + } + } +} + +func TestToDNSLinkFQDN(t *testing.T) { + for _, test := range []struct { + in string + out string + }{ + {"singlelabel", "singlelabel"}, + {"docs-ipfs-io", "docs.ipfs.io"}, + {"dnslink-long--name-example-com", "dnslink.long-name.example.com"}, + } { + out := toDNSLinkFQDN(test.in) + if out != test.out { + t.Errorf("(%s) returned (%s), expected (%s)", test.in, out, test.out) + } + } +} + +func TestIsHTTPSRequest(t *testing.T) { + httpRequest := httptest.NewRequest("GET", "http://127.0.0.1:8080", nil) + httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil) + httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil) + httpsProxiedRequest.Header.Set("X-Forwarded-Proto", "https") + for _, test := range []struct { + in *http.Request + out bool + }{ + {httpRequest, false}, + {httpsRequest, true}, + {httpsProxiedRequest, true}, + } { + out := isHTTPSRequest(test.in) + if out != test.out { + t.Errorf("(%+v): returned %t, expected %t", test.in, out, test.out) } } } @@ -77,10 +157,9 @@ func TestPortStripping(t *testing.T) { t.Errorf("(%s): returned '%s', expected '%s'", test.in, out, test.out) } } - } -func TestDNSPrefix(t *testing.T) { +func TestToDNSLabel(t *testing.T) { for _, test := range []struct { in string out string @@ -96,7 +175,7 @@ func TestDNSPrefix(t *testing.T) { {"bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")}, } { inCID, _ := cid.Decode(test.in) - out, err := toDNSPrefix(test.in, inCID) + out, err := toDNSLabel(test.in, inCID) if out != test.out || !equalError(err, test.err) { t.Errorf("(%s): returned (%s, %v) expected (%s, %v)", test.in, out, err, test.out, test.err) } diff --git a/docs/config.md b/docs/config.md index 2ea82bca39a..d2806a8a7d0 100644 --- a/docs/config.md +++ b/docs/config.md @@ -709,8 +709,9 @@ Below is a list of the most common public gateway setups. ``` **Backward-compatible:** this feature enables automatic redirects from content paths to subdomains: `http://dweb.link/ipfs/{cid}` → `http://{cid}.ipfs.dweb.link` - **X-Forwarded-Proto:** if you run go-ipfs behind a reverse proxy that provides TLS, make it add a `X-Forwarded-Proto: https` HTTP header to ensure users are redirected to `https://`, not `http://`. The NGINX directive is `proxy_set_header X-Forwarded-Proto "https";`.: + **X-Forwarded-Proto:** if you run go-ipfs behind a reverse proxy that provides TLS, make it add a `X-Forwarded-Proto: https` HTTP header to ensure users are redirected to `https://`, not `http://`. It will also ensure DNSLink names are inlined to fit in a single DNS label, so they work fine with a wildcart TLS cert ([details](https://github.com/ipfs/in-web-browsers/issues/169)). The NGINX directive is `proxy_set_header X-Forwarded-Proto "https";`.: `http://dweb.link/ipfs/{cid}` → `https://{cid}.ipfs.dweb.link` + `http://dweb.link/ipns/your-dnslink.site.example.com` → `https://your--dnslink-site-example-com.ipfs.dweb.link` **X-Forwarded-Host:** we also support `X-Forwarded-Host: example.com` if you want to override subdomain gateway host from the original request: `http://dweb.link/ipfs/{cid}` → `http://{cid}.ipfs.example.com` diff --git a/test/sharness/t0114-gateway-subdomains.sh b/test/sharness/t0114-gateway-subdomains.sh index 93232577638..4c5f49b6351 100755 --- a/test/sharness/t0114-gateway-subdomains.sh +++ b/test/sharness/t0114-gateway-subdomains.sh @@ -403,6 +403,14 @@ test_hostname_gateway_response_should_contain \ "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" +# DNSLink on Public gateway with a single-level wildcard TLS cert +# "Option C" from https://github.com/ipfs/in-web-browsers/issues/169 +test_expect_success \ + "request for example.com/ipns/{fqdn} with X-Forwarded-Proto redirects to TLS-safe label in subdomain" " + curl -H \"Host: example.com\" -H \"X-Forwarded-Proto: https\" -sD - \"http://127.0.0.1:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki\" > response && + test_should_contain \"Location: https://en-wikipedia--on--ipfs-org.ipns.example.com/wiki\" response + " + # *.ipfs.example.com: subdomain requests made with custom FQDN in Host header test_hostname_gateway_response_should_contain \ @@ -539,14 +547,22 @@ test_hostname_gateway_response_should_contain \ "http://127.0.0.1:$GWAY_PORT" \ "$CID_VAL" +# DNSLink on Public gateway with a single-level wildcard TLS cert +# "Option C" from https://github.com/ipfs/in-web-browsers/issues/169 +test_expect_success \ + "request for {single-label-dnslink}.ipns.example.com with X-Forwarded-Proto returns expected payload" " + curl -H \"Host: dnslink--subdomain--gw--test-example-org.ipns.example.com\" -H \"X-Forwarded-Proto: https\" -sD - \"http://127.0.0.1:$GWAY_PORT\" > response && + test_should_contain \"$CID_VAL\" response + " + ## Test subdomain handling of CIDs that do not fit in a single DNS Label (>63chars) ## https://github.com/ipfs/go-ipfs/issues/7318 ## ============================================================================ # ed25519 fits under 63 char limit when represented in base36 IPNS_KEY="test_key_ed25519" -IPNS_ED25519_B58MH=$(ipfs key list -l -f b58mh | grep $IPNS_KEY | cut -d " " -f1 | tr -d "\n") -IPNS_ED25519_B36CID=$(ipfs key list -l -f b36cid | grep $IPNS_KEY | cut -d " " -f1 | tr -d "\n") +IPNS_ED25519_B58MH=$(ipfs key list -l --ipns-base b58mh | grep $IPNS_KEY | cut -d" " -f1 | tr -d "\n") +IPNS_ED25519_B36CID=$(ipfs key list -l --ipns-base base36 | grep $IPNS_KEY | cut -d" " -f1 | tr -d "\n") # sha512 will be over 63char limit, even when represented in Base36 CIDv1_TOO_LONG=$(echo $CID_VAL | ipfs add --cid-version 1 --hash sha2-512 -Q) From 88dd257ace53b56759a760ab9df189e43a7fda17 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Fri, 8 Jan 2021 00:31:46 +0100 Subject: [PATCH 2/3] test: false for isHTTPSRequest As suggested in https://github.com/ipfs/go-ipfs/pull/7847#discussion_r551933162 --- core/corehttp/hostname.go | 2 +- core/corehttp/hostname_test.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/corehttp/hostname.go b/core/corehttp/hostname.go index 7e2d07821c7..3ce1d08c26d 100644 --- a/core/corehttp/hostname.go +++ b/core/corehttp/hostname.go @@ -414,7 +414,7 @@ func toDNSLabel(rootID string, rootCID cid.Cid) (dnsCID string, err error) { } // Returns true if HTTP request involves TLS certificate. -// See https://github.com/ipfs/in-web-browsers/issues/169 to uderstand how it +// See https://github.com/ipfs/in-web-browsers/issues/169 to understand how it // impacts DNSLink websites on public gateways. func isHTTPSRequest(r *http.Request) bool { // X-Forwarded-Proto if added by a reverse proxy diff --git a/core/corehttp/hostname_test.go b/core/corehttp/hostname_test.go index f469a7720cd..3ece5e88f8f 100644 --- a/core/corehttp/hostname_test.go +++ b/core/corehttp/hostname_test.go @@ -106,6 +106,9 @@ func TestIsHTTPSRequest(t *testing.T) { httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil) httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil) httpsProxiedRequest.Header.Set("X-Forwarded-Proto", "https") + httpProxiedRequest := httptest.NewRequest("GET", "http://proxied-http-request-stub.example.com", nil) + httpProxiedRequest.Header.Set("X-Forwarded-Proto", "http") + oddballRequest := httptest.NewRequest("GET", "foo://127.0.0.1:8080", nil) for _, test := range []struct { in *http.Request out bool @@ -113,6 +116,8 @@ func TestIsHTTPSRequest(t *testing.T) { {httpRequest, false}, {httpsRequest, true}, {httpsProxiedRequest, true}, + {httpProxiedRequest, false}, + {oddballRequest, false}, } { out := isHTTPSRequest(test.in) if out != test.out { From f932510b88a08bb674e4191431b3d33a7b3ec690 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 14 Jan 2021 20:14:35 +0100 Subject: [PATCH 3/3] fix: check if rootID has DNSLink before uninlining This kinda enables to run their custom DNS resolver with custom tlds/names that are independent from the public DNS network. --- core/corehttp/hostname.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/corehttp/hostname.go b/core/corehttp/hostname.go index 3ce1d08c26d..da133f7abe1 100644 --- a/core/corehttp/hostname.go +++ b/core/corehttp/hostname.go @@ -209,11 +209,14 @@ func HostnameOption() ServeOption { // TLS cert if represented as a single DNS label: // https://my-v--long-example-com.ipns.dweb.link if ns == "ipns" && !strings.Contains(rootID, ".") { - // my-v--long-example-com → my.v-long.example.com - dnslinkFQDN := toDNSLinkFQDN(rootID) - if isDNSLinkName(r.Context(), coreAPI, dnslinkFQDN) { - // update path prefix to use real FQDN with DNSLink - pathPrefix = "/ipns/" + dnslinkFQDN + // if there is no TXT recordfor rootID + if !isDNSLinkName(r.Context(), coreAPI, rootID) { + // my-v--long-example-com → my.v-long.example.com + dnslinkFQDN := toDNSLinkFQDN(rootID) + if isDNSLinkName(r.Context(), coreAPI, dnslinkFQDN) { + // update path prefix to use real FQDN with DNSLink + pathPrefix = "/ipns/" + dnslinkFQDN + } } } }