webseed/request.go | 10 +++++++--- webseed/request_test.go | 100 +++++++++++++++++++++++------------------------------ diff --git a/webseed/request.go b/webseed/request.go index a38e6372952357a7a8ea9d723bcafa84086bb887..53fe6dba1e3b1fe13750ae61e247ec4669031afd 100644 --- a/webseed/request.go +++ b/webseed/request.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" "net/url" - "path" "strings" "github.com/anacrolix/torrent/metainfo" @@ -24,9 +23,14 @@ func defaultPathEscaper(pathComps []string) string { var ret []string for _, comp := range pathComps { - ret = append(ret, url.QueryEscape(comp)) + esc := url.PathEscape(comp) + // S3 incorrectly escapes + in paths to spaces, so we add an extra encoding for that. This + // seems to be handled correctly regardless of whether an endpoint uses query or path + // escaping. + esc = strings.ReplaceAll(esc, "+", "%2B") + ret = append(ret, esc) } - return path.Join(ret...) + return strings.Join(ret, "/") } func trailingPath( diff --git a/webseed/request_test.go b/webseed/request_test.go index 7f691e0a04523b16f0e3bc11d9afa32b1ab2b45b..af3071f3fd4a61f930a94f53027ff17e88ab3a42 100644 --- a/webseed/request_test.go +++ b/webseed/request_test.go @@ -2,71 +2,59 @@ package webseed import ( "net/url" - "path" "testing" qt "github.com/frankban/quicktest" ) -func TestEscapePath(t *testing.T) { +func TestDefaultPathEscaper(t *testing.T) { c := qt.New(t) - test := func( - parts []string, result string, - escaper PathEscaper, - unescaper func(string) (string, error), - ) { - unescaped, err := unescaper(escaper(parts)) - if !c.Check(err, qt.IsNil) { - return - } - c.Check(unescaped, qt.Equals, result) + test := func(unescaped string, parts ...string) { + assertPartsUnescape(c, unescaped, parts...) + } + for _, tc := range defaultPathEscapeTestCases { + test(tc.escaped, tc.parts...) } +} - // Test with nil escapers (always uses url.QueryEscape) - // ------ - test( - []string{"a_b-c", "d + e.f"}, - "a_b-c/d + e.f", - defaultPathEscaper, - url.QueryUnescape, - ) - test( - []string{"a_1-b_c2", "d 3. (e, f).g"}, - "a_1-b_c2/d 3. (e, f).g", - defaultPathEscaper, - url.QueryUnescape, - ) +// So we can manually check, and use these to seed fuzzing. +var defaultPathEscapeTestCases = []struct { + escaped string + parts []string +}{ + {"/", []string{"", ""}}, + {"a_b-c/d + e.f", []string{"a_b-c", "d + e.f"}}, + {"a_1-b_c2/d 3. (e, f).g", []string{"a_1-b_c2", "d 3. (e, f).g"}}, + {"a_b-c/d + e.f", []string{"a_b-c", "d + e.f"}}, + {"a_1-b_c2/d 3. (e, f).g", []string{"a_1-b_c2", "d 3. (e, f).g"}}, + {"war/and/peace", []string{"war", "and", "peace"}}, + {"he//o#world/world", []string{"he//o#world", "world"}}, + {`ノ┬─┬ノ ︵ ( \o°o)\`, []string{`ノ┬─┬ノ ︵ ( \o°o)\`}}, + { + `%aa + %bb/Parsi Tv - سرقت و باز کردن در ماشین در کم‌تر از ۳ ثانیه + فیلم.webm`, + []string{`%aa + %bb`, `Parsi Tv - سرقت و باز کردن در ماشین در کم‌تر از ۳ ثانیه + فیلم.webm`}, + }, +} - // Test with custom escapers - // ------ - test( - []string{"a_b-c", "d + e.f"}, - "a_b-c/d + e.f", - func(s []string) string { - var ret []string - for _, comp := range s { - ret = append(ret, url.PathEscape(comp)) - } - return path.Join(ret...) - }, - url.PathUnescape, - ) - test( - []string{"a_1-b_c2", "d 3. (e, f).g"}, - "a_1-b_c2/d 3. (e, f).g", - func(s []string) string { - var ret []string - for _, comp := range s { - ret = append(ret, url.PathEscape(comp)) - } - return path.Join(ret...) - }, - url.PathUnescape, - ) +func assertPartsUnescape(c *qt.C, unescaped string, parts ...string) { + escaped := defaultPathEscaper(parts) + pathUnescaped, err := url.PathUnescape(escaped) + c.Assert(err, qt.IsNil) + c.Assert(pathUnescaped, qt.Equals, unescaped) + queryUnescaped, err := url.QueryUnescape(escaped) + c.Assert(err, qt.IsNil) + c.Assert(queryUnescaped, qt.Equals, unescaped) } -func TestEscapePathForEmptyInfoName(t *testing.T) { - qt.Check(t, defaultPathEscaper([]string{`ノ┬─┬ノ ︵ ( \o°o)\`}), qt.Equals, "%E3%83%8E%E2%94%AC%E2%94%80%E2%94%AC%E3%83%8E+%EF%B8%B5+%28+%5Co%C2%B0o%29%5C") - qt.Check(t, defaultPathEscaper([]string{"hello", "world"}), qt.Equals, "hello/world") - qt.Check(t, defaultPathEscaper([]string{"war", "and", "peace"}), qt.Equals, "war/and/peace") +func FuzzDefaultPathEscaper(f *testing.F) { + for _, tc := range defaultPathEscapeTestCases { + if len(tc.parts) == 2 { + f.Add(tc.parts[0], tc.parts[1]) + } + } + // I think a single separator is enough to test special handling around /. Also fuzzing doesn't + // let us take []string as an input. + f.Fuzz(func(t *testing.T, first, second string) { + assertPartsUnescape(qt.New(t), first+"/"+second, first, second) + }) }