@cryptotaxi247 / kubo / commits / 231fab811

feat: support ED25519 libp2p-key in subdomains

This: - adds subdomain gateway support for ED25519 CIDs in a way that fits in a single DNS label to enable TLS for every IPNS website. - cleans up subdomain redirect logic and adds more explicit error handling. TL;DR on router logic: When CID is longer than 63 characters, router at /ipfs/* and /ipns/* converts to Base36, and if that does not help, returns a human readable 400 Bad Request error. Addressing code review: https://github.com/ipfs/go-ipfs/pull/7441#pullrequestreview-440043209 refactor: use b36 for all libp2p-keys in subdomains Consensus reached in https://github.com/ipfs/go-ipfs/pull/7441#discussion_r452372828 https://github.com/ipfs/go-ipfs/pull/7441#discussion_r451477890 https://github.com/ipfs/go-ipfs/pull/7441#discussion_r452500272

Marcin Rataj committed May 25, 2020 at 16:15 UTC 231fab811d83322e61fe8d9c65d7bcd9fd6d243c
4 files changed +212 -48
core/corehttp/hostname.go
+105 -32
@@ -41,12 +41,15 @@ var defaultKnownGateways = map[string]config.GatewaySpec{
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 }
@@ -101,7 +104,12 @@ func HostnameOption() ServeOption {
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
@@ -131,7 +139,7 @@ func HostnameOption() ServeOption {
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)
@@ -158,16 +166,44 @@ func HostnameOption() ServeOption {
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
@@ -183,7 +219,7 @@ func HostnameOption() ServeOption {
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)
@@ -273,18 +309,38 @@ func isPeerIDNamespace(ns string) bool {
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
@@ -304,11 +360,11 @@ func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, ok
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
@@ -327,25 +383,42 @@ func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, ok
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
core/corehttp/hostname_test.go
+43 -11
@@ -1,9 +1,11 @@
1 package corehttp
2
3 import (
4 + "errors"
5 "net/http/httptest"
6 "testing"
7
8 + cid "github.com/ipfs/go-cid"
9 config "github.com/ipfs/go-ipfs-config"
10 )
11
@@ -15,23 +17,25 @@ func TestToSubdomainURL(t *testing.T) {
17 path string
18 // out:
19 url string
18 - ok bool
20 + err error
21 }{
22 // DNSLink
21 - {"localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", true},
23 + {"localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
24 // Hostname with port
23 - {"localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", true},
25 + {"localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
26 // CIDv0 → CIDv1base32
25 - {"localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", true},
27 + {"localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
28 + // CIDv1 with long sha512
29 + {"localhost", "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
30 // PeerID as CIDv1 needs to have libp2p-key multicodec
27 - {"localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://bafzbeieqhtl2l3mrszjnhv6hf2iloiitsx7mexiolcnywnbcrzkqxwslja.ipns.localhost/", true},
28 - {"localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm.ipns.localhost/", true},
29 - // PeerID: ed25519+identity multihash
30 - {"localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://bafzaajaiaejcat4yhiwnr2qz73mtu6vrnj2krxlpfoa3wo2pllfi37quorgwh2jw.ipns.localhost/", true},
31 + {"localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
32 + {"localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
33 + // PeerID: ed25519+identity multihash → CIDv1Base36
34 + {"localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
35 } {
32 - url, ok := toSubdomainURL(test.hostname, test.path, r)
33 - if ok != test.ok || url != test.url {
34 - t.Errorf("(%s, %s) returned (%s, %t), expected (%s, %t)", test.hostname, test.path, url, ok, test.url, ok)
36 + url, err := toSubdomainURL(test.hostname, test.path, r)
37 + if url != test.url || !equalError(err, test.err) {
38 + t.Errorf("(%s, %s) returned (%s, %v), expected (%s, %v)", test.hostname, test.path, url, err, test.url, test.err)
39 }
40 }
41 }
@@ -75,6 +79,30 @@ func TestPortStripping(t *testing.T) {
79
80 }
81
82 +func TestDNSPrefix(t *testing.T) {
83 + for _, test := range []struct {
84 + in string
85 + out string
86 + err error
87 + }{
88 + // <= 63
89 + {"QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", nil},
90 + {"bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", nil},
91 + // > 63
92 + // PeerID: ed25519+identity multihash → CIDv1Base36
93 + {"bafzaajaiaejca4syrpdu6gdx4wsdnokxkprgzxf4wrstuc34gxw5k5jrag2so5gk", "k51qzi5uqu5dj16qyiq0tajolkojyl9qdkr254920wxv7ghtuwcz593tp69z9m", nil},
94 + // CIDv1 with long sha512 → error
95 + {"bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
96 + } {
97 + inCID, _ := cid.Decode(test.in)
98 + out, err := toDNSPrefix(test.in, inCID)
99 + if out != test.out || !equalError(err, test.err) {
100 + t.Errorf("(%s): returned (%s, %v) expected (%s, %v)", test.in, out, err, test.out, test.err)
101 + }
102 + }
103 +
104 +}
105 +
106 func TestKnownSubdomainDetails(t *testing.T) {
107 gwSpec := config.GatewaySpec{
108 UseSubdomains: true,
@@ -150,3 +178,7 @@ func TestKnownSubdomainDetails(t *testing.T) {
178 }
179
180 }
181 +
182 +func equalError(a, b error) bool {
183 + return (a == nil && b == nil) || (a != nil && b != nil && a.Error() == b.Error())
184 +}
test/sharness/t0114-gateway-subdomains.sh
+63 -4
@@ -110,8 +110,8 @@ test_expect_success "Add the test directory" '
110 test_expect_success "Publish test text file to IPNS" '
111 PEERID=$(ipfs id --format="<id>")
112 IPNS_IDv0=$(echo "$PEERID" | ipfs cid format -v 0)
113 - IPNS_IDv1=$(echo "$PEERID" | ipfs cid format -v 1 --codec libp2p-key -b base32)
114 - IPNS_IDv1_DAGPB=$(echo "$IPNS_IDv0" | ipfs cid format -v 1 -b base32)
113 + IPNS_IDv1=$(echo "$PEERID" | ipfs cid format -v 1 --codec libp2p-key -b base36)
114 + IPNS_IDv1_DAGPB=$(echo "$IPNS_IDv0" | ipfs cid format -v 1 -b base36)
115 test_check_peerid "${PEERID}" &&
116 ipfs name publish --allow-offline -Q "/ipfs/$CIDv1" > name_publish_out &&
117 ipfs name resolve "$PEERID" > output &&
@@ -119,7 +119,6 @@ test_expect_success "Publish test text file to IPNS" '
119 test_cmp expected2 output
120 '
121
122 -
122 # ensure we start with empty Gateway.PublicGateways
123 test_expect_success 'start daemon with empty config for Gateway.PublicGateways' '
124 test_kill_ipfs_daemon &&
@@ -262,6 +261,7 @@ test_expect_success "request for deep path resource at {cid}.ipfs.localhost/sub/
261 test_should_contain "subdir2-bar" list_response
262 '
263
264 +
265 # *.ipns.localhost
266
267 # <libp2p-key>.ipns.localhost
@@ -480,6 +480,66 @@ test_hostname_gateway_response_should_contain \
480 "http://127.0.0.1:$GWAY_PORT" \
481 "$CID_VAL"
482
483 +## Test subdomain handling of CIDs that do not fit in a single DNS Label (>63chars)
484 +## https://github.com/ipfs/go-ipfs/issues/7318
485 +## ============================================================================
486 +
487 +# TODO: replace with cidv1
488 +# ed25519 fits under 63 char limit when represented in base36
489 +CIDv1_ED25519_RAW="12D3KooWP3ggTJV8LGckDHc4bVyXGhEWuBskoFyE6Rn2BJBqJtpa"
490 +CIDv1_ED25519_DNSSAFE="k51qzi5uqu5dl2yn0d6xu8q5aqa61jh8zeyixz9tsju80n15ssiyew48912c63"
491 +# sha512 will be over 63char limit, even when represented in Base36
492 +CIDv1_TOO_LONG=$(echo $CID_VAL | ipfs add --cid-version 1 --hash sha2-512 -Q)
493 +
494 +# local: *.localhost
495 +test_localhost_gateway_response_should_contain \
496 + "request for a ED25519 CID at localhost/ipfs/{CIDv1} returns Location HTTP header for DNS-safe subdomain redirect in browsers" \
497 + "http://localhost:$GWAY_PORT/ipns/$CIDv1_ED25519_RAW" \
498 + "Location: http://${CIDv1_ED25519_DNSSAFE}.ipns.localhost:$GWAY_PORT/"
499 +
500 +# router should not redirect to hostnames that could fail due to DNS limits
501 +test_localhost_gateway_response_should_contain \
502 + "request for a too long CID at localhost/ipfs/{CIDv1} returns human readable error" \
503 + "http://localhost:$GWAY_PORT/ipfs/$CIDv1_TOO_LONG" \
504 + "CID incompatible with DNS label length limit of 63"
505 +
506 +test_localhost_gateway_response_should_contain \
507 + "request for a too long CID at localhost/ipfs/{CIDv1} returns HTTP Error 400 Bad Request" \
508 + "http://localhost:$GWAY_PORT/ipfs/$CIDv1_TOO_LONG" \
509 + "400 Bad Request"
510 +
511 +# direct request should also fail (provides the same UX as router and avoids confusion)
512 +test_localhost_gateway_response_should_contain \
513 + "request for a too long CID at {CIDv1}.ipfs.localhost returns expected payload" \
514 + "http://$CIDv1_TOO_LONG.ipfs.localhost:$GWAY_PORT" \
515 + "400 Bad Request"
516 +
517 +# public subdomain gateway: *.example.com
518 +
519 +test_hostname_gateway_response_should_contain \
520 + "request for a ED25519 CID at example.com/ipfs/{CIDv1} returns Location HTTP header for DNS-safe subdomain redirect in browsers" \
521 + "example.com" \
522 + "http://127.0.0.1:$GWAY_PORT/ipns/$CIDv1_ED25519_RAW" \
523 + "Location: http://${CIDv1_ED25519_DNSSAFE}.ipns.example.com"
524 +
525 +test_hostname_gateway_response_should_contain \
526 + "request for a too long CID at example.com/ipfs/{CIDv1} returns human readable error" \
527 + "example.com" \
528 + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1_TOO_LONG" \
529 + "CID incompatible with DNS label length limit of 63"
530 +
531 +test_hostname_gateway_response_should_contain \
532 + "request for a too long CID at example.com/ipfs/{CIDv1} returns HTTP Error 400 Bad Request" \
533 + "example.com" \
534 + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1_TOO_LONG" \
535 + "400 Bad Request"
536 +
537 +test_hostname_gateway_response_should_contain \
538 + "request for a too long CID at {CIDv1}.ipfs.example.com returns HTTP Error 400 Bad Request" \
539 + "$CIDv1_TOO_LONG.ipfs.example.com" \
540 + "http://127.0.0.1:$GWAY_PORT/" \
541 + "400 Bad Request"
542 +
543 # Disable selected Paths for the subdomain gateway hostname
544 # =============================================================================
545
@@ -501,7 +561,6 @@ test_hostname_gateway_response_should_contain \
561 "http://127.0.0.1:$GWAY_PORT" \
562 "404 Not Found"
563
504 -
564 ## ============================================================================
565 ## Test path-based requests with a custom hostname config
566 ## ============================================================================
test/sharness/t0184-http-proxy-over-p2p.sh
+1 -1
@@ -216,7 +216,7 @@ test_expect_success 'handle multipart/form-data http request' '
216 '
217
218 # subdomain gateway at *.p2p.example.com requires PeerdID in base32
219 -RECEIVER_ID_CIDv1=$( ipfs cid format -v 1 -b b --codec libp2p-key -- $RECEIVER_ID)
219 +RECEIVER_ID_CIDv1=$( ipfs cid format -v 1 --codec libp2p-key -b base36 -- $RECEIVER_ID)
220
221 # OK: $peerid.p2p.example.com/http/index.txt
222 test_expect_success "handle http request to a subdomain gateway" '