diff --git a/gin.go b/gin.go index e17596aa..d7110908 100644 --- a/gin.go +++ b/gin.go @@ -9,6 +9,7 @@ import ( "html/template" "net" "net/http" + "net/url" "os" "path" "regexp" @@ -643,11 +644,57 @@ func (engine *Engine) HandleContext(c *Context) { c.index = oldIndexValue } +// compareRawWithEscaped compares the RawPath with the EscapedPath of a URL. +// It returns true if they are equal, false otherwise. +func compareRawWithEscaped(url *url.URL) bool { + rawPath := url.RawPath + escapedPath := url.EscapedPath() + + if len(rawPath) != len(escapedPath) { + return false + } + + for i := 0; i < len(rawPath); i++ { + if rawPath[i] != escapedPath[i] { + return false + } + + if rawPath[i] == '%' && i+2 < len(rawPath) { + if rawPath[i+1] == '2' && (rawPath[i+2] == 'F' || rawPath[i+2] == 'f') { + return false + } + + var diff byte + if rawPath[i+1] > escapedPath[i+1] { + diff = rawPath[i+1] - escapedPath[i+1] + } else { + diff = escapedPath[i+1] - rawPath[i+1] + } + if diff != 0 && diff != 32 { + return false + } + + if rawPath[i+2] > escapedPath[i+2] { + diff = rawPath[i+2] - escapedPath[i+2] + } else { + diff = escapedPath[i+2] - rawPath[i+2] + } + if diff != 0 && diff != 32 { + return false + } + + i += 2 + } + } + + return true +} + func (engine *Engine) handleHTTPRequest(c *Context) { httpMethod := c.Request.Method rPath := c.Request.URL.Path unescape := false - if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { + if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 && !compareRawWithEscaped(c.Request.URL) { rPath = c.Request.URL.RawPath unescape = engine.UnescapePathValues } diff --git a/routes_test.go b/routes_test.go index 995ff51c..bb7e482f 100644 --- a/routes_test.go +++ b/routes_test.go @@ -789,3 +789,19 @@ func TestEngineHandleMethodNotAllowedCornerCase(t *testing.T) { w := PerformRequest(r, http.MethodGet, "/base/v1/user/groups") assert.Equal(t, http.StatusNotFound, w.Code) } + +// Test the fix for https://github.com/gin-gonic/gin/issues/4034 +func TestLowercasePercentEncodePath(t *testing.T) { + route := Default() + route.UnescapePathValues = false + route.UseRawPath = true + route.RedirectFixedPath = true + route.GET("/핫", func(ctx *Context) { + ctx.JSON(200, H{}) + }) + req := httptest.NewRequest("GET", "/%ed%95%ab", nil) + w := httptest.NewRecorder() + route.ServeHTTP(w, req) + assert.Equal(t, 200, w.Code) + assert.Equal(t, "{}", w.Body.String()) +}