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`
Marcin Rataj committed
Jan 1, 2021 at 21:39 UTC
09178aa717689a0ef9fd2042ad355320a16ffb35
4 files changed
+218
-41
core/corehttp/hostname.go
+103
-22
@@ -91,7 +91,7 @@ func HostnameOption() ServeOption {
91
if gw.UseSubdomains {
92
// Yes, redirect if applicable
93
// Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link
94
- newURL, err := toSubdomainURL(host, r.URL.Path, r)
94
+ newURL, err := toSubdomainURL(host, r.URL.Path, r, coreAPI)
95
if err != nil {
96
http.Error(w, err.Error(), http.StatusBadRequest)
97
return
@@ -126,7 +126,7 @@ func HostnameOption() ServeOption {
126
// Not a whitelisted path
127
128
// Try DNSLink, if it was not explicitly disabled for the hostname
129
- if !gw.NoDNSLink && isDNSLinkRequest(r.Context(), coreAPI, host) {
129
+ if !gw.NoDNSLink && isDNSLinkName(r.Context(), coreAPI, host) {
130
// rewrite path and handle as DNSLink
131
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
132
childMux.ServeHTTP(w, withHostnameContext(r, host))
@@ -139,8 +139,10 @@ func HostnameOption() ServeOption {
139
}
140
141
// HTTP Host check: is this one of our subdomain-based "known gateways"?
142
- // Example: {cid}.ipfs.localhost, {cid}.ipfs.dweb.link
143
- if gw, hostname, ns, rootID, ok := knownSubdomainDetails(host, knownGateways); ok {
142
+ // IPFS details extracted from the host: {rootID}.{ns}.{gwHostname}
143
+ // /ipfs/ example: {cid}.ipfs.localhost:8080, {cid}.ipfs.dweb.link
144
+ // /ipns/ example: {libp2p-key}.ipns.localhost:8080, {inlined-dnslink-fqdn}.ipns.dweb.link
145
+ if gw, gwHostname, ns, rootID, ok := knownSubdomainDetails(host, knownGateways); ok {
146
// Looks like we're using a known gateway in subdomain mode.
147
148
// Assemble original path prefix.
@@ -156,14 +158,14 @@ func HostnameOption() ServeOption {
158
// Check if rootID is a valid CID
159
if rootCID, err := cid.Decode(rootID); err == nil {
160
// Do we need to redirect root CID to a canonical DNS representation?
159
- dnsCID, err := toDNSPrefix(rootID, rootCID)
161
+ dnsCID, err := toDNSLabel(rootID, rootCID)
162
if err != nil {
163
http.Error(w, err.Error(), http.StatusBadRequest)
164
return
165
}
166
if !strings.HasPrefix(r.Host, dnsCID) {
167
dnsPrefix := "/" + ns + "/" + dnsCID
166
- newURL, err := toSubdomainURL(hostname, dnsPrefix+r.URL.Path, r)
168
+ newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, coreAPI)
169
if err != nil {
170
http.Error(w, err.Error(), http.StatusBadRequest)
171
return
@@ -179,7 +181,7 @@ func HostnameOption() ServeOption {
181
// Do we need to fix multicodec in PeerID represented as CIDv1?
182
if isPeerIDNamespace(ns) {
183
if rootCID.Type() != cid.Libp2pKey {
182
- newURL, err := toSubdomainURL(hostname, pathPrefix+r.URL.Path, r)
184
+ newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, coreAPI)
185
if err != nil {
186
http.Error(w, err.Error(), http.StatusBadRequest)
187
return
@@ -191,13 +193,36 @@ func HostnameOption() ServeOption {
193
}
194
}
195
}
196
+ } else { // rootID is not a CID..
197
+
198
+ // Check if rootID is a single DNS label with an inlined
199
+ // DNSLink FQDN a single DNS label. We support this so
200
+ // loading DNSLink names over TLS "just works" on public
201
+ // HTTP gateways.
202
+ //
203
+ // Rationale for doing this can be found under "Option C"
204
+ // at: https://github.com/ipfs/in-web-browsers/issues/169
205
+ //
206
+ // TLDR is:
207
+ // https://dweb.link/ipns/my.v-long.example.com
208
+ // can be loaded from a subdomain gateway with a wildcard
209
+ // TLS cert if represented as a single DNS label:
210
+ // https://my-v--long-example-com.ipns.dweb.link
211
+ if ns == "ipns" && !strings.Contains(rootID, ".") {
212
+ // my-v--long-example-com → my.v-long.example.com
213
+ dnslinkFQDN := toDNSLinkFQDN(rootID)
214
+ if isDNSLinkName(r.Context(), coreAPI, dnslinkFQDN) {
215
+ // update path prefix to use real FQDN with DNSLink
216
+ pathPrefix = "/ipns/" + dnslinkFQDN
217
+ }
218
+ }
219
}
220
221
// Rewrite the path to not use subdomains
222
r.URL.Path = pathPrefix + r.URL.Path
223
224
// Serve path request
200
- childMux.ServeHTTP(w, withHostnameContext(r, hostname))
225
+ childMux.ServeHTTP(w, withHostnameContext(r, gwHostname))
226
return
227
}
228
// We don't have a known gateway. Fallback on DNSLink lookup
@@ -206,7 +231,7 @@ func HostnameOption() ServeOption {
231
// 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)?
232
// 2. does Host header include a fully qualified domain name (FQDN)?
233
// 3. does DNSLink record exist in DNS?
209
- if !cfg.Gateway.NoDNSLink && isDNSLinkRequest(r.Context(), coreAPI, host) {
234
+ if !cfg.Gateway.NoDNSLink && isDNSLinkName(r.Context(), coreAPI, host) {
235
// rewrite path and handle as DNSLink
236
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
237
childMux.ServeHTTP(w, withHostnameContext(r, host))
@@ -305,9 +330,10 @@ func isKnownHostname(hostname string, knownGateways gatewayHosts) (gw *config.Ga
330
}
331
332
// Parses Host header and looks for a known gateway matching subdomain host.
308
-// If found, returns GatewaySpec and subdomain components.
333
+// If found, returns GatewaySpec and subdomain components extracted from Host
334
+// header: {rootID}.{ns}.{gwHostname}
335
// Note: hostname is host + optional port
310
-func knownSubdomainDetails(hostname string, knownGateways gatewayHosts) (gw *config.GatewaySpec, knownHostname, ns, rootID string, ok bool) {
336
+func knownSubdomainDetails(hostname string, knownGateways gatewayHosts) (gw *config.GatewaySpec, gwHostname, ns, rootID string, ok bool) {
337
labels := strings.Split(hostname, ".")
338
// Look for FQDN of a known gateway hostname.
339
// Example: given "dist.ipfs.io.ipns.dweb.link":
@@ -336,9 +362,8 @@ func knownSubdomainDetails(hostname string, knownGateways gatewayHosts) (gw *con
362
return nil, "", "", "", false
363
}
364
339
-// isDNSLinkRequest returns bool that indicates if request
340
-// should return data from content path listed in DNSLink record (if exists)
341
-func isDNSLinkRequest(ctx context.Context, ipfs iface.CoreAPI, host string) bool {
365
+// isDNSLinkName returns bool if a valid DNS TXT record exist for provided host
366
+func isDNSLinkName(ctx context.Context, ipfs iface.CoreAPI, host string) bool {
367
fqdn := stripPort(host)
368
if len(fqdn) == 0 && !isd.IsDomain(fqdn) {
369
return false
@@ -368,8 +393,8 @@ func isPeerIDNamespace(ns string) bool {
393
}
394
}
395
371
-// Converts an identifier to DNS-safe representation that fits in 63 characters
372
-func toDNSPrefix(rootID string, rootCID cid.Cid) (prefix string, err error) {
396
+// Converts a CID to DNS-safe representation that fits in 63 characters
397
+func toDNSLabel(rootID string, rootCID cid.Cid) (dnsCID string, err error) {
398
// Return as-is if things fit
399
if len(rootID) <= dnsLabelMaxLength {
400
return rootID, nil
@@ -388,12 +413,45 @@ func toDNSPrefix(rootID string, rootCID cid.Cid) (prefix string, err error) {
413
return "", fmt.Errorf("CID incompatible with DNS label length limit of 63: %s", rootID)
414
}
415
416
+// Returns true if HTTP request involves TLS certificate.
417
+// See https://github.com/ipfs/in-web-browsers/issues/169 to uderstand how it
418
+// impacts DNSLink websites on public gateways.
419
+func isHTTPSRequest(r *http.Request) bool {
420
+ // X-Forwarded-Proto if added by a reverse proxy
421
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
422
+ xproto := r.Header.Get("X-Forwarded-Proto")
423
+ // Is request a native TLS (not used atm, but future-proofing)
424
+ // or a proxied HTTPS (eg. go-ipfs behind nginx at a public gw)?
425
+ return r.URL.Scheme == "https" || xproto == "https"
426
+}
427
+
428
+// Converts a FQDN to DNS-safe representation that fits in 63 characters:
429
+// my.v-long.example.com → my-v--long-example-com
430
+func toDNSLinkDNSLabel(fqdn string) (dnsLabel string, err error) {
431
+ dnsLabel = strings.ReplaceAll(fqdn, "-", "--")
432
+ dnsLabel = strings.ReplaceAll(dnsLabel, ".", "-")
433
+ if len(dnsLabel) > dnsLabelMaxLength {
434
+ return "", fmt.Errorf("DNSLink representation incompatible with DNS label length limit of 63: %s", dnsLabel)
435
+ }
436
+ return dnsLabel, nil
437
+}
438
+
439
+// Converts a DNS-safe representation of DNSLink FQDN to real FQDN:
440
+// my-v--long-example-com → my.v-long.example.com
441
+func toDNSLinkFQDN(dnsLabel string) (fqdn string) {
442
+ fqdn = strings.ReplaceAll(dnsLabel, "--", "@") // @ placeholder is unused in DNS labels
443
+ fqdn = strings.ReplaceAll(fqdn, "-", ".")
444
+ fqdn = strings.ReplaceAll(fqdn, "@", "-")
445
+ return fqdn
446
+}
447
+
448
// Converts a hostname/path to a subdomain-based URL, if applicable.
392
-func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, err error) {
449
+func toSubdomainURL(hostname, path string, r *http.Request, ipfs iface.CoreAPI) (redirURL string, err error) {
450
var scheme, ns, rootID, rest string
451
452
query := r.URL.RawQuery
453
parts := strings.SplitN(path, "/", 4)
454
+ isHTTPS := isHTTPSRequest(r)
455
safeRedirectURL := func(in string) (out string, err error) {
456
safeURI, err := url.ParseRequestURI(in)
457
if err != nil {
@@ -402,10 +460,7 @@ func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, er
460
return safeURI.String(), nil
461
}
462
405
- // Support X-Forwarded-Proto if added by a reverse proxy
406
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
407
- xproto := r.Header.Get("X-Forwarded-Proto")
408
- if xproto == "https" {
463
+ if isHTTPS {
464
scheme = "https:"
465
} else {
466
scheme = "http:"
@@ -475,10 +530,36 @@ func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, er
530
}
531
// 2. Make sure CID fits in a DNS label, adjust encoding if needed
532
// (https://github.com/ipfs/go-ipfs/issues/7318)
478
- rootID, err = toDNSPrefix(rootID, rootCID)
533
+ rootID, err = toDNSLabel(rootID, rootCID)
534
if err != nil {
535
return "", err
536
}
537
+ } else { // rootID is not a CID
538
+
539
+ // Check if rootID is a FQDN with DNSLink and convert it to TLS-safe
540
+ // representation that fits in a single DNS label. We support this so
541
+ // loading DNSLink names over TLS "just works" on public HTTP gateways
542
+ // that pass 'https' in X-Forwarded-Proto to go-ipfs.
543
+ //
544
+ // Rationale can be found under "Option C"
545
+ // at: https://github.com/ipfs/in-web-browsers/issues/169
546
+ //
547
+ // TLDR is:
548
+ // /ipns/my.v-long.example.com
549
+ // can be loaded from a subdomain gateway with a wildcard TLS cert if
550
+ // represented as a single DNS label:
551
+ // https://my-v--long-example-com.ipns.dweb.link
552
+ if isHTTPS && ns == "ipns" && strings.Contains(rootID, ".") {
553
+ if isDNSLinkName(r.Context(), ipfs, rootID) {
554
+ // my.v-long.example.com → my-v--long-example-com
555
+ dnsLabel, err := toDNSLinkDNSLabel(rootID)
556
+ if err != nil {
557
+ return "", err
558
+ }
559
+ // update path prefix to use real FQDN with DNSLink
560
+ rootID = dnsLabel
561
+ }
562
+ }
563
}
564
565
return safeRedirectURL(fmt.Sprintf(
core/corehttp/hostname_test.go
+95
-16
@@ -2,41 +2,121 @@ package corehttp
2
3
import (
4
"errors"
5
+ "net/http"
6
"net/http/httptest"
7
"testing"
8
9
cid "github.com/ipfs/go-cid"
10
config "github.com/ipfs/go-ipfs-config"
11
+ files "github.com/ipfs/go-ipfs-files"
12
+ coreapi "github.com/ipfs/go-ipfs/core/coreapi"
13
+ path "github.com/ipfs/go-path"
14
)
15
16
func TestToSubdomainURL(t *testing.T) {
13
- r := httptest.NewRequest("GET", "http://request-stub.example.com", nil)
17
+ ns := mockNamesys{}
18
+ n, err := newNodeWithMockNamesys(ns)
19
+ if err != nil {
20
+ t.Fatal(err)
21
+ }
22
+ coreAPI, err := coreapi.NewCoreAPI(n)
23
+ if err != nil {
24
+ t.Fatal(err)
25
+ }
26
+ testCID, err := coreAPI.Unixfs().Add(n.Context(), files.NewBytesFile([]byte("fnord")))
27
+ if err != nil {
28
+ t.Fatal(err)
29
+ }
30
+ ns["/ipns/dnslink.long-name.example.com"] = path.FromString(testCID.String())
31
+ ns["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.FromString(testCID.String())
32
+ httpRequest := httptest.NewRequest("GET", "http://127.0.0.1:8080", nil)
33
+ httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil)
34
+ httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil)
35
+ httpsProxiedRequest.Header.Set("X-Forwarded-Proto", "https")
36
+
37
for _, test := range []struct {
38
// in:
16
- hostname string
17
- path string
39
+ request *http.Request
40
+ gwHostname string
41
+ path string
42
// out:
43
url string
44
err error
45
}{
46
// DNSLink
23
- {"localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
47
+ {httpRequest, "localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
48
// Hostname with port
25
- {"localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
49
+ {httpRequest, "localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
50
// CIDv0 → CIDv1base32
27
- {"localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
51
+ {httpRequest, "localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
52
// CIDv1 with long sha512
29
- {"localhost", "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
53
+ {httpRequest, "localhost", "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
54
// PeerID as CIDv1 needs to have libp2p-key multicodec
31
- {"localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
32
- {"localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
55
+ {httpRequest, "localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
56
+ {httpRequest, "localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
57
// PeerID: ed25519+identity multihash → CIDv1Base36
34
- {"localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
35
- {"sub.localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
58
+ {httpRequest, "localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
59
+ {httpRequest, "sub.localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
60
+ // 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},
64
} {
37
- url, err := toSubdomainURL(test.hostname, test.path, r)
65
+ url, err := toSubdomainURL(test.gwHostname, test.path, test.request, coreAPI)
66
if url != test.url || !equalError(err, test.err) {
39
- t.Errorf("(%s, %s) returned (%s, %v), expected (%s, %v)", test.hostname, test.path, url, err, test.url, test.err)
67
+ t.Errorf("(%s, %s) returned (%s, %v), expected (%s, %v)", test.gwHostname, test.path, url, err, test.url, test.err)
68
+ }
69
+ }
70
+}
71
+
72
+func TestToDNSLinkDNSLabel(t *testing.T) {
73
+ for _, test := range []struct {
74
+ in string
75
+ out string
76
+ err error
77
+ }{
78
+ {"dnslink.long-name.example.com", "dnslink-long--name-example-com", nil},
79
+ {"dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com", "", errors.New("DNSLink representation incompatible with DNS label length limit of 63: dnslink-too--long-f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o-example-com")},
80
+ } {
81
+ out, err := toDNSLinkDNSLabel(test.in)
82
+ if out != test.out || !equalError(err, test.err) {
83
+ t.Errorf("(%s) returned (%s, %v), expected (%s, %v)", test.in, out, err, test.out, test.err)
84
+ }
85
+ }
86
+}
87
+
88
+func TestToDNSLinkFQDN(t *testing.T) {
89
+ for _, test := range []struct {
90
+ in string
91
+ out string
92
+ }{
93
+ {"singlelabel", "singlelabel"},
94
+ {"docs-ipfs-io", "docs.ipfs.io"},
95
+ {"dnslink-long--name-example-com", "dnslink.long-name.example.com"},
96
+ } {
97
+ out := toDNSLinkFQDN(test.in)
98
+ if out != test.out {
99
+ t.Errorf("(%s) returned (%s), expected (%s)", test.in, out, test.out)
100
+ }
101
+ }
102
+}
103
+
104
+func TestIsHTTPSRequest(t *testing.T) {
105
+ httpRequest := httptest.NewRequest("GET", "http://127.0.0.1:8080", nil)
106
+ httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil)
107
+ httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil)
108
+ httpsProxiedRequest.Header.Set("X-Forwarded-Proto", "https")
109
+ for _, test := range []struct {
110
+ in *http.Request
111
+ out bool
112
+ }{
113
+ {httpRequest, false},
114
+ {httpsRequest, true},
115
+ {httpsProxiedRequest, true},
116
+ } {
117
+ out := isHTTPSRequest(test.in)
118
+ if out != test.out {
119
+ t.Errorf("(%+v): returned %t, expected %t", test.in, out, test.out)
120
}
121
}
122
}
@@ -77,10 +157,9 @@ func TestPortStripping(t *testing.T) {
157
t.Errorf("(%s): returned '%s', expected '%s'", test.in, out, test.out)
158
}
159
}
80
-
160
}
161
83
-func TestDNSPrefix(t *testing.T) {
162
+func TestToDNSLabel(t *testing.T) {
163
for _, test := range []struct {
164
in string
165
out string
@@ -96,7 +175,7 @@ func TestDNSPrefix(t *testing.T) {
175
{"bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
176
} {
177
inCID, _ := cid.Decode(test.in)
99
- out, err := toDNSPrefix(test.in, inCID)
178
+ out, err := toDNSLabel(test.in, inCID)
179
if out != test.out || !equalError(err, test.err) {
180
t.Errorf("(%s): returned (%s, %v) expected (%s, %v)", test.in, out, err, test.out, test.err)
181
}
docs/config.md
+2
-1
@@ -709,8 +709,9 @@ Below is a list of the most common public gateway setups.
709
```
710
**Backward-compatible:** this feature enables automatic redirects from content paths to subdomains:
711
`http://dweb.link/ipfs/{cid}` → `http://{cid}.ipfs.dweb.link`
712
- **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";`.:
712
+ **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";`.:
713
`http://dweb.link/ipfs/{cid}` → `https://{cid}.ipfs.dweb.link`
714
+ `http://dweb.link/ipns/your-dnslink.site.example.com` → `https://your--dnslink-site-example-com.ipfs.dweb.link`
715
**X-Forwarded-Host:** we also support `X-Forwarded-Host: example.com` if you want to override subdomain gateway host from the original request:
716
`http://dweb.link/ipfs/{cid}` → `http://{cid}.ipfs.example.com`
717
test/sharness/t0114-gateway-subdomains.sh
+18
-2
@@ -403,6 +403,14 @@ test_hostname_gateway_response_should_contain \
403
"http://127.0.0.1:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
404
"Location: http://en.wikipedia-on-ipfs.org.ipns.example.com/wiki"
405
406
+# DNSLink on Public gateway with a single-level wildcard TLS cert
407
+# "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
408
+test_expect_success \
409
+ "request for example.com/ipns/{fqdn} with X-Forwarded-Proto redirects to TLS-safe label in subdomain" "
410
+ 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 &&
411
+ test_should_contain \"Location: https://en-wikipedia--on--ipfs-org.ipns.example.com/wiki\" response
412
+ "
413
+
414
# *.ipfs.example.com: subdomain requests made with custom FQDN in Host header
415
416
test_hostname_gateway_response_should_contain \
@@ -539,14 +547,22 @@ test_hostname_gateway_response_should_contain \
547
"http://127.0.0.1:$GWAY_PORT" \
548
"$CID_VAL"
549
550
+# DNSLink on Public gateway with a single-level wildcard TLS cert
551
+# "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
552
+test_expect_success \
553
+ "request for {single-label-dnslink}.ipns.example.com with X-Forwarded-Proto returns expected payload" "
554
+ 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 &&
555
+ test_should_contain \"$CID_VAL\" response
556
+ "
557
+
558
## Test subdomain handling of CIDs that do not fit in a single DNS Label (>63chars)
559
## https://github.com/ipfs/go-ipfs/issues/7318
560
## ============================================================================
561
562
# ed25519 fits under 63 char limit when represented in base36
563
IPNS_KEY="test_key_ed25519"
548
-IPNS_ED25519_B58MH=$(ipfs key list -l -f b58mh | grep $IPNS_KEY | cut -d " " -f1 | tr -d "\n")
549
-IPNS_ED25519_B36CID=$(ipfs key list -l -f b36cid | grep $IPNS_KEY | cut -d " " -f1 | tr -d "\n")
564
+IPNS_ED25519_B58MH=$(ipfs key list -l --ipns-base b58mh | grep $IPNS_KEY | cut -d" " -f1 | tr -d "\n")
565
+IPNS_ED25519_B36CID=$(ipfs key list -l --ipns-base base36 | grep $IPNS_KEY | cut -d" " -f1 | tr -d "\n")
566
# sha512 will be over 63char limit, even when represented in Base36
567
CIDv1_TOO_LONG=$(echo $CID_VAL | ipfs add --cid-version 1 --hash sha2-512 -Q)
568