Skip to content

Commit 3563792

Browse files
committed
net/http: unescape paths and patterns by segment
When parsing patterns and matching, split the path into segments at slashes, then unescape each segment. This behaves as most people would expect: - The pattern "/%61" matches the paths "/a" and "/%61". - The pattern "/%7B" matches the path "/{". (If we did not unescape patterns, there would be no way to write that pattern: because "/{" is a parse error because it is an invalid wildcard.) - The pattern "/user/{u}" matches "/user/john%2Fdoe" with u set to "john/doe". - The unexpected redirections of #21955 will not occur. A later CL will restore the old behavior behind a GODEBUG setting. Updates #61410. Fixes #21955. Change-Id: I99025e149021fc94bf87d351699270460db532d9 Reviewed-on: https://go-review.googlesource.com/c/go/+/530575 Run-TryBot: Jonathan Amsterdam <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Damien Neil <[email protected]>
1 parent b7e0dfc commit 3563792

File tree

6 files changed

+97
-38
lines changed

6 files changed

+97
-38
lines changed

Diff for: src/net/http/pattern.go

+12
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package http
99
import (
1010
"errors"
1111
"fmt"
12+
"net/url"
1213
"strings"
1314
"unicode"
1415
)
@@ -141,6 +142,7 @@ func parsePattern(s string) (_ *pattern, err error) {
141142
seg, rest = rest[:i], rest[i:]
142143
if i := strings.IndexByte(seg, '{'); i < 0 {
143144
// Literal.
145+
seg = pathUnescape(seg)
144146
p.segments = append(p.segments, segment{s: seg})
145147
} else {
146148
// Wildcard.
@@ -178,6 +180,7 @@ func parsePattern(s string) (_ *pattern, err error) {
178180
return p, nil
179181
}
180182

183+
// TODO(jba): remove this; it is unused.
181184
func isValidHTTPToken(s string) bool {
182185
if s == "" {
183186
return false
@@ -204,6 +207,15 @@ func isValidWildcardName(s string) bool {
204207
return true
205208
}
206209

210+
func pathUnescape(path string) string {
211+
u, err := url.PathUnescape(path)
212+
if err != nil {
213+
// Invalidly escaped path; use the original
214+
return path
215+
}
216+
return u
217+
}
218+
207219
// relationship is a relationship between two patterns, p1 and p2.
208220
type relationship string
209221

Diff for: src/net/http/pattern_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ func TestParsePattern(t *testing.T) {
9494
"a.com/foo//",
9595
pattern{host: "a.com", segments: []segment{lit("foo"), lit(""), multi("")}},
9696
},
97+
{
98+
"/%61%62/%7b/%",
99+
pattern{segments: []segment{lit("ab"), lit("{"), lit("%")}},
100+
},
97101
} {
98102
got := mustParsePattern(t, test.in)
99103
if !got.equal(&test.want) {
@@ -113,6 +117,8 @@ func TestParsePatternError(t *testing.T) {
113117
{"/{w}x", "at offset 1: bad wildcard segment"},
114118
{"/x{w}", "at offset 1: bad wildcard segment"},
115119
{"/{wx", "at offset 1: bad wildcard segment"},
120+
{"/a/{/}/c", "at offset 3: bad wildcard segment"},
121+
{"/a/{%61}/c", "at offset 3: bad wildcard name"}, // wildcard names aren't unescaped
116122
{"/{a$}", "at offset 1: bad wildcard name"},
117123
{"/{}", "at offset 1: empty wildcard"},
118124
{"POST a.com/x/{}/y", "at offset 13: empty wildcard"},

Diff for: src/net/http/request_test.go

+67-17
Original file line numberDiff line numberDiff line change
@@ -1445,23 +1445,22 @@ func TestPathValue(t *testing.T) {
14451445
"d": "",
14461446
},
14471447
},
1448-
// TODO(jba): uncomment these tests when we implement path escaping (forthcoming).
1449-
// {
1450-
// "/names/{name}/{other...}",
1451-
// "/names/" + url.PathEscape("/john") + "/address",
1452-
// map[string]string{
1453-
// "name": "/john",
1454-
// "other": "address",
1455-
// },
1456-
// },
1457-
// {
1458-
// "/names/{name}/{other...}",
1459-
// "/names/" + url.PathEscape("john/doe") + "/address",
1460-
// map[string]string{
1461-
// "name": "john/doe",
1462-
// "other": "address",
1463-
// },
1464-
// },
1448+
{
1449+
"/names/{name}/{other...}",
1450+
"/names/%2fjohn/address",
1451+
map[string]string{
1452+
"name": "/john",
1453+
"other": "address",
1454+
},
1455+
},
1456+
{
1457+
"/names/{name}/{other...}",
1458+
"/names/john%2Fdoe/there/is%2F/more",
1459+
map[string]string{
1460+
"name": "john/doe",
1461+
"other": "there/is//more",
1462+
},
1463+
},
14651464
} {
14661465
mux := NewServeMux()
14671466
mux.HandleFunc(test.pattern, func(w ResponseWriter, r *Request) {
@@ -1555,3 +1554,54 @@ func TestStatus(t *testing.T) {
15551554
}
15561555
}
15571556
}
1557+
1558+
func TestEscapedPathsAndPatterns(t *testing.T) {
1559+
matches := []struct {
1560+
pattern string
1561+
paths []string
1562+
}{
1563+
{
1564+
"/a", // this pattern matches a path that unescapes to "/a"
1565+
[]string{"/a", "/%61"},
1566+
},
1567+
{
1568+
"/%62", // patterns are unescaped by segment; matches paths that unescape to "/b"
1569+
[]string{"/b", "/%62"},
1570+
},
1571+
{
1572+
"/%7B/%7D", // the only way to write a pattern that matches '{' or '}'
1573+
[]string{"/{/}", "/%7b/}", "/{/%7d", "/%7B/%7D"},
1574+
},
1575+
{
1576+
"/%x", // patterns that do not unescape are left unchanged
1577+
[]string{"/%25x"},
1578+
},
1579+
}
1580+
1581+
mux := NewServeMux()
1582+
var gotPattern string
1583+
for _, m := range matches {
1584+
mux.HandleFunc(m.pattern, func(w ResponseWriter, r *Request) {
1585+
gotPattern = m.pattern
1586+
})
1587+
}
1588+
1589+
server := httptest.NewServer(mux)
1590+
defer server.Close()
1591+
1592+
for _, m := range matches {
1593+
for _, p := range m.paths {
1594+
res, err := Get(server.URL + p)
1595+
if err != nil {
1596+
t.Fatal(err)
1597+
}
1598+
if res.StatusCode != 200 {
1599+
t.Errorf("%s: got code %d, want 200", p, res.StatusCode)
1600+
continue
1601+
}
1602+
if g, w := gotPattern, m.pattern; g != w {
1603+
t.Errorf("%s: pattern: got %q, want %q", p, g, w)
1604+
}
1605+
}
1606+
}
1607+
}

Diff for: src/net/http/routing_tree.go

+5-14
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
package http
2020

2121
import (
22-
"net/url"
2322
"strings"
2423
)
2524

@@ -180,7 +179,7 @@ func (n *routingNode) matchPath(path string, matches []string) (*routingNode, []
180179
// We skip this step if the segment is a trailing slash, because single wildcards
181180
// don't match trailing slashes.
182181
if seg != "/" {
183-
if n, m := n.emptyChild.matchPath(rest, append(matches, matchValue(seg))); n != nil {
182+
if n, m := n.emptyChild.matchPath(rest, append(matches, seg)); n != nil {
184183
return n, m
185184
}
186185
}
@@ -190,35 +189,27 @@ func (n *routingNode) matchPath(path string, matches []string) (*routingNode, []
190189
// Don't record a match for a nameless wildcard (which arises from a
191190
// trailing slash in the pattern).
192191
if c.pattern.lastSegment().s != "" {
193-
matches = append(matches, matchValue(path[1:])) // remove initial slash
192+
matches = append(matches, pathUnescape(path[1:])) // remove initial slash
194193
}
195194
return c, matches
196195
}
197196
return nil, nil
198197
}
199198

200-
func matchValue(path string) string {
201-
m, err := url.PathUnescape(path)
202-
if err != nil {
203-
// Path is not properly escaped, so use the original.
204-
return path
205-
}
206-
return m
207-
}
208-
209199
// firstSegment splits path into its first segment, and the rest.
210200
// The path must begin with "/".
211201
// If path consists of only a slash, firstSegment returns ("/", "").
202+
// The segment is returned unescaped, if possible.
212203
func firstSegment(path string) (seg, rest string) {
213204
if path == "/" {
214205
return "/", ""
215206
}
216207
path = path[1:] // drop initial slash
217208
i := strings.IndexByte(path, '/')
218209
if i < 0 {
219-
return path, ""
210+
i = len(path)
220211
}
221-
return path[:i], path[i:]
212+
return pathUnescape(path[:i]), path[i:]
222213
}
223214

224215
// matchingMethods adds to methodSet all the methods that would result in a

Diff for: src/net/http/routing_tree_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ func TestRoutingFirstSegment(t *testing.T) {
2222
{"/a/b/c", []string{"a", "b", "c"}},
2323
{"/a/b/", []string{"a", "b", "/"}},
2424
{"/", []string{"/"}},
25+
{"/a/%62/c", []string{"a", "b", "c"}},
26+
{"/a%2Fb%2fc", []string{"a/b/c"}},
2527
} {
2628
var got []string
2729
rest := test.in

Diff for: src/net/http/server.go

+5-7
Original file line numberDiff line numberDiff line change
@@ -2422,10 +2422,9 @@ func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
24222422
// after the redirect.
24232423
func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string, _ *pattern, matches []string) {
24242424
var n *routingNode
2425-
// TODO(jba): use escaped path. This is an independent change that is also part
2426-
// of proposal https://go.dev/issue/61410.
2427-
path := r.URL.Path
24282425
host := r.URL.Host
2426+
escapedPath := r.URL.EscapedPath()
2427+
path := escapedPath
24292428
// CONNECT requests are not canonicalized.
24302429
if r.Method == "CONNECT" {
24312430
// If r.URL.Path is /tree and its handler is not registered,
@@ -2451,7 +2450,7 @@ func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string, _ *patte
24512450
if u != nil {
24522451
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path, nil, nil
24532452
}
2454-
if path != r.URL.Path {
2453+
if path != escapedPath {
24552454
// Redirect to cleaned path.
24562455
patStr := ""
24572456
if n != nil {
@@ -2478,8 +2477,7 @@ func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string, _ *patte
24782477
}
24792478

24802479
// matchOrRedirect looks up a node in the tree that matches the host, method and path.
2481-
// The path is known to be in canonical form, except for CONNECT methods.
2482-
2480+
//
24832481
// If the url argument is non-nil, handler also deals with trailing-slash
24842482
// redirection: when a path doesn't match exactly, the match is tried again
24852483
// after appending "/" to the path. If that second match succeeds, the last
@@ -2496,7 +2494,7 @@ func (mux *ServeMux) matchOrRedirect(host, method, path string, u *url.URL) (_ *
24962494
path += "/"
24972495
n2, _ := mux.tree.match(host, method, path)
24982496
if exactMatch(n2, path) {
2499-
return nil, nil, &url.URL{Path: path, RawQuery: u.RawQuery}
2497+
return nil, nil, &url.URL{Path: cleanPath(u.Path) + "/", RawQuery: u.RawQuery}
25002498
}
25012499
}
25022500
return n, matches, nil

0 commit comments

Comments
 (0)