mirror of
https://github.com/coder/coder.git
synced 2026-10-29 17:44:33 +08:00
fix: handle urls with multiple slashes (#16527)
Fixes: https://github.com/coder/coder/issues/9877 This PR introduces another middleware to rewrite URLs when multiple slashes are used.
This commit is contained in:
@@ -788,6 +788,7 @@ func New(options *Options) *API {
|
|||||||
httpmw.AttachRequestID,
|
httpmw.AttachRequestID,
|
||||||
httpmw.ExtractRealIP(api.RealIPConfig),
|
httpmw.ExtractRealIP(api.RealIPConfig),
|
||||||
httpmw.Logger(api.Logger),
|
httpmw.Logger(api.Logger),
|
||||||
|
singleSlashMW,
|
||||||
rolestore.CustomRoleMW,
|
rolestore.CustomRoleMW,
|
||||||
prometheusMW,
|
prometheusMW,
|
||||||
// Build-Version is helpful for debugging.
|
// Build-Version is helpful for debugging.
|
||||||
@@ -1731,3 +1732,31 @@ func ReadExperiments(log slog.Logger, raw []string) codersdk.Experiments {
|
|||||||
}
|
}
|
||||||
return exps
|
return exps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var multipleSlashesRe = regexp.MustCompile(`/+`)
|
||||||
|
|
||||||
|
func singleSlashMW(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var path string
|
||||||
|
rctx := chi.RouteContext(r.Context())
|
||||||
|
if rctx != nil && rctx.RoutePath != "" {
|
||||||
|
path = rctx.RoutePath
|
||||||
|
} else {
|
||||||
|
path = r.URL.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize multiple slashes to a single slash
|
||||||
|
newPath := multipleSlashesRe.ReplaceAllString(path, "/")
|
||||||
|
|
||||||
|
// Apply the cleaned path
|
||||||
|
// The approach is consistent with: https://github.com/go-chi/chi/blob/e846b8304c769c4f1a51c9de06bebfaa4576bd88/middleware/strip.go#L24-L28
|
||||||
|
if rctx != nil {
|
||||||
|
rctx.RoutePath = newPath
|
||||||
|
} else {
|
||||||
|
r.URL.Path = newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package coderd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStripSlashesMW(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputPath string
|
||||||
|
wantPath string
|
||||||
|
}{
|
||||||
|
{"No changes", "/api/v1/buildinfo", "/api/v1/buildinfo"},
|
||||||
|
{"Double slashes", "/api//v2//buildinfo", "/api/v2/buildinfo"},
|
||||||
|
{"Triple slashes", "/api///v2///buildinfo", "/api/v2/buildinfo"},
|
||||||
|
{"Leading slashes", "///api/v2/buildinfo", "/api/v2/buildinfo"},
|
||||||
|
{"Root path", "/", "/"},
|
||||||
|
{"Double slashes root", "//", "/"},
|
||||||
|
{"Only slashes", "/////", "/"},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run("chi/"+tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
req := httptest.NewRequest("GET", tt.inputPath, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// given
|
||||||
|
rctx := chi.NewRouteContext()
|
||||||
|
rctx.RoutePath = tt.inputPath
|
||||||
|
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||||
|
|
||||||
|
// when
|
||||||
|
singleSlashMW(handler).ServeHTTP(rec, req)
|
||||||
|
updatedCtx := chi.RouteContext(req.Context())
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert.Equal(t, tt.inputPath, req.URL.Path)
|
||||||
|
assert.Equal(t, tt.wantPath, updatedCtx.RoutePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stdlib/"+tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
req := httptest.NewRequest("GET", tt.inputPath, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// when
|
||||||
|
singleSlashMW(handler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert.Equal(t, tt.wantPath, req.URL.Path)
|
||||||
|
assert.Nil(t, chi.RouteContext(req.Context()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,12 @@ export default defineConfig({
|
|||||||
"csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax",
|
"csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax",
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
|
"//": {
|
||||||
|
changeOrigin: true,
|
||||||
|
target: process.env.CODER_HOST || "http://localhost:3000",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
rewrite: (path) => path.replace(/\/+/g, "/"),
|
||||||
|
},
|
||||||
"/api": {
|
"/api": {
|
||||||
ws: true,
|
ws: true,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user