net/http: preserve path escaping in redirects

Currently, when performing path sanitization and trailing-slash
redirects, we do not populate the RawPath field of the returned URL. As
a result, when a URL path is already escaped, our redirect behavior can
be rather surprising:

- When "/a//b%2fc" is sanitized, it is double-escaped, becoming
  "/a/b/%252fc".
- When doing trailing slash-redirect, "/a%2fb" will lose its escaping,
  becoming "/a/b/" instead of "/a%2fb/".

Fix this by populating the RawPath field.

Also updated TestServeWithSlashRedirectForHostPatterns that still uses
the old 301 status code rather than 307 after CL 720820.

Fixes #79897

Change-Id: Ibdd1f119afdd6824e52185be3852234a6a6a6964
Reviewed-on: https://go-review.googlesource.com/c/go/+/792860
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
diff --git a/src/net/http/serve_test.go b/src/net/http/serve_test.go
index 7e83984..f8710af 100644
--- a/src/net/http/serve_test.go
+++ b/src/net/http/serve_test.go
@@ -308,6 +308,7 @@
 	{"codesearch.google.com/search", serve(202)},
 	{"codesearch.google.com/", serve(203)},
 	{"example.com/", HandlerFunc(checkQueryStringHandler)},
+	{"/pkg/bar/extra%2fpath", serve(200)},
 }
 
 // serve returns a handler that sends a response with the given code.
@@ -427,6 +428,9 @@
 	{"GET", "google.com", "/", 404, false},
 	{"GET", "example.com", "/test/?example.com/test/", 200, false},
 	{"GET", "example.com", "test/?example.com/test/", 200, true},
+	{"GET", "google.com", "/pkg/bar//extra%2fpath", 200, true},
+	{"GET", "google.com", "/dir/b%2fc/..", 200, true},
+	{"GET", "google.com", "/doesnotexist/b%2fc/..", 404, true},
 }
 
 // TestServeMuxHandlerRedirects tests that automatic redirects generated by
@@ -601,6 +605,7 @@
 	mux.Handle("example.com:3000/pkg/connect/", stringHandler("example.com:3000/pkg/connect/"))
 	mux.Handle("example.com:9000/", stringHandler("example.com:9000/"))
 	mux.Handle("/pkg/baz/", stringHandler("/pkg/baz/"))
+	mux.Handle("example.com/a%2fb/", stringHandler("example.com/a%2fb/"))
 
 	tests := []struct {
 		method string
@@ -622,6 +627,7 @@
 		{"CONNECT", "http://example.com:3000/pkg/foo", 404, "", ""},
 		{"CONNECT", "http://example.com:3000/pkg/baz", 307, "/pkg/baz/", ""},
 		{"CONNECT", "http://example.com:3000/pkg/connect", 307, "/pkg/connect/", ""},
+		{"GET", "http://example.com/a%2fb", 307, "/a%2fb/", ""},
 	}
 
 	for i, tt := range tests {
@@ -633,7 +639,7 @@
 			t.Errorf("#%d: Status = %d; want = %d", i, got, want)
 		}
 
-		if tt.code == 301 {
+		if tt.code == 307 {
 			if got, want := w.HeaderMap.Get("Location"), tt.loc; got != want {
 				t.Errorf("#%d: Location = %q; want = %q", i, got, want)
 			}
diff --git a/src/net/http/server.go b/src/net/http/server.go
index 933dcc9..1ef5621 100644
--- a/src/net/http/server.go
+++ b/src/net/http/server.go
@@ -2747,7 +2747,7 @@
 			if n != nil {
 				patStr = n.pattern.String()
 			}
-			u := &url.URL{Path: path, RawQuery: r.URL.RawQuery}
+			u := urlFromEscaped(path, r.URL.RawQuery)
 			return RedirectHandler(u.String(), StatusTemporaryRedirect), patStr, nil, nil
 		}
 	}
@@ -2792,12 +2792,35 @@
 			// of findHandler, and that method returns before it does the "n == nil" check where
 			// the first return value matters. We return it here only to make the pattern available
 			// to findHandler.
-			return n2, nil, &url.URL{Path: cleanPath(u.Path) + "/", RawQuery: u.RawQuery}
+			return n2, nil, urlFromEscaped(path, u.RawQuery)
 		}
 	}
 	return n, matches, nil
 }
 
+// urlFromEscaped returns a url.URL constructed from an escaped path and a raw
+// query.
+//
+// It ensures that the Path and RawPath fields are in sync by unescaping the
+// escaped path. Populating only the Path field and leaving RawPath empty (or
+// failing to keep them in sync) can cause url.URL.String to produce a URL with
+// either unexpected escaping (e.g., double-escaping "%" into "%25" in an
+// already escaped path) or a lack thereof (e.g., losing the escaping of "%2f"
+// and turning it into a literal path separator "/").
+func urlFromEscaped(escaped, rawQuery string) *url.URL {
+	unescaped, err := url.PathUnescape(escaped)
+	// Should be impossible, since ServeMux will reject unparsable URLs way
+	// earlier.
+	if err != nil {
+		unescaped = escaped
+	}
+	return &url.URL{
+		Path:     unescaped,
+		RawPath:  escaped,
+		RawQuery: rawQuery,
+	}
+}
+
 // exactMatch reports whether the node's pattern exactly matches the path.
 // As a special case, if the node is nil, exactMatch return false.
 //