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 '{}'