feat(gateway): _redirects file support (#8890)
https://github.com/ipfs/kubo/pull/8890 https://github.com/ipfs/specs/pull/290
Justin Johnson committed
Sep 23, 2022 at 11:44 UTC
bcaacdd6c3168392c2e2673e5a35e8a8387edbbc
10 files changed
+593
-93
core/coreapi/name.go
+3
-1
@@ -37,6 +37,8 @@ func (e *ipnsEntry) Value() path.Path {
37
return e.value
38
}
39
40
+type requestContextKey string
41
+
42
// Publish announces new IPNS name and returns the new IPNS entry.
43
func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.NamePublishOption) (coreiface.IpnsEntry, error) {
44
ctx, span := tracing.Span(ctx, "CoreAPI.NameAPI", "Publish", trace.WithAttributes(attribute.String("path", p.String())))
@@ -76,7 +78,7 @@ func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.Nam
78
79
if options.TTL != nil {
80
// nolint: staticcheck // non-backward compatible change
79
- ctx = context.WithValue(ctx, "ipns-publish-ttl", *options.TTL)
81
+ ctx = context.WithValue(ctx, requestContextKey("ipns-publish-ttl"), *options.TTL)
82
}
83
84
eol := time.Now().Add(options.ValidTime)
core/corehttp/gateway_handler.go
+47
-89
@@ -13,7 +13,6 @@ import (
13
gopath "path"
14
"regexp"
15
"runtime/debug"
16
- "strconv"
16
"strings"
17
"time"
18
@@ -378,23 +377,6 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
377
return
378
}
379
381
- // Resolve path to the final DAG node for the ETag
382
- resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath)
383
- switch err {
384
- case nil:
385
- case coreiface.ErrOffline:
386
- webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable)
387
- return
388
- default:
389
- // if Accept is text/html, see if ipfs-404.html is present
390
- if i.servePretty404IfPresent(w, r, contentPath) {
391
- logger.Debugw("serve pretty 404 if present")
392
- return
393
- }
394
- webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusBadRequest)
395
- return
396
- }
397
-
380
// Detect when explicit Accept header or ?format parameter are present
381
responseFormat, formatParams, err := customResponseFormat(r)
382
if err != nil {
@@ -402,6 +384,11 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
384
return
385
}
386
trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResponseFormat", responseFormat))
387
+
388
+ resolvedPath, contentPath, ok := i.handlePathResolution(w, r, responseFormat, contentPath, logger)
389
+ if !ok {
390
+ return
391
+ }
392
trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResolvedPath", resolvedPath.String()))
393
394
// Detect when If-None-Match HTTP header allows returning HTTP 304 Not Modified
@@ -450,36 +437,6 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
437
}
438
}
439
453
-func (i *gatewayHandler) servePretty404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool {
454
- resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath)
455
- if err != nil {
456
- return false
457
- }
458
-
459
- dr, err := i.api.Unixfs().Get(r.Context(), resolved404Path)
460
- if err != nil {
461
- return false
462
- }
463
- defer dr.Close()
464
-
465
- f, ok := dr.(files.File)
466
- if !ok {
467
- return false
468
- }
469
-
470
- size, err := f.Size()
471
- if err != nil {
472
- return false
473
- }
474
-
475
- log.Debugw("using pretty 404 file", "path", contentPath)
476
- w.Header().Set("Content-Type", ctype)
477
- w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
478
- w.WriteHeader(http.StatusNotFound)
479
- _, err = io.CopyN(w, f, size)
480
- return err == nil
481
-}
482
-
440
func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) {
441
p, err := i.api.Unixfs().Add(r.Context(), files.NewReaderFile(r.Body))
442
if err != nil {
@@ -920,55 +877,56 @@ func customResponseFormat(r *http.Request) (mediaType string, params map[string]
877
return "", nil, nil
878
}
879
923
-func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) {
924
- filename404, ctype, err := preferred404Filename(r.Header.Values("Accept"))
925
- if err != nil {
926
- return nil, "", err
880
+// returns unquoted path with all special characters revealed as \u codes
881
+func debugStr(path string) string {
882
+ q := fmt.Sprintf("%+q", path)
883
+ if len(q) >= 3 {
884
+ q = q[1 : len(q)-1]
885
}
886
+ return q
887
+}
888
929
- pathComponents := strings.Split(contentPath.String(), "/")
930
-
931
- for idx := len(pathComponents); idx >= 3; idx-- {
932
- pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...)
933
- parsed404Path := ipath.New("/" + pretty404)
934
- if parsed404Path.IsValid() != nil {
935
- break
936
- }
937
- resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path)
938
- if err != nil {
939
- continue
940
- }
941
- return resolvedPath, ctype, nil
942
- }
889
+// Resolve the provided contentPath including any special handling related to
890
+// the requested responseFormat. Returned ok flag indicates if gateway handler
891
+// should continue processing the request.
892
+func (i *gatewayHandler) handlePathResolution(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipath.Path, logger *zap.SugaredLogger) (resolvedPath ipath.Resolved, newContentPath ipath.Path, ok bool) {
893
+ // Attempt to resolve the provided path.
894
+ resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath)
895
944
- return nil, "", fmt.Errorf("no pretty 404 in any parent folder")
945
-}
896
+ switch err {
897
+ case nil:
898
+ return resolvedPath, contentPath, true
899
+ case coreiface.ErrOffline:
900
+ webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable)
901
+ return nil, nil, false
902
+ default:
903
+ // The path can't be resolved.
904
+ if isUnixfsResponseFormat(responseFormat) {
905
+ // If we have origin isolation (subdomain gw, DNSLink website),
906
+ // and response type is UnixFS (default for website hosting)
907
+ // check for presence of _redirects file and apply rules defined there.
908
+ // See: https://github.com/ipfs/specs/pull/290
909
+ if hasOriginIsolation(r) {
910
+ resolvedPath, newContentPath, ok, hadMatchingRule := i.serveRedirectsIfPresent(w, r, resolvedPath, contentPath, logger)
911
+ if hadMatchingRule {
912
+ logger.Debugw("applied a rule from _redirects file")
913
+ return resolvedPath, newContentPath, ok
914
+ }
915
+ }
916
947
-func preferred404Filename(acceptHeaders []string) (string, string, error) {
948
- // If we ever want to offer a 404 file for a different content type
949
- // then this function will need to parse q weightings, but for now
950
- // the presence of anything matching HTML is enough.
951
- for _, acceptHeader := range acceptHeaders {
952
- accepted := strings.Split(acceptHeader, ",")
953
- for _, spec := range accepted {
954
- contentType := strings.SplitN(spec, ";", 1)[0]
955
- switch contentType {
956
- case "*/*", "text/*", "text/html":
957
- return "ipfs-404.html", "text/html", nil
917
+ // if Accept is text/html, see if ipfs-404.html is present
918
+ // This logic isn't documented and will likely be removed at some point.
919
+ // Any 404 logic in _redirects above will have already run by this time, so it's really an extra fall back
920
+ if i.serveLegacy404IfPresent(w, r, contentPath) {
921
+ logger.Debugw("served legacy 404")
922
+ return nil, nil, false
923
}
924
}
960
- }
925
962
- return "", "", fmt.Errorf("there is no 404 file for the requested content types")
963
-}
964
-
965
-// returns unquoted path with all special characters revealed as \u codes
966
-func debugStr(path string) string {
967
- q := fmt.Sprintf("%+q", path)
968
- if len(q) >= 3 {
969
- q = q[1 : len(q)-1]
926
+ // Note: webError will replace http.StatusBadRequest with StatusNotFound if necessary
927
+ webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusBadRequest)
928
+ return nil, nil, false
929
}
971
- return q
930
}
931
932
// Detect 'Cache-Control: only-if-cached' in request and return data if it is already in the local datastore.
core/corehttp/gateway_handler_unixfs__redirects.go
new
+287
@@ -0,0 +1,287 @@
1
+package corehttp
2
+
3
+import (
4
+ "fmt"
5
+ "io"
6
+ "net/http"
7
+ gopath "path"
8
+ "strconv"
9
+ "strings"
10
+
11
+ files "github.com/ipfs/go-ipfs-files"
12
+ redirects "github.com/ipfs/go-ipfs-redirects-file"
13
+ ipath "github.com/ipfs/interface-go-ipfs-core/path"
14
+ "go.uber.org/zap"
15
+)
16
+
17
+// Resolving a UnixFS path involves determining if the provided `path.Path` exists and returning the `path.Resolved`
18
+// corresponding to that path. For UnixFS, path resolution is more involved.
19
+//
20
+// When a path under requested CID does not exist, Gateway will check if a `_redirects` file exists
21
+// underneath the root CID of the path, and apply rules defined there.
22
+// See sepcification introduced in: https://github.com/ipfs/specs/pull/290
23
+//
24
+// Scenario 1:
25
+// If a path exists, we always return the `path.Resolved` corresponding to that path, regardless of the existence of a `_redirects` file.
26
+//
27
+// Scenario 2:
28
+// If a path does not exist, usually we should return a `nil` resolution path and an error indicating that the path
29
+// doesn't exist. However, a `_redirects` file may exist and contain a redirect rule that redirects that path to a different path.
30
+// We need to evaluate the rule and perform the redirect if present.
31
+//
32
+// Scenario 3:
33
+// Another possibility is that the path corresponds to a rewrite rule (i.e. a rule with a status of 200).
34
+// In this case, we don't perform a redirect, but do need to return a `path.Resolved` and `path.Path` corresponding to
35
+// the rewrite destination path.
36
+//
37
+// Note that for security reasons, redirect rules are only processed when the request has origin isolation.
38
+// See https://github.com/ipfs/specs/pull/290 for more information.
39
+func (i *gatewayHandler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, logger *zap.SugaredLogger) (newResolvedPath ipath.Resolved, newContentPath ipath.Path, continueProcessing bool, hadMatchingRule bool) {
40
+ redirectsFile := i.getRedirectsFile(r, contentPath, logger)
41
+ if redirectsFile != nil {
42
+ redirectRules, err := i.getRedirectRules(r, redirectsFile)
43
+ if err != nil {
44
+ internalWebError(w, err)
45
+ return nil, nil, false, true
46
+ }
47
+
48
+ redirected, newPath, err := i.handleRedirectsFileRules(w, r, contentPath, redirectRules)
49
+ if err != nil {
50
+ err = fmt.Errorf("trouble processing _redirects file at %q: %w", redirectsFile.String(), err)
51
+ internalWebError(w, err)
52
+ return nil, nil, false, true
53
+ }
54
+
55
+ if redirected {
56
+ return nil, nil, false, true
57
+ }
58
+
59
+ // 200 is treated as a rewrite, so update the path and continue
60
+ if newPath != "" {
61
+ // Reassign contentPath and resolvedPath since the URL was rewritten
62
+ contentPath = ipath.New(newPath)
63
+ resolvedPath, err = i.api.ResolvePath(r.Context(), contentPath)
64
+ if err != nil {
65
+ internalWebError(w, err)
66
+ return nil, nil, false, true
67
+ }
68
+
69
+ return resolvedPath, contentPath, true, true
70
+ }
71
+ }
72
+ // No matching rule, paths remain the same, continue regular processing
73
+ return resolvedPath, contentPath, true, false
74
+}
75
+
76
+func (i *gatewayHandler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, redirectRules []redirects.Rule) (redirected bool, newContentPath string, err error) {
77
+ // Attempt to match a rule to the URL path, and perform the corresponding redirect or rewrite
78
+ pathParts := strings.Split(contentPath.String(), "/")
79
+ if len(pathParts) > 3 {
80
+ // All paths should start with /ipfs/cid/, so get the path after that
81
+ urlPath := "/" + strings.Join(pathParts[3:], "/")
82
+ rootPath := strings.Join(pathParts[:3], "/")
83
+ // Trim off the trailing /
84
+ urlPath = strings.TrimSuffix(urlPath, "/")
85
+
86
+ for _, rule := range redirectRules {
87
+ // Error right away if the rule is invalid
88
+ if !rule.MatchAndExpandPlaceholders(urlPath) {
89
+ continue
90
+ }
91
+
92
+ // We have a match!
93
+
94
+ // Rewrite
95
+ if rule.Status == 200 {
96
+ // Prepend the rootPath
97
+ toPath := rootPath + rule.To
98
+ return false, toPath, nil
99
+ }
100
+
101
+ // Or 4xx
102
+ if rule.Status == 404 || rule.Status == 410 || rule.Status == 451 {
103
+ toPath := rootPath + rule.To
104
+ content4xxPath := ipath.New(toPath)
105
+ err := i.serve4xx(w, r, content4xxPath, rule.Status)
106
+ return true, toPath, err
107
+ }
108
+
109
+ // Or redirect
110
+ if rule.Status >= 301 && rule.Status <= 308 {
111
+ http.Redirect(w, r, rule.To, rule.Status)
112
+ return true, "", nil
113
+ }
114
+ }
115
+ }
116
+
117
+ // No redirects matched
118
+ return false, "", nil
119
+}
120
+
121
+func (i *gatewayHandler) getRedirectRules(r *http.Request, redirectsFilePath ipath.Resolved) ([]redirects.Rule, error) {
122
+ // Convert the path into a file node
123
+ node, err := i.api.Unixfs().Get(r.Context(), redirectsFilePath)
124
+ if err != nil {
125
+ return nil, fmt.Errorf("could not get _redirects: %w", err)
126
+ }
127
+ defer node.Close()
128
+
129
+ // Convert the node into a file
130
+ f, ok := node.(files.File)
131
+ if !ok {
132
+ return nil, fmt.Errorf("could not parse _redirects: %w", err)
133
+ }
134
+
135
+ // Parse redirect rules from file
136
+ redirectRules, err := redirects.Parse(f)
137
+ if err != nil {
138
+ return nil, fmt.Errorf("could not parse _redirects: %w", err)
139
+ }
140
+
141
+ return redirectRules, nil
142
+}
143
+
144
+// Returns a resolved path to the _redirects file located in the root CID path of the requested path
145
+func (i *gatewayHandler) getRedirectsFile(r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) ipath.Resolved {
146
+ // contentPath is the full ipfs path to the requested resource,
147
+ // regardless of whether path or subdomain resolution is used.
148
+ rootPath := getRootPath(contentPath)
149
+
150
+ // Check for _redirects file.
151
+ // Any path resolution failures are ignored and we just assume there's no _redirects file.
152
+ // Note that ignoring these errors also ensures that the use of the empty CID (bafkqaaa) in tests doesn't fail.
153
+ path := ipath.Join(rootPath, "_redirects")
154
+ resolvedPath, err := i.api.ResolvePath(r.Context(), path)
155
+ if err != nil {
156
+ return nil
157
+ }
158
+ return resolvedPath
159
+}
160
+
161
+// Returns the root CID Path for the given path
162
+func getRootPath(path ipath.Path) ipath.Path {
163
+ parts := strings.Split(path.String(), "/")
164
+ return ipath.New(gopath.Join("/", path.Namespace(), parts[2]))
165
+}
166
+
167
+func (i *gatewayHandler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPath ipath.Path, status int) error {
168
+ resolved4xxPath, err := i.api.ResolvePath(r.Context(), content4xxPath)
169
+ if err != nil {
170
+ return err
171
+ }
172
+
173
+ node, err := i.api.Unixfs().Get(r.Context(), resolved4xxPath)
174
+ if err != nil {
175
+ return err
176
+ }
177
+ defer node.Close()
178
+
179
+ f, ok := node.(files.File)
180
+ if !ok {
181
+ return fmt.Errorf("could not convert node for %d page to file", status)
182
+ }
183
+
184
+ size, err := f.Size()
185
+ if err != nil {
186
+ return fmt.Errorf("could not get size of %d page", status)
187
+ }
188
+
189
+ log.Debugf("using _redirects: custom %d file at %q", status, content4xxPath)
190
+ w.Header().Set("Content-Type", "text/html")
191
+ w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
192
+ addCacheControlHeaders(w, r, content4xxPath, resolved4xxPath.Cid())
193
+ w.WriteHeader(status)
194
+ _, err = io.CopyN(w, f, size)
195
+ return err
196
+}
197
+
198
+func hasOriginIsolation(r *http.Request) bool {
199
+ _, gw := r.Context().Value(requestContextKey("gw-hostname")).(string)
200
+ _, dnslink := r.Context().Value("dnslink-hostname").(string)
201
+
202
+ if gw || dnslink {
203
+ return true
204
+ }
205
+
206
+ return false
207
+}
208
+
209
+func isUnixfsResponseFormat(responseFormat string) bool {
210
+ // The implicit response format is UnixFS
211
+ return responseFormat == ""
212
+}
213
+
214
+// Deprecated: legacy ipfs-404.html files are superseded by _redirects file
215
+// This is provided only for backward-compatibility, until websites migrate
216
+// to 404s managed via _redirects file (https://github.com/ipfs/specs/pull/290)
217
+func (i *gatewayHandler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool {
218
+ resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath)
219
+ if err != nil {
220
+ return false
221
+ }
222
+
223
+ dr, err := i.api.Unixfs().Get(r.Context(), resolved404Path)
224
+ if err != nil {
225
+ return false
226
+ }
227
+ defer dr.Close()
228
+
229
+ f, ok := dr.(files.File)
230
+ if !ok {
231
+ return false
232
+ }
233
+
234
+ size, err := f.Size()
235
+ if err != nil {
236
+ return false
237
+ }
238
+
239
+ log.Debugw("using pretty 404 file", "path", contentPath)
240
+ w.Header().Set("Content-Type", ctype)
241
+ w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
242
+ w.WriteHeader(http.StatusNotFound)
243
+ _, err = io.CopyN(w, f, size)
244
+ return err == nil
245
+}
246
+
247
+func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) {
248
+ filename404, ctype, err := preferred404Filename(r.Header.Values("Accept"))
249
+ if err != nil {
250
+ return nil, "", err
251
+ }
252
+
253
+ pathComponents := strings.Split(contentPath.String(), "/")
254
+
255
+ for idx := len(pathComponents); idx >= 3; idx-- {
256
+ pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...)
257
+ parsed404Path := ipath.New("/" + pretty404)
258
+ if parsed404Path.IsValid() != nil {
259
+ break
260
+ }
261
+ resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path)
262
+ if err != nil {
263
+ continue
264
+ }
265
+ return resolvedPath, ctype, nil
266
+ }
267
+
268
+ return nil, "", fmt.Errorf("no pretty 404 in any parent folder")
269
+}
270
+
271
+func preferred404Filename(acceptHeaders []string) (string, string, error) {
272
+ // If we ever want to offer a 404 file for a different content type
273
+ // then this function will need to parse q weightings, but for now
274
+ // the presence of anything matching HTML is enough.
275
+ for _, acceptHeader := range acceptHeaders {
276
+ accepted := strings.Split(acceptHeader, ",")
277
+ for _, spec := range accepted {
278
+ contentType := strings.SplitN(spec, ";", 1)[0]
279
+ switch contentType {
280
+ case "*/*", "text/*", "text/html":
281
+ return "ipfs-404.html", "text/html", nil
282
+ }
283
+ }
284
+ }
285
+
286
+ return "", "", fmt.Errorf("there is no 404 file for the requested content types")
287
+}
core/corehttp/gateway_handler_unixfs_dir.go
+1
-1
@@ -185,7 +185,7 @@ func (i *gatewayHandler) serveDirectory(ctx context.Context, w http.ResponseWrit
185
var gwURL string
186
187
// Get gateway hostname and build gateway URL.
188
- if h, ok := r.Context().Value("gw-hostname").(string); ok {
188
+ if h, ok := r.Context().Value(requestContextKey("gw-hostname")).(string); ok {
189
gwURL = "//" + h
190
} else {
191
gwURL = ""
core/corehttp/hostname.go
+5
-2
@@ -221,7 +221,8 @@ func HostnameOption() ServeOption {
221
if !cfg.Gateway.NoDNSLink && isDNSLinkName(r.Context(), coreAPI, host) {
222
// rewrite path and handle as DNSLink
223
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
224
- childMux.ServeHTTP(w, withHostnameContext(r, host))
224
+ ctx := context.WithValue(r.Context(), requestContextKey("dnslink-hostname"), host)
225
+ childMux.ServeHTTP(w, withHostnameContext(r.WithContext(ctx), host))
226
return
227
}
228
@@ -242,6 +243,8 @@ type wildcardHost struct {
243
spec *config.GatewaySpec
244
}
245
246
+type requestContextKey string
247
+
248
// Extends request context to include hostname of a canonical gateway root
249
// (subdomain root or dnslink fqdn)
250
func withHostnameContext(r *http.Request, hostname string) *http.Request {
@@ -250,7 +253,7 @@ func withHostnameContext(r *http.Request, hostname string) *http.Request {
253
// Host header, subdomain gateways have more comples rules (knownSubdomainDetails)
254
// More: https://github.com/ipfs/dir-index-html/issues/42
255
// nolint: staticcheck // non-backward compatible change
253
- ctx := context.WithValue(r.Context(), "gw-hostname", hostname)
256
+ ctx := context.WithValue(r.Context(), requestContextKey("gw-hostname"), hostname)
257
return r.WithContext(ctx)
258
}
259
docs/examples/kubo-as-a-library/go.sum
+4
@@ -571,6 +571,7 @@ github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY
571
github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY=
572
github.com/ipfs/go-ipfs-provider v0.7.1 h1:eKToBUAb6ZY8iiA6AYVxzW4G1ep67XUaaEBUIYpxhfw=
573
github.com/ipfs/go-ipfs-provider v0.7.1/go.mod h1:QwdDYRYnC5sYGLlOwVDY/0ZB6T3zcMtu+5+GdGeUuw8=
574
+github.com/ipfs/go-ipfs-redirects-file v0.1.1/go.mod h1:tAwRjCV0RjLTjH8DR/AU7VYvfQECg+lpUy2Mdzv7gyk=
575
github.com/ipfs/go-ipfs-routing v0.0.1/go.mod h1:k76lf20iKFxQTjcJokbPM9iBXVXVZhcOwc360N4nuKs=
576
github.com/ipfs/go-ipfs-routing v0.1.0/go.mod h1:hYoUkJLyAUKhF58tysKpids8RNDPO42BVMgK5dNsoqY=
577
github.com/ipfs/go-ipfs-routing v0.2.1 h1:E+whHWhJkdN9YeoHZNj5itzc+OR292AJ2uE9FFiW0BY=
@@ -1535,9 +1536,11 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
1536
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
1537
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
1538
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
1539
+github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
1540
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
1541
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
1542
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
1543
+github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb/go.mod h1:ikPs9bRWicNw3S7XpJ8sK/smGwU9WcSVU3dy9qahYBM=
1544
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
1545
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
1546
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@@ -2179,6 +2182,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
2182
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
2183
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
2184
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2185
+gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2186
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2187
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
2188
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
go.mod
+2
@@ -117,6 +117,7 @@ require (
117
require (
118
github.com/benbjohnson/clock v1.3.0
119
github.com/ipfs/go-delegated-routing v0.6.0
120
+ github.com/ipfs/go-ipfs-redirects-file v0.1.1
121
github.com/ipfs/go-log/v2 v2.5.1
122
)
123
@@ -225,6 +226,7 @@ require (
226
github.com/tidwall/gjson v1.14.0 // indirect
227
github.com/tidwall/match v1.1.1 // indirect
228
github.com/tidwall/pretty v1.2.0 // indirect
229
+ github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb // indirect
230
github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect
231
github.com/whyrusleeping/cbor-gen v0.0.0-20210219115102-f37d292932f2 // indirect
232
github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f // indirect
go.sum
+5
@@ -565,6 +565,8 @@ github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY
565
github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY=
566
github.com/ipfs/go-ipfs-provider v0.7.1 h1:eKToBUAb6ZY8iiA6AYVxzW4G1ep67XUaaEBUIYpxhfw=
567
github.com/ipfs/go-ipfs-provider v0.7.1/go.mod h1:QwdDYRYnC5sYGLlOwVDY/0ZB6T3zcMtu+5+GdGeUuw8=
568
+github.com/ipfs/go-ipfs-redirects-file v0.1.1 h1:Io++k0Vf/wK+tfnhEh63Yte1oQK5VGT2hIEYpD0Rzx8=
569
+github.com/ipfs/go-ipfs-redirects-file v0.1.1/go.mod h1:tAwRjCV0RjLTjH8DR/AU7VYvfQECg+lpUy2Mdzv7gyk=
570
github.com/ipfs/go-ipfs-routing v0.0.1/go.mod h1:k76lf20iKFxQTjcJokbPM9iBXVXVZhcOwc360N4nuKs=
571
github.com/ipfs/go-ipfs-routing v0.1.0/go.mod h1:hYoUkJLyAUKhF58tysKpids8RNDPO42BVMgK5dNsoqY=
572
github.com/ipfs/go-ipfs-routing v0.2.1 h1:E+whHWhJkdN9YeoHZNj5itzc+OR292AJ2uE9FFiW0BY=
@@ -1511,9 +1513,12 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
1513
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
1514
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
1515
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
1516
+github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
1517
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
1518
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
1519
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
1520
+github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb h1:Ywfo8sUltxogBpFuMOFRrrSifO788kAFxmvVw31PtQQ=
1521
+github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb/go.mod h1:ikPs9bRWicNw3S7XpJ8sK/smGwU9WcSVU3dy9qahYBM=
1522
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
1523
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
1524
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
test/sharness/t0109-gateway-web-_redirects-data/redirects.car
Binary files /dev/null and b/test/sharness/t0109-gateway-web-_redirects-data/redirects.car differ
test/sharness/t0109-gateway-web-_redirects.sh
new
+239
@@ -0,0 +1,239 @@
1
+#!/usr/bin/env bash
2
+
3
+test_description="Test HTTP Gateway _redirects support"
4
+
5
+. lib/test-lib.sh
6
+
7
+test_init_ipfs
8
+test_launch_ipfs_daemon
9
+
10
+## ============================================================================
11
+## Test _redirects file support
12
+## ============================================================================
13
+
14
+# Import test case
15
+# Run `ipfs cat /ipfs/$REDIRECTS_DIR_CID/_redirects` to see sample _redirects file
16
+test_expect_success "Add the _redirects file test directory" '
17
+ ipfs dag import ../t0109-gateway-web-_redirects-data/redirects.car
18
+'
19
+CAR_ROOT_CID=QmQyqMY5vUBSbSxyitJqthgwZunCQjDVtNd8ggVCxzuPQ4
20
+
21
+REDIRECTS_DIR_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/examples | cut -d "/" -f3)
22
+REDIRECTS_DIR_HOSTNAME="${REDIRECTS_DIR_CID}.ipfs.localhost:$GWAY_PORT"
23
+
24
+test_expect_success "request for $REDIRECTS_DIR_HOSTNAME/redirect-one redirects with default of 301, per _redirects file" '
25
+ curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/redirect-one" > response &&
26
+ test_should_contain "301 Moved Permanently" response &&
27
+ test_should_contain "Location: /one.html" response
28
+'
29
+
30
+test_expect_success "request for $REDIRECTS_DIR_HOSTNAME/301-redirect-one redirects with 301, per _redirects file" '
31
+ curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/301-redirect-one" > response &&
32
+ test_should_contain "301 Moved Permanently" response &&
33
+ test_should_contain "Location: /one.html" response
34
+'
35
+
36
+test_expect_success "request for $REDIRECTS_DIR_HOSTNAME/302-redirect-two redirects with 302, per _redirects file" '
37
+ curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/302-redirect-two" > response &&
38
+ test_should_contain "302 Found" response &&
39
+ test_should_contain "Location: /two.html" response
40
+'
41
+
42
+test_expect_success "request for $REDIRECTS_DIR_HOSTNAME/200-index returns 200, per _redirects file" '
43
+ curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/200-index" > response &&
44
+ test_should_contain "my index" response &&
45
+ test_should_contain "200 OK" response
46
+'
47
+
48
+test_expect_success "request for $REDIRECTS_DIR_HOSTNAME/posts/:year/:month/:day/:title redirects with 301 and placeholders, per _redirects file" '
49
+ curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/posts/2022/01/01/hello-world" > response &&
50
+ test_should_contain "301 Moved Permanently" response &&
51
+ test_should_contain "Location: /articles/2022/01/01/hello-world" response
52
+'
53
+
54
+test_expect_success "request for $REDIRECTS_DIR_HOSTNAME/splat/one.html redirects with 301 and splat placeholder, per _redirects file" '
55
+ curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/splat/one.html" > response &&
56
+ test_should_contain "301 Moved Permanently" response &&
57
+ test_should_contain "Location: /redirected-splat/one.html" response
58
+'
59
+
60
+# ensure custom 4xx works and has the same cache headers as regular /ipfs/ path
61
+CUSTOM_4XX_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/examples/404.html | cut -d "/" -f3)
62
+test_expect_success "request for $REDIRECTS_DIR_HOSTNAME/not-found/has-no-redirects-entry returns custom 404, per _redirects file" '
63
+ curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/not-found/has-no-redirects-entry" > response &&
64
+ test_should_contain "404 Not Found" response &&
65
+ test_should_contain "Cache-Control: public, max-age=29030400, immutable" response &&
66
+ test_should_contain "Etag: \"$CUSTOM_4XX_CID\"" response &&
67
+ test_should_contain "my 404" response
68
+'
69
+
70
+CUSTOM_4XX_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/examples/410.html | cut -d "/" -f3)
71
+test_expect_success "request for $REDIRECTS_DIR_HOSTNAME/gone/has-no-redirects-entry returns custom 410, per _redirects file" '
72
+ curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/gone/has-no-redirects-entry" > response &&
73
+ test_should_contain "410 Gone" response &&
74
+ test_should_contain "Cache-Control: public, max-age=29030400, immutable" response &&
75
+ test_should_contain "Etag: \"$CUSTOM_4XX_CID\"" response &&
76
+ test_should_contain "my 410" response
77
+'
78
+
79
+CUSTOM_4XX_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/examples/451.html | cut -d "/" -f3)
80
+test_expect_success "request for $REDIRECTS_DIR_HOSTNAME/unavail/has-no-redirects-entry returns custom 451, per _redirects file" '
81
+ curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/unavail/has-no-redirects-entry" > response &&
82
+ test_should_contain "451 Unavailable For Legal Reasons" response &&
83
+ test_should_contain "Cache-Control: public, max-age=29030400, immutable" response &&
84
+ test_should_contain "Etag: \"$CUSTOM_4XX_CID\"" response &&
85
+ test_should_contain "my 451" response
86
+'
87
+
88
+test_expect_success "request for $REDIRECTS_DIR_HOSTNAME/catch-all returns 200, per _redirects file" '
89
+ curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/catch-all" > response &&
90
+ test_should_contain "200 OK" response &&
91
+ test_should_contain "my index" response
92
+'
93
+
94
+# This test ensures _redirects is supported only on Web Gateways that use Host header (DNSLink, Subdomain)
95
+test_expect_success "request for http://127.0.0.1:$GWAY_PORT/ipfs/$REDIRECTS_DIR_CID/301-redirect-one returns generic 404 (no custom 404 from _redirects since no origin isolation)" '
96
+ curl -sD - "http://127.0.0.1:$GWAY_PORT/ipfs/$REDIRECTS_DIR_CID/301-redirect-one" > response &&
97
+ test_should_contain "404 Not Found" response &&
98
+ test_should_not_contain "my 404" response
99
+'
100
+
101
+# With CRLF line terminator
102
+NEWLINE_REDIRECTS_DIR_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/newlines | cut -d "/" -f3)
103
+NEWLINE_REDIRECTS_DIR_HOSTNAME="${NEWLINE_REDIRECTS_DIR_CID}.ipfs.localhost:$GWAY_PORT"
104
+
105
+test_expect_success "newline: _redirects has CRLF line terminators" '
106
+ ipfs cat /ipfs/$NEWLINE_REDIRECTS_DIR_CID/_redirects | file - > response &&
107
+ test_should_contain "with CRLF line terminators" response
108
+'
109
+
110
+test_expect_success "newline: request for $NEWLINE_REDIRECTS_DIR_HOSTNAME/redirect-one redirects with default of 301, per _redirects file" '
111
+ curl -sD - --resolve $NEWLINE_REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$NEWLINE_REDIRECTS_DIR_HOSTNAME/redirect-one" > response &&
112
+ test_should_contain "301 Moved Permanently" response &&
113
+ test_should_contain "Location: /one.html" response
114
+'
115
+
116
+# Good codes
117
+GOOD_REDIRECTS_DIR_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/good-codes | cut -d "/" -f3)
118
+GOOD_REDIRECTS_DIR_HOSTNAME="${GOOD_REDIRECTS_DIR_CID}.ipfs.localhost:$GWAY_PORT"
119
+
120
+test_expect_success "good codes: request for $GOOD_REDIRECTS_DIR_HOSTNAME/redirect-one redirects with default of 301, per _redirects file" '
121
+ curl -sD - --resolve $GOOD_REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$GOOD_REDIRECTS_DIR_HOSTNAME/a301" > response &&
122
+ test_should_contain "301 Moved Permanently" response &&
123
+ test_should_contain "Location: /b301" response
124
+'
125
+
126
+# Bad codes
127
+BAD_REDIRECTS_DIR_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/bad-codes | cut -d "/" -f3)
128
+BAD_REDIRECTS_DIR_HOSTNAME="${BAD_REDIRECTS_DIR_CID}.ipfs.localhost:$GWAY_PORT"
129
+
130
+# if accessing a path that doesn't exist, read _redirects and fail parsing, and return error
131
+test_expect_success "bad codes: request for $BAD_REDIRECTS_DIR_HOSTNAME/not-found returns error about bad code" '
132
+ curl -sD - --resolve $BAD_REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$BAD_REDIRECTS_DIR_HOSTNAME/not-found" > response &&
133
+ test_should_contain "500" response &&
134
+ test_should_contain "status code 999 is not supported" response
135
+'
136
+
137
+# if accessing a path that does exist, don't read _redirects and therefore don't fail parsing
138
+test_expect_success "bad codes: request for $BAD_REDIRECTS_DIR_HOSTNAME/found.html doesn't return error about bad code" '
139
+ curl -sD - --resolve $BAD_REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$BAD_REDIRECTS_DIR_HOSTNAME/found.html" > response &&
140
+ test_should_contain "200" response &&
141
+ test_should_contain "my found" response &&
142
+ test_should_not_contain "unsupported redirect status" response
143
+'
144
+
145
+# Invalid file, containing "hello"
146
+INVALID_REDIRECTS_DIR_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/invalid | cut -d "/" -f3)
147
+INVALID_REDIRECTS_DIR_HOSTNAME="${INVALID_REDIRECTS_DIR_CID}.ipfs.localhost:$GWAY_PORT"
148
+
149
+# if accessing a path that doesn't exist, read _redirects and fail parsing, and return error
150
+test_expect_success "invalid file: request for $INVALID_REDIRECTS_DIR_HOSTNAME/not-found returns error about invalid redirects file" '
151
+ curl -sD - --resolve $INVALID_REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$INVALID_REDIRECTS_DIR_HOSTNAME/not-found" > response &&
152
+ test_should_contain "500" response &&
153
+ test_should_contain "could not parse _redirects:" response
154
+'
155
+
156
+# Invalid file, containing forced redirect
157
+INVALID_REDIRECTS_DIR_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/forced | cut -d "/" -f3)
158
+INVALID_REDIRECTS_DIR_HOSTNAME="${INVALID_REDIRECTS_DIR_CID}.ipfs.localhost:$GWAY_PORT"
159
+
160
+# if accessing a path that doesn't exist, read _redirects and fail parsing, and return error
161
+test_expect_success "invalid file: request for $INVALID_REDIRECTS_DIR_HOSTNAME/not-found returns error about invalid redirects file" '
162
+ curl -sD - --resolve $INVALID_REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$INVALID_REDIRECTS_DIR_HOSTNAME/not-found" > response &&
163
+ test_should_contain "500" response &&
164
+ test_should_contain "could not parse _redirects:" response &&
165
+ test_should_contain "forced redirects (or \"shadowing\") are not supported" response
166
+'
167
+
168
+# if accessing a path that doesn't exist and _redirects file is too large, return error
169
+TOO_LARGE_REDIRECTS_DIR_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/too-large | cut -d "/" -f3)
170
+TOO_LARGE_REDIRECTS_DIR_HOSTNAME="${TOO_LARGE_REDIRECTS_DIR_CID}.ipfs.localhost:$GWAY_PORT"
171
+test_expect_success "invalid file: request for $TOO_LARGE_REDIRECTS_DIR_HOSTNAME/not-found returns error about too large redirects file" '
172
+ curl -sD - --resolve $TOO_LARGE_REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$TOO_LARGE_REDIRECTS_DIR_HOSTNAME/not-found" > response &&
173
+ test_should_contain "500" response &&
174
+ test_should_contain "could not parse _redirects:" response &&
175
+ test_should_contain "redirects file size cannot exceed" response
176
+'
177
+
178
+test_kill_ipfs_daemon
179
+
180
+# disable wildcard DNSLink gateway
181
+# and enable it on specific DNSLink hostname
182
+ipfs config --json Gateway.NoDNSLink true && \
183
+ipfs config --json Gateway.PublicGateways '{
184
+ "dnslink-enabled-on-fqdn.example.org": {
185
+ "NoDNSLink": false,
186
+ "UseSubdomains": false,
187
+ "Paths": ["/ipfs"]
188
+ },
189
+ "dnslink-disabled-on-fqdn.example.com": {
190
+ "NoDNSLink": true,
191
+ "UseSubdomains": false,
192
+ "Paths": []
193
+ }
194
+}' || exit 1
195
+
196
+# DNSLink test requires a daemon in online mode with precached /ipns/ mapping
197
+# REDIRECTS_DIR_CID=$(ipfs resolve -r /ipfs/$CAR_ROOT_CID/examples | cut -d "/" -f3)
198
+DNSLINK_FQDN="dnslink-enabled-on-fqdn.example.org"
199
+NO_DNSLINK_FQDN="dnslink-disabled-on-fqdn.example.com"
200
+export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$REDIRECTS_DIR_CID"
201
+
202
+# restart daemon to apply config changes
203
+test_launch_ipfs_daemon
204
+
205
+# make sure test setup is valid (fail if CoreAPI is unable to resolve)
206
+test_expect_success "spoofed DNSLink record resolves in cli" "
207
+ ipfs resolve /ipns/$DNSLINK_FQDN > result &&
208
+ test_should_contain \"$REDIRECTS_DIR_CID\" result &&
209
+ ipfs cat /ipns/$DNSLINK_FQDN/_redirects > result &&
210
+ test_should_contain \"index.html\" result
211
+"
212
+
213
+test_expect_success "request for $DNSLINK_FQDN/redirect-one redirects with default of 301, per _redirects file" '
214
+ curl -sD - --resolve $DNSLINK_FQDN:$GWAY_PORT:127.0.0.1 "http://$DNSLINK_FQDN:$GWAY_PORT/redirect-one" > response &&
215
+ test_should_contain "301 Moved Permanently" response &&
216
+ test_should_contain "Location: /one.html" response
217
+'
218
+
219
+# ensure custom 404 works and has the same cache headers as regular /ipns/ paths
220
+test_expect_success "request for $DNSLINK_FQDN/en/has-no-redirects-entry returns custom 404, per _redirects file" '
221
+ curl -sD - --resolve $DNSLINK_FQDN:$GWAY_PORT:127.0.0.1 "http://$DNSLINK_FQDN:$GWAY_PORT/not-found/has-no-redirects-entry" > response &&
222
+ test_should_contain "404 Not Found" response &&
223
+ test_should_contain "Etag: \"Qmd9GD7Bauh6N2ZLfNnYS3b7QVAijbud83b8GE8LPMNBBP\"" response &&
224
+ test_should_not_contain "Cache-Control: public, max-age=29030400, immutable" response &&
225
+ test_should_not_contain "immutable" response &&
226
+ test_should_contain "Date: " response &&
227
+ test_should_contain "my 404" response
228
+'
229
+
230
+test_expect_success "request for $NO_DNSLINK_FQDN/redirect-one does not redirect, since DNSLink is disabled" '
231
+ curl -sD - --resolve $NO_DNSLINK_FQDN:$GWAY_PORT:127.0.0.1 "http://$NO_DNSLINK_FQDN:$GWAY_PORT/redirect-one" > response &&
232
+ test_should_not_contain "one.html" response &&
233
+ test_should_not_contain "301 Moved Permanently" response &&
234
+ test_should_not_contain "Location:" response
235
+'
236
+
237
+test_kill_ipfs_daemon
238
+
239
+test_done