@cryptotaxi247 / kubo / commits / 13e6bcfb4

feat: wildcard support for public gateways

Add support for one or more wildcards in the hostname definition of a public gateway. This is useful for example to support easily multiples environment. Wildcarded hostname are set in the config as for example "*.domain.tld".

Michael Muré committed May 14, 2020 at 20:35 UTC 13e6bcfb4f7d2db7d80af77f1b32b2394abca9da
4 files changed +213 -60
core/corehttp/hostname.go
+74 -24
@@ -6,6 +6,7 @@ import (
6 "net"
7 "net/http"
8 "net/url"
9 + "regexp"
10 "strings"
11
12 cid "github.com/ipfs/go-cid"
@@ -24,17 +25,17 @@ import (
25
26 var defaultPaths = []string{"/ipfs/", "/ipns/", "/api/", "/p2p/", "/version"}
27
27 -var pathGatewaySpec = config.GatewaySpec{
28 +var pathGatewaySpec = &config.GatewaySpec{
29 Paths: defaultPaths,
30 UseSubdomains: false,
31 }
32
32 -var subdomainGatewaySpec = config.GatewaySpec{
33 +var subdomainGatewaySpec = &config.GatewaySpec{
34 Paths: defaultPaths,
35 UseSubdomains: true,
36 }
37
37 -var defaultKnownGateways = map[string]config.GatewaySpec{
38 +var defaultKnownGateways = map[string]*config.GatewaySpec{
39 "localhost": subdomainGatewaySpec,
40 "ipfs.io": pathGatewaySpec,
41 "gateway.ipfs.io": pathGatewaySpec,
@@ -58,22 +59,8 @@ func HostnameOption() ServeOption {
59 if err != nil {
60 return nil, err
61 }
61 - knownGateways := make(
62 - map[string]config.GatewaySpec,
63 - len(defaultKnownGateways)+len(cfg.Gateway.PublicGateways),
64 - )
65 - for hostname, gw := range defaultKnownGateways {
66 - knownGateways[hostname] = gw
67 - }
68 - for hostname, gw := range cfg.Gateway.PublicGateways {
69 - if gw == nil {
70 - // Allows the user to remove gateways but _also_
71 - // allows us to continuously update the list.
72 - delete(knownGateways, hostname)
73 - } else {
74 - knownGateways[hostname] = *gw
75 - }
76 - }
62 +
63 + knownGateways := prepareKnownGateways(cfg.Gateway.PublicGateways)
64
65 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
66 // Unfortunately, many (well, ipfs.io) gateways use
@@ -233,22 +220,85 @@ func HostnameOption() ServeOption {
220 }
221 }
222
223 +type gatewayHosts struct {
224 + exact map[string]*config.GatewaySpec
225 + wildcard []wildcardHost
226 +}
227 +
228 +type wildcardHost struct {
229 + re *regexp.Regexp
230 + spec *config.GatewaySpec
231 +}
232 +
233 +func prepareKnownGateways(publicGateways map[string]*config.GatewaySpec) gatewayHosts {
234 + var hosts gatewayHosts
235 +
236 + if len(publicGateways) == 0 {
237 + hosts.exact = make(
238 + map[string]*config.GatewaySpec,
239 + len(defaultKnownGateways),
240 + )
241 + for hostname, gw := range defaultKnownGateways {
242 + hosts.exact[hostname] = gw
243 + }
244 + return hosts
245 + }
246 +
247 + hosts.exact = make(map[string]*config.GatewaySpec, len(publicGateways))
248 +
249 + for hostname, gw := range publicGateways {
250 + if gw == nil {
251 + continue
252 + }
253 + if strings.Contains(hostname, "*") {
254 + // from *.domain.tld, construct a regexp that match any direct subdomain
255 + // of .domain.tld.
256 + //
257 + // Regexp will be in the form of ^[^.]+\.domain.tld(?::\d+)?$
258 +
259 + escaped := strings.ReplaceAll(hostname, ".", `\.`)
260 + regexed := strings.ReplaceAll(escaped, "*", "[^.]+")
261 +
262 + re, err := regexp.Compile(fmt.Sprintf(`^%s(?::\d+)?$`, regexed))
263 + if err != nil {
264 + log.Warn("invalid wildcard gateway hostname \"%s\"", hostname)
265 + }
266 +
267 + hosts.wildcard = append(hosts.wildcard, wildcardHost{re: re, spec: gw})
268 + } else {
269 + hosts.exact[hostname] = gw
270 + }
271 + }
272 +
273 + return hosts
274 +}
275 +
276 // isKnownHostname checks Gateway.PublicGateways and returns matching
277 // GatewaySpec with gracefull fallback to version without port
238 -func isKnownHostname(hostname string, knownGateways map[string]config.GatewaySpec) (gw config.GatewaySpec, ok bool) {
278 +func isKnownHostname(hostname string, knownGateways gatewayHosts) (gw *config.GatewaySpec, ok bool) {
279 // Try hostname (host+optional port - value from Host header as-is)
240 - if gw, ok := knownGateways[hostname]; ok {
280 + if gw, ok := knownGateways.exact[hostname]; ok {
281 + return gw, ok
282 + }
283 + // Also test without port
284 + if gw, ok = knownGateways.exact[stripPort(hostname)]; ok {
285 return gw, ok
286 }
243 - // Fallback to hostname without port
244 - gw, ok = knownGateways[stripPort(hostname)]
287 +
288 + // Wildcard support. Test both with and without port.
289 + for _, host := range knownGateways.wildcard {
290 + if host.re.MatchString(hostname) {
291 + return host.spec, true
292 + }
293 + }
294 +
295 return gw, ok
296 }
297
298 // Parses Host header and looks for a known subdomain gateway host.
299 // If found, returns GatewaySpec and subdomain components.
300 // Note: hostname is host + optional port
251 -func knownSubdomainDetails(hostname string, knownGateways map[string]config.GatewaySpec) (gw config.GatewaySpec, knownHostname, ns, rootID string, ok bool) {
301 +func knownSubdomainDetails(hostname string, knownGateways gatewayHosts) (gw *config.GatewaySpec, knownHostname, ns, rootID string, ok bool) {
302 labels := strings.Split(hostname, ".")
303 // Look for FQDN of a known gateway hostname.
304 // Example: given "dist.ipfs.io.ipns.dweb.link":
core/corehttp/hostname_test.go
+50 -36
@@ -32,6 +32,7 @@ func TestToSubdomainURL(t *testing.T) {
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 + {"sub.localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
36 } {
37 url, err := toSubdomainURL(test.hostname, test.path, r)
38 if url != test.url || !equalError(err, test.err) {
@@ -104,60 +105,73 @@ func TestDNSPrefix(t *testing.T) {
105 }
106
107 func TestKnownSubdomainDetails(t *testing.T) {
107 - gwSpec := config.GatewaySpec{
108 - UseSubdomains: true,
109 - }
110 - knownGateways := map[string]config.GatewaySpec{
111 - "localhost": gwSpec,
112 - "dweb.link": gwSpec,
113 - "dweb.ipfs.pvt.k12.ma.us": gwSpec, // note the sneaky ".ipfs." ;-)
114 - }
108 + gwLocalhost := &config.GatewaySpec{}
109 + gwDweb := &config.GatewaySpec{}
110 + gwLong := &config.GatewaySpec{}
111 + gwWildcard1 := &config.GatewaySpec{}
112 + gwWildcard2 := &config.GatewaySpec{}
113 +
114 + knownGateways := prepareKnownGateways(map[string]*config.GatewaySpec{
115 + "localhost": gwLocalhost,
116 + "dweb.link": gwDweb,
117 + "dweb.ipfs.pvt.k12.ma.us": gwLong, // note the sneaky ".ipfs." ;-)
118 + "*.wildcard1.tld": gwWildcard1,
119 + "*.*.wildcard2.tld": gwWildcard2,
120 + })
121
122 for _, test := range []struct {
123 // in:
124 hostHeader string
125 // out:
126 + gw *config.GatewaySpec
127 hostname string
128 ns string
129 rootID string
130 ok bool
131 }{
132 // no subdomain
126 - {"127.0.0.1:8080", "", "", "", false},
127 - {"[::1]:8080", "", "", "", false},
128 - {"hey.look.example.com", "", "", "", false},
129 - {"dweb.link", "", "", "", false},
133 + {"127.0.0.1:8080", nil, "", "", "", false},
134 + {"[::1]:8080", nil, "", "", "", false},
135 + {"hey.look.example.com", nil, "", "", "", false},
136 + {"dweb.link", nil, "", "", "", false},
137 // malformed Host header
131 - {".....dweb.link", "", "", "", false},
132 - {"link", "", "", "", false},
133 - {"8080:dweb.link", "", "", "", false},
134 - {" ", "", "", "", false},
135 - {"", "", "", "", false},
138 + {".....dweb.link", nil, "", "", "", false},
139 + {"link", nil, "", "", "", false},
140 + {"8080:dweb.link", nil, "", "", "", false},
141 + {" ", nil, "", "", "", false},
142 + {"", nil, "", "", "", false},
143 // unknown gateway host
137 - {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.unknown.example.com", "", "", "", false},
144 + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.unknown.example.com", nil, "", "", "", false},
145 // cid in subdomain, known gateway
139 - {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost:8080", "localhost:8080", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
140 - {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.link", "dweb.link", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
146 + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost:8080", gwLocalhost, "localhost:8080", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
147 + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.link", gwDweb, "dweb.link", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
148 // capture everything before .ipfs.
142 - {"foo.bar.boo-buzz.ipfs.dweb.link", "dweb.link", "ipfs", "foo.bar.boo-buzz", true},
149 + {"foo.bar.boo-buzz.ipfs.dweb.link", gwDweb, "dweb.link", "ipfs", "foo.bar.boo-buzz", true},
150 // ipns
144 - {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.localhost:8080", "localhost:8080", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
145 - {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.link", "dweb.link", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
151 + {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.localhost:8080", gwLocalhost, "localhost:8080", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
152 + {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.link", gwDweb, "dweb.link", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
153 // edge case check: public gateway under long TLD (see: https://publicsuffix.org)
147 - {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
148 - {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
154 + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.ipfs.pvt.k12.ma.us", gwLong, "dweb.ipfs.pvt.k12.ma.us", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
155 + {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.ipfs.pvt.k12.ma.us", gwLong, "dweb.ipfs.pvt.k12.ma.us", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
156 // dnslink in subdomain
150 - {"en.wikipedia-on-ipfs.org.ipns.localhost:8080", "localhost:8080", "ipns", "en.wikipedia-on-ipfs.org", true},
151 - {"en.wikipedia-on-ipfs.org.ipns.localhost", "localhost", "ipns", "en.wikipedia-on-ipfs.org", true},
152 - {"dist.ipfs.io.ipns.localhost:8080", "localhost:8080", "ipns", "dist.ipfs.io", true},
153 - {"en.wikipedia-on-ipfs.org.ipns.dweb.link", "dweb.link", "ipns", "en.wikipedia-on-ipfs.org", true},
157 + {"en.wikipedia-on-ipfs.org.ipns.localhost:8080", gwLocalhost, "localhost:8080", "ipns", "en.wikipedia-on-ipfs.org", true},
158 + {"en.wikipedia-on-ipfs.org.ipns.localhost", gwLocalhost, "localhost", "ipns", "en.wikipedia-on-ipfs.org", true},
159 + {"dist.ipfs.io.ipns.localhost:8080", gwLocalhost, "localhost:8080", "ipns", "dist.ipfs.io", true},
160 + {"en.wikipedia-on-ipfs.org.ipns.dweb.link", gwDweb, "dweb.link", "ipns", "en.wikipedia-on-ipfs.org", true},
161 // edge case check: public gateway under long TLD (see: https://publicsuffix.org)
155 - {"foo.dweb.ipfs.pvt.k12.ma.us", "", "", "", false},
156 - {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
157 - {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
162 + {"foo.dweb.ipfs.pvt.k12.ma.us", nil, "", "", "", false},
163 + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.ipfs.pvt.k12.ma.us", gwLong, "dweb.ipfs.pvt.k12.ma.us", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
164 + {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.ipfs.pvt.k12.ma.us", gwLong, "dweb.ipfs.pvt.k12.ma.us", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
165 // other namespaces
159 - {"api.localhost", "", "", "", false},
160 - {"peerid.p2p.localhost", "localhost", "p2p", "peerid", true},
166 + {"api.localhost", nil, "", "", "", false},
167 + {"peerid.p2p.localhost", gwLocalhost, "localhost", "p2p", "peerid", true},
168 + // wildcards
169 + {"wildcard1.tld", nil, "", "", "", false},
170 + {".wildcard1.tld", nil, "", "", "", false},
171 + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.wildcard1.tld", nil, "", "", "", false},
172 + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.sub.wildcard1.tld", gwWildcard1, "sub.wildcard1.tld", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
173 + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.sub1.sub2.wildcard1.tld", nil, "", "", "", false},
174 + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.sub1.sub2.wildcard2.tld", gwWildcard2, "sub1.sub2.wildcard2.tld", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
175 } {
176 gw, hostname, ns, rootID, ok := knownSubdomainDetails(test.hostHeader, knownGateways)
177 if ok != test.ok {
@@ -172,8 +186,8 @@ func TestKnownSubdomainDetails(t *testing.T) {
186 if hostname != test.hostname {
187 t.Errorf("knownSubdomainDetails(%s): hostname is '%s', expected '%s'", test.hostHeader, hostname, test.hostname)
188 }
175 - if ok && gw.UseSubdomains != gwSpec.UseSubdomains {
176 - t.Errorf("knownSubdomainDetails(%s): gw is %+v, expected %+v", test.hostHeader, gw, gwSpec)
189 + if gw != test.gw {
190 + t.Errorf("knownSubdomainDetails(%s): gw is %+v, expected %+v", test.hostHeader, gw, test.gw)
191 }
192 }
193
docs/config.md
+6
@@ -586,6 +586,12 @@ Type: `array[string]`
586
587 `PublicGateways` is a dictionary for defining gateway behavior on specified hostnames.
588
589 +Hostnames can optionally be defined with one or more wildcards.
590 +
591 +Examples:
592 +- `*.example.com` will match requests to `http://foo.example.com/ipfs/*` or `http://{cid}.ipfs.bar.example.com/*`.
593 +- `foo-*.example.com` will match requests to `http://foo-bar.example.com/ipfs/*` or `http://{cid}.ipfs.foo-xyz.example.com/*`.
594 +
595 #### `Gateway.PublicGateways: Paths`
596
597 Array of paths that should be exposed on the hostname.
test/sharness/t0114-gateway-subdomains.sh
+83
@@ -821,6 +821,89 @@ test_expect_success "request for http://fake.domain.com/ipfs/{CID} with X-Forwar
821 test_should_contain \"Location: https://$CIDv1.ipfs.example.com/\" response
822 "
823
824 +## ============================================================================
825 +## Test support for wildcards in gateway config
826 +## ============================================================================
827 +
828 +# set explicit subdomain gateway config for the hostnames
829 +ipfs config --json Gateway.PublicGateways '{
830 + "*.example1.com": {
831 + "UseSubdomains": true,
832 + "Paths": ["/ipfs"]
833 + },
834 + "*.*.example2.com": {
835 + "UseSubdomains": true,
836 + "Paths": ["/ipfs"]
837 + },
838 + "foo.*.example3.com": {
839 + "UseSubdomains": true,
840 + "Paths": ["/ipfs"]
841 + },
842 + "foo.bar-*-boo.example4.com": {
843 + "UseSubdomains": true,
844 + "Paths": ["/ipfs"]
845 + }
846 +}' || exit 1
847 +# restart daemon to apply config changes
848 +test_kill_ipfs_daemon
849 +test_launch_ipfs_daemon --offline
850 +
851 +# *.example1.com
852 +
853 +test_hostname_gateway_response_should_contain \
854 + "request for foo.example1.com/ipfs/{CIDv1} produces redirect to {CIDv1}.ipfs.foo.example1.com" \
855 + "foo.example1.com" \
856 + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
857 + "Location: http://$CIDv1.ipfs.foo.example1.com/"
858 +
859 +test_hostname_gateway_response_should_contain \
860 + "request for {CID}.ipfs.foo.example1.com should return expected payload" \
861 + "${CIDv1}.ipfs.foo.example1.com" \
862 + "http://127.0.0.1:$GWAY_PORT/" \
863 + "$CID_VAL"
864 +
865 +# *.*.example2.com
866 +
867 +test_hostname_gateway_response_should_contain \
868 + "request for foo.bar.example2.com/ipfs/{CIDv1} produces redirect to {CIDv1}.ipfs.foo.bar.example2.com" \
869 + "foo.bar.example2.com" \
870 + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
871 + "Location: http://$CIDv1.ipfs.foo.bar.example2.com/"
872 +
873 +test_hostname_gateway_response_should_contain \
874 + "request for {CID}.ipfs.foo.bar.example2.com should return expected payload" \
875 + "${CIDv1}.ipfs.foo.bar.example2.com" \
876 + "http://127.0.0.1:$GWAY_PORT/" \
877 + "$CID_VAL"
878 +
879 +# foo.*.example3.com
880 +
881 +test_hostname_gateway_response_should_contain \
882 + "request for foo.bar.example3.com/ipfs/{CIDv1} produces redirect to {CIDv1}.ipfs.foo.bar.example3.com" \
883 + "foo.bar.example3.com" \
884 + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
885 + "Location: http://$CIDv1.ipfs.foo.bar.example3.com/"
886 +
887 +test_hostname_gateway_response_should_contain \
888 + "request for {CID}.ipfs.foo.bar.example3.com should return expected payload" \
889 + "${CIDv1}.ipfs.foo.bar.example3.com" \
890 + "http://127.0.0.1:$GWAY_PORT/" \
891 + "$CID_VAL"
892 +
893 +# foo.bar-*-boo.example4.com
894 +
895 +test_hostname_gateway_response_should_contain \
896 + "request for foo.bar-dev-boo.example4.com/ipfs/{CIDv1} produces redirect to {CIDv1}.ipfs.foo.bar-dev-boo.example4.com" \
897 + "foo.bar-dev-boo.example4.com" \
898 + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
899 + "Location: http://$CIDv1.ipfs.foo.bar-dev-boo.example4.com/"
900 +
901 +test_hostname_gateway_response_should_contain \
902 + "request for {CID}.ipfs.foo.bar-dev-boo.example4.com should return expected payload" \
903 + "${CIDv1}.ipfs.foo.bar-dev-boo.example4.com" \
904 + "http://127.0.0.1:$GWAY_PORT/" \
905 + "$CID_VAL"
906 +
907 # =============================================================================
908 # ensure we end with empty Gateway.PublicGateways
909 ipfs config --json Gateway.PublicGateways '{}'