Skip to content

Commit cf82ae5

Browse files
authored
Merge pull request #9123 from ipfs/fix/gw-trailing-slash-unixfs-dirs
fix(gw): directory URL normalization
2 parents bcab64d + 3182986 commit cf82ae5

File tree

6 files changed

+60
-32
lines changed

6 files changed

+60
-32
lines changed

core/corehttp/gateway_handler_unixfs_dir.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,30 @@ func (i *gatewayHandler) serveDirectory(ctx context.Context, w http.ResponseWrit
4141
}
4242
originalUrlPath := requestURI.Path
4343

44-
// Check if directory has index.html, if so, serveFile
45-
idxPath := ipath.Join(contentPath, "index.html")
46-
idx, err := i.api.Unixfs().Get(ctx, idxPath)
47-
switch err.(type) {
48-
case nil:
49-
cpath := contentPath.String()
50-
dirwithoutslash := cpath[len(cpath)-1] != '/'
44+
// Ensure directory paths end with '/'
45+
if originalUrlPath[len(originalUrlPath)-1] != '/' {
46+
// don't redirect to trailing slash if it's go get
47+
// https://github.com/ipfs/kubo/pull/3963
5148
goget := r.URL.Query().Get("go-get") == "1"
52-
if dirwithoutslash && !goget {
53-
// See comment above where originalUrlPath is declared.
49+
if !goget {
5450
suffix := "/"
51+
// preserve query parameters
5552
if r.URL.RawQuery != "" {
56-
// preserve query parameters
5753
suffix = suffix + "?" + r.URL.RawQuery
5854
}
59-
55+
// /ipfs/cid/foo?bar must be redirected to /ipfs/cid/foo/?bar
6056
redirectURL := originalUrlPath + suffix
61-
logger.Debugw("serving index.html file", "to", redirectURL, "status", http.StatusFound, "path", idxPath)
62-
http.Redirect(w, r, redirectURL, http.StatusFound)
57+
logger.Debugw("directory location moved permanently", "status", http.StatusMovedPermanently)
58+
http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
6359
return
6460
}
61+
}
6562

63+
// Check if directory has index.html, if so, serveFile
64+
idxPath := ipath.Join(contentPath, "index.html")
65+
idx, err := i.api.Unixfs().Get(ctx, idxPath)
66+
switch err.(type) {
67+
case nil:
6668
f, ok := idx.(files.File)
6769
if !ok {
6870
internalWebError(w, files.ErrNotReader)
@@ -163,7 +165,7 @@ func (i *gatewayHandler) serveDirectory(ctx context.Context, w http.ResponseWrit
163165
// add the correct link depending on whether the path ends with a slash
164166
default:
165167
if strings.HasSuffix(backLink, "/") {
166-
backLink += "./.."
168+
backLink += ".."
167169
} else {
168170
backLink += "/.."
169171
}

core/corehttp/gateway_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -380,9 +380,9 @@ func TestIPNSHostnameRedirect(t *testing.T) {
380380
t.Fatal(err)
381381
}
382382

383-
// expect 302 redirect to same path, but with trailing slash
384-
if res.StatusCode != 302 {
385-
t.Errorf("status is %d, expected 302", res.StatusCode)
383+
// expect 301 redirect to same path, but with trailing slash
384+
if res.StatusCode != 301 {
385+
t.Errorf("status is %d, expected 301", res.StatusCode)
386386
}
387387
hdr := res.Header["Location"]
388388
if len(hdr) < 1 {
@@ -403,9 +403,9 @@ func TestIPNSHostnameRedirect(t *testing.T) {
403403
t.Fatal(err)
404404
}
405405

406-
// expect 302 redirect to same path, but with prefix and trailing slash
407-
if res.StatusCode != 302 {
408-
t.Errorf("status is %d, expected 302", res.StatusCode)
406+
// expect 301 redirect to same path, but with prefix and trailing slash
407+
if res.StatusCode != 301 {
408+
t.Errorf("status is %d, expected 301", res.StatusCode)
409409
}
410410
hdr = res.Header["Location"]
411411
if len(hdr) < 1 {
@@ -492,7 +492,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
492492
if !matchPathOrBreadcrumbs(s, "/ipns/<a href=\"//example.net/\">example.net</a>/<a href=\"//example.net/foo%3F%20%23%3C%27\">foo? #&lt;&#39;</a>") {
493493
t.Fatalf("expected a path in directory listing")
494494
}
495-
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/./..\">") {
495+
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/..\">") {
496496
t.Fatalf("expected backlink in directory listing")
497497
}
498498
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/file.txt\">") {
@@ -566,7 +566,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
566566
if !matchPathOrBreadcrumbs(s, "/ipns/<a href=\"//example.net/\">example.net</a>/<a href=\"//example.net/foo%3F%20%23%3C%27\">foo? #&lt;&#39;</a>/<a href=\"//example.net/foo%3F%20%23%3C%27/bar\">bar</a>") {
567567
t.Fatalf("expected a path in directory listing")
568568
}
569-
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/bar/./..\">") {
569+
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/bar/..\">") {
570570
t.Fatalf("expected backlink in directory listing")
571571
}
572572
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/bar/file.txt\">") {

test/sharness/t0110-gateway.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,17 @@ test_expect_success "GET IPFS directory file output looks good" '
7272

7373
test_expect_success "GET IPFS directory with index.html returns redirect to add trailing slash" "
7474
curl -sI -o response_without_slash \"http://127.0.0.1:$port/ipfs/$HASH2/dirwithindex?query=to-remember\" &&
75+
test_should_contain \"HTTP/1.1 301 Moved Permanently\" response_without_slash &&
7576
test_should_contain \"Location: /ipfs/$HASH2/dirwithindex/?query=to-remember\" response_without_slash
7677
"
7778

79+
# This enables go get to parse go-import meta tags from index.html files stored in IPFS
80+
# https://github.com/ipfs/kubo/pull/3963
81+
test_expect_success "GET IPFS directory with index.html and no trailing slash returns expected output when go-get is passed" "
82+
curl -s -o response_with_slash \"http://127.0.0.1:$port/ipfs/$HASH2/dirwithindex?go-get=1\" &&
83+
test_should_contain \"hello i am a webpage\" response_with_slash
84+
"
85+
7886
test_expect_success "GET IPFS directory with index.html and trailing slash returns expected output" "
7987
curl -s -o response_with_slash \"http://127.0.0.1:$port/ipfs/$HASH2/dirwithindex/?query=to-remember\" &&
8088
test_should_contain \"hello i am a webpage\" response_with_slash

test/sharness/t0113-gateway-symlink.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ test_expect_success "Add the test directory" '
2222
'
2323

2424
test_expect_success "Test the directory listing" '
25-
curl "$GWAY_ADDR/ipfs/$HASH" > list_response &&
25+
curl "$GWAY_ADDR/ipfs/$HASH/" > list_response &&
2626
test_should_contain ">foo<" list_response &&
2727
test_should_contain ">bar<" list_response
2828
'

test/sharness/t0114-gateway-subdomains.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ test_expect_success "valid file and subdirectory paths in directory listing at {
268268

269269
test_expect_success "valid parent directory path in directory listing at {cid}.ipfs.localhost/sub/dir" '
270270
curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME/ipfs/ipns/" > list_response &&
271-
test_should_contain "<a href=\"/ipfs/ipns/./..\">..</a>" list_response &&
271+
test_should_contain "<a href=\"/ipfs/ipns/..\">..</a>" list_response &&
272272
test_should_contain "<a href=\"/ipfs/ipns/bar\">bar</a>" list_response
273273
'
274274

@@ -441,7 +441,7 @@ test_expect_success "valid file and directory paths in directory listing at {cid
441441

442442
test_expect_success "valid parent directory path in directory listing at {cid}.ipfs.example.com/sub/dir" '
443443
curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT/ipfs/ipns/ > list_response &&
444-
test_should_contain "<a href=\"/ipfs/ipns/./..\">..</a>" list_response &&
444+
test_should_contain "<a href=\"/ipfs/ipns/..\">..</a>" list_response &&
445445
test_should_contain "<a href=\"/ipfs/ipns/bar\">bar</a>" list_response
446446
'
447447

test/sharness/t0115-gateway-dir-listing.sh

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,14 @@ test_expect_success "path gw: backlink on root CID should be hidden" '
4343
test_should_not_contain "<a href=\"/ipfs/$DIR_CID/\">..</a>" list_response
4444
'
4545

46-
test_expect_success "path gw: Etag should be present" '
46+
test_expect_success "path gw: redirect dir listing to URL with trailing slash" '
4747
curl -sD - http://127.0.0.1:$GWAY_PORT/ipfs/${DIR_CID}/ą/ę > list_response &&
48+
test_should_contain "HTTP/1.1 301 Moved Permanently" list_response &&
49+
test_should_contain "Location: /ipfs/${DIR_CID}/%c4%85/%c4%99/" list_response
50+
'
51+
52+
test_expect_success "path gw: Etag should be present" '
53+
curl -sD - http://127.0.0.1:$GWAY_PORT/ipfs/${DIR_CID}/ą/ę/ > list_response &&
4854
test_should_contain "Index of" list_response &&
4955
test_should_contain "Etag: \"DirIndex-" list_response
5056
'
@@ -72,31 +78,37 @@ test_expect_success "path gw: hash column should be a CID link with filename par
7278
DIR_HOSTNAME="${DIR_CID}.ipfs.localhost"
7379
# note: we skip DNS lookup by running curl with --resolve $DIR_HOSTNAME:127.0.0.1
7480

75-
test_expect_success "path gw: backlink on root CID should be hidden" '
81+
test_expect_success "subdomain gw: backlink on root CID should be hidden" '
7682
curl -sD - --resolve $DIR_HOSTNAME:$GWAY_PORT:127.0.0.1 http://$DIR_HOSTNAME:$GWAY_PORT/ > list_response &&
7783
test_should_contain "Index of" list_response &&
7884
test_should_not_contain "<a href=\"/\">..</a>" list_response
7985
'
8086

81-
test_expect_success "path gw: Etag should be present" '
87+
test_expect_success "subdomain gw: redirect dir listing to URL with trailing slash" '
8288
curl -sD - --resolve $DIR_HOSTNAME:$GWAY_PORT:127.0.0.1 http://$DIR_HOSTNAME:$GWAY_PORT/ą/ę > list_response &&
89+
test_should_contain "HTTP/1.1 301 Moved Permanently" list_response &&
90+
test_should_contain "Location: /%c4%85/%c4%99/" list_response
91+
'
92+
93+
test_expect_success "subdomain gw: Etag should be present" '
94+
curl -sD - --resolve $DIR_HOSTNAME:$GWAY_PORT:127.0.0.1 http://$DIR_HOSTNAME:$GWAY_PORT/ą/ę/ > list_response &&
8395
test_should_contain "Index of" list_response &&
8496
test_should_contain "Etag: \"DirIndex-" list_response
8597
'
8698

87-
test_expect_success "path gw: backlink on subdirectory should point at parent directory" '
99+
test_expect_success "subdomain gw: backlink on subdirectory should point at parent directory" '
88100
test_should_contain "<a href=\"/%C4%85/%C4%99/..\">..</a>" list_response
89101
'
90102

91103
test_expect_success "subdomain gw: breadcrumbs should leverage path-based router mounted on the parent domain" '
92104
test_should_contain "/ipfs/<a href=\"//localhost:$GWAY_PORT/ipfs/$DIR_CID\">$DIR_CID</a>/<a href=\"//localhost:$GWAY_PORT/ipfs/$DIR_CID/%C4%85\">ą</a>/<a href=\"//localhost:$GWAY_PORT/ipfs/$DIR_CID/%C4%85/%C4%99\">ę</a>" list_response
93105
'
94106

95-
test_expect_success "path gw: name column should be a link to content root mounted at subdomain origin" '
107+
test_expect_success "subdomain gw: name column should be a link to content root mounted at subdomain origin" '
96108
test_should_contain "<a href=\"/%C4%85/%C4%99/file-%C5%BA%C5%82.txt\">file-źł.txt</a>" list_response
97109
'
98110

99-
test_expect_success "path gw: hash column should be a CID link to path router with filename param" '
111+
test_expect_success "subdomain gw: hash column should be a CID link to path router with filename param" '
100112
test_should_contain "<a class=\"ipfs-hash\" translate=\"no\" href=\"//localhost:$GWAY_PORT/ipfs/$FILE_CID?filename=file-%25C5%25BA%25C5%2582.txt\">" list_response
101113
'
102114

@@ -121,8 +133,14 @@ test_expect_success "dnslink gw: backlink on root CID should be hidden" '
121133
test_should_not_contain "<a href=\"/\">..</a>" list_response
122134
'
123135

124-
test_expect_success "dnslink gw: Etag should be present" '
136+
test_expect_success "dnslink gw: redirect dir listing to URL with trailing slash" '
125137
curl -sD - --resolve $DNSLINK_HOSTNAME:$GWAY_PORT:127.0.0.1 http://$DNSLINK_HOSTNAME:$GWAY_PORT/ą/ę > list_response &&
138+
test_should_contain "HTTP/1.1 301 Moved Permanently" list_response &&
139+
test_should_contain "Location: /%c4%85/%c4%99/" list_response
140+
'
141+
142+
test_expect_success "dnslink gw: Etag should be present" '
143+
curl -sD - --resolve $DNSLINK_HOSTNAME:$GWAY_PORT:127.0.0.1 http://$DNSLINK_HOSTNAME:$GWAY_PORT/ą/ę/ > list_response &&
126144
test_should_contain "Index of" list_response &&
127145
test_should_contain "Etag: \"DirIndex-" list_response
128146
'

0 commit comments

Comments
 (0)