41
"dweb.link": subdomainGatewaySpec,
42
}
43
44
+// Label's max length in DNS (https://tools.ietf.org/html/rfc1034#page-7)
45
+const dnsLabelMaxLength int = 63
46
+
47
// HostnameOption rewrites an incoming request based on the Host header.
48
func HostnameOption() ServeOption {
49
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
50
childMux := http.NewServeMux()
51
49
- coreApi, err := coreapi.NewCoreAPI(n)
52
+ coreAPI, err := coreapi.NewCoreAPI(n)
53
if err != nil {
54
return nil, err
55
}
104
if gw.UseSubdomains {
105
// Yes, redirect if applicable
106
// Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link
104
- if newURL, ok := toSubdomainURL(host, r.URL.Path, r); ok {
107
+ newURL, err := toSubdomainURL(host, r.URL.Path, r)
108
+ if err != nil {
109
+ http.Error(w, err.Error(), http.StatusBadRequest)
110
+ return
111
+ }
112
+ if newURL != "" {
113
// Just to be sure single Origin can't be abused in
114
// web browsers that ignored the redirect for some
115
// reason, Clear-Site-Data header clears browsing
139
// Not a whitelisted path
140
141
// Try DNSLink, if it was not explicitly disabled for the hostname
134
- if !gw.NoDNSLink && isDNSLinkRequest(r.Context(), coreApi, host) {
142
+ if !gw.NoDNSLink && isDNSLinkRequest(r.Context(), coreAPI, host) {
143
// rewrite path and handle as DNSLink
144
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
145
childMux.ServeHTTP(w, r)
166
return
167
}
168
161
- // Do we need to fix multicodec in PeerID represented as CIDv1?
162
- if isPeerIDNamespace(ns) {
163
- keyCid, err := cid.Decode(rootID)
164
- if err == nil && keyCid.Type() != cid.Libp2pKey {
165
- if newURL, ok := toSubdomainURL(hostname, pathPrefix+r.URL.Path, r); ok {
166
- // Redirect to CID fixed inside of toSubdomainURL()
169
+ // Check if rootID is a valid CID
170
+ if rootCID, err := cid.Decode(rootID); err == nil {
171
+ // Do we need to redirect root CID to a canonical DNS representation?
172
+ dnsCID, err := toDNSPrefix(rootID, rootCID)
173
+ if err != nil {
174
+ http.Error(w, err.Error(), http.StatusBadRequest)
175
+ return
176
+ }
177
+ if !strings.HasPrefix(r.Host, dnsCID) {
178
+ dnsPrefix := "/" + ns + "/" + dnsCID
179
+ newURL, err := toSubdomainURL(hostname, dnsPrefix+r.URL.Path, r)
180
+ if err != nil {
181
+ http.Error(w, err.Error(), http.StatusBadRequest)
182
+ return
183
+ }
184
+ if newURL != "" {
185
+ // Redirect to deterministic CID to ensure CID
186
+ // always gets the same Origin on the web
187
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
188
return
189
}
190
}
191
+
192
+ // Do we need to fix multicodec in PeerID represented as CIDv1?
193
+ if isPeerIDNamespace(ns) {
194
+ if rootCID.Type() != cid.Libp2pKey {
195
+ newURL, err := toSubdomainURL(hostname, pathPrefix+r.URL.Path, r)
196
+ if err != nil {
197
+ http.Error(w, err.Error(), http.StatusBadRequest)
198
+ return
199
+ }
200
+ if newURL != "" {
201
+ // Redirect to CID fixed inside of toSubdomainURL()
202
+ http.Redirect(w, r, newURL, http.StatusMovedPermanently)
203
+ return
204
+ }
205
+ }
206
+ }
207
}
208
209
// Rewrite the path to not use subdomains
219
// 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)?
220
// 2. does Host header include a fully qualified domain name (FQDN)?
221
// 3. does DNSLink record exist in DNS?
186
- if !cfg.Gateway.NoDNSLink && isDNSLinkRequest(r.Context(), coreApi, host) {
222
+ if !cfg.Gateway.NoDNSLink && isDNSLinkRequest(r.Context(), coreAPI, host) {
223
// rewrite path and handle as DNSLink
224
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
225
childMux.ServeHTTP(w, r)
309
}
310
}
311
312
+// Converts an identifier to DNS-safe representation that fits in 63 characters
313
+func toDNSPrefix(rootID string, rootCID cid.Cid) (prefix string, err error) {
314
+ // Return as-is if things fit
315
+ if len(rootID) <= dnsLabelMaxLength {
316
+ return rootID, nil
317
+ }
318
+
319
+ // Convert to Base36 and see if that helped
320
+ rootID, err = cid.NewCidV1(rootCID.Type(), rootCID.Hash()).StringOfBase(mbase.Base36)
321
+ if err != nil {
322
+ return "", err
323
+ }
324
+ if len(rootID) <= dnsLabelMaxLength {
325
+ return rootID, nil
326
+ }
327
+
328
+ // Can't win with DNS at this point, return error
329
+ return "", fmt.Errorf("CID incompatible with DNS label length limit of 63: %s", rootID)
330
+}
331
+
332
// Converts a hostname/path to a subdomain-based URL, if applicable.
277
-func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, ok bool) {
333
+func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, err error) {
334
var scheme, ns, rootID, rest string
335
336
query := r.URL.RawQuery
337
parts := strings.SplitN(path, "/", 4)
282
- safeRedirectURL := func(in string) (out string, ok bool) {
338
+ safeRedirectURL := func(in string) (out string, err error) {
339
safeURI, err := url.ParseRequestURI(in)
340
if err != nil {
285
- return "", false
341
+ return "", err
342
}
287
- return safeURI.String(), true
343
+ return safeURI.String(), nil
344
}
345
346
// Support X-Forwarded-Proto if added by a reverse proxy
360
ns = parts[1]
361
rootID = parts[2]
362
default:
307
- return "", false
363
+ return "", nil
364
}
365
366
if !isSubdomainNamespace(ns) {
311
- return "", false
367
+ return "", nil
368
}
369
370
// add prefix if query is present
383
}
384
385
// If rootID is a CID, ensure it uses DNS-friendly text representation
330
- if rootCid, err := cid.Decode(rootID); err == nil {
331
- multicodec := rootCid.Type()
332
-
333
- // PeerIDs represented as CIDv1 are expected to have libp2p-key
334
- // multicodec (https://github.com/libp2p/specs/pull/209).
335
- // We ease the transition by fixing multicodec on the fly:
336
- // https://github.com/ipfs/go-ipfs/issues/5287#issuecomment-492163929
337
- if isPeerIDNamespace(ns) && multicodec != cid.Libp2pKey {
338
- multicodec = cid.Libp2pKey
386
+ if rootCID, err := cid.Decode(rootID); err == nil {
387
+ multicodec := rootCID.Type()
388
+ var base mbase.Encoding = mbase.Base32
389
+
390
+ // Normalizations specific to /ipns/{libp2p-key}
391
+ if isPeerIDNamespace(ns) {
392
+ // Using Base36 for /ipns/ for consistency
393
+ // Context: https://github.com/ipfs/go-ipfs/pull/7441#discussion_r452372828
394
+ base = mbase.Base36
395
+
396
+ // PeerIDs represented as CIDv1 are expected to have libp2p-key
397
+ // multicodec (https://github.com/libp2p/specs/pull/209).
398
+ // We ease the transition by fixing multicodec on the fly:
399
+ // https://github.com/ipfs/go-ipfs/issues/5287#issuecomment-492163929
400
+ if multicodec != cid.Libp2pKey {
401
+ multicodec = cid.Libp2pKey
402
+ }
403
}
404
341
- // if object turns out to be a valid CID,
342
- // ensure text representation used in subdomain is CIDv1 in Base32
343
- // https://github.com/ipfs/in-web-browsers/issues/89
344
- rootID, err = cid.NewCidV1(multicodec, rootCid.Hash()).StringOfBase(mbase.Base32)
405
+ // Ensure CID text representation used in subdomain is compatible
406
+ // with the way DNS and URIs are implemented in user agents.
407
+ //
408
+ // 1. Switch to CIDv1 and enable case-insensitive Base encoding
409
+ // to avoid issues when user agent force-lowercases the hostname
410
+ // before making the request
411
+ // (https://github.com/ipfs/in-web-browsers/issues/89)
412
+ rootCID = cid.NewCidV1(multicodec, rootCID.Hash())
413
+ rootID, err = rootCID.StringOfBase(base)
414
+ if err != nil {
415
+ return "", err
416
+ }
417
+ // 2. Make sure CID fits in a DNS label, adjust encoding if needed
418
+ // (https://github.com/ipfs/go-ipfs/issues/7318)
419
+ rootID, err = toDNSPrefix(rootID, rootCID)
420
if err != nil {
346
- // should not error, but if it does, its clealy not possible to
347
- // produce a subdomain URL
348
- return "", false
421
+ return "", err
422
}
423
}
424