diff --git a/proxy/proxy.go b/proxy/proxy.go index d986d50..154a6bc 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -306,11 +306,14 @@ func (p *Server) forwardRequest(conn net.Conn, req *http.Request, https bool) { scheme = "https" } - // Create a new request to the target server + // RawPath preserves the original percent-encoding (e.g. %2F in scoped + // npm package names like /@sentry%2Fbun) so url.URL.String() doesn't + // re-encode reserved characters as their literal form. targetURL := &url.URL{ Scheme: scheme, Host: req.Host, Path: req.URL.Path, + RawPath: req.URL.RawPath, RawQuery: req.URL.RawQuery, } diff --git a/proxy/proxy_framework_test.go b/proxy/proxy_framework_test.go index dbfee2b..36a332b 100644 --- a/proxy/proxy_framework_test.go +++ b/proxy/proxy_framework_test.go @@ -8,6 +8,7 @@ import ( "log/slog" "net" "net/http" + "net/http/httptest" "net/url" "os" "os/user" @@ -290,6 +291,28 @@ func (pt *ProxyTest) ExpectAllowedViaProxy(targetURL, expectedBody string) { require.Equal(pt.t, expectedBody, string(body), "Expected response body does not match") } +// ExpectRawURI spins up a temporary httptest backend, sends a request through +// the proxy with the given path, and asserts the backend received the expected +// raw URI. Useful for verifying that percent-encoded characters survive forwarding. +func (pt *ProxyTest) ExpectRawURI(requestPath, expectedRawURI string) { + pt.t.Helper() + + var receivedRawURI string + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedRawURI = r.RequestURI + w.WriteHeader(http.StatusOK) + })) + defer backend.Close() + + resp, err := pt.proxyClient.Get(backend.URL + requestPath) + require.NoError(pt.t, err, "Failed to make request via proxy") + defer resp.Body.Close() //nolint:errcheck + + require.Equal(pt.t, http.StatusOK, resp.StatusCode) + require.Equal(pt.t, expectedRawURI, receivedRawURI, + "proxy must preserve raw URI encoding") +} + // ExpectAllowedContainsViaProxy makes a request through the proxy using proxy transport (implicit CONNECT for HTTPS) // and expects it to be allowed, checking that response contains the given text func (pt *ProxyTest) ExpectAllowedContainsViaProxy(targetURL, containsText string) { diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 8171365..ffd1c8d 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -27,6 +27,23 @@ func TestProxyServerBasicHTTP(t *testing.T) { }) } +// TestProxyServerPercentEncoding verifies that the proxy preserves +// percent-encoded reserved characters when forwarding requests. +func TestProxyServerPercentEncoding(t *testing.T) { + pt := NewProxyTest(t, + WithCertManager(t.TempDir()), + WithAllowedDomain("127.0.0.1"), + ).Start() + defer pt.Stop() + + t.Run("PercentEncodedSlash", func(t *testing.T) { + pt.ExpectRawURI( + "/npm/npm-all/@sentry%2Fbun", + "/npm/npm-all/@sentry%2Fbun", + ) + }) +} + // TestProxyServerBasicHTTPS tests basic HTTPS request handling func TestProxyServerBasicHTTPS(t *testing.T) { pt := NewProxyTest(t,