Files
gitea/modules/web/router.go
T
wxiaoguangandGitHub 6904f6480c refactor: http request binding (#38971)
Better than before, still not good enough (more work can be done in the
future)

And add the missing error handling in the PrivateContext "bind"
middleware.

By the way, picked some "TrimSpace" changes from "fix: trim whitespace
from SMTP address and port - #38934" (fix #38926)
2026-08-19 14:15:42 +08:00

320 lines
11 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package web
import (
"fmt"
"net/http"
"net/url"
"reflect"
"strings"
"gitea.dev/modules/htmlutil"
"gitea.dev/modules/public"
"gitea.dev/modules/reqctx"
"gitea.dev/modules/setting"
"gitea.dev/modules/translation"
"gitea.dev/modules/validation"
"gitea.dev/modules/web/middleware"
"gitea.dev/modules/web/types"
"github.com/go-chi/chi/v5"
)
// Bind binding the request form to a form object and assign context data
func Bind[T middleware.Form]() http.HandlerFunc {
return func(resp http.ResponseWriter, req *http.Request) {
form, errs := middleware.BindFormValidate[T](req, validation.Binder())
ctx := reqctx.FromContext(req.Context())
data := ctx.GetData()
locale := ctx.Value(translation.ContextKey).(translation.Locale) //nolint:forcetypeassert // must exist
SetForm(data, form)
// Legacy template error handling: try to restore the form's values as much as possible,
// especially for RenderWithErrDeprecated to re-render the form with errors.
middleware.AssignForm(form, data)
errorMessage, errorFieldName, _ := middleware.BuildValidationErrorForUser(form, locale, errs)
if errorMessage != "" {
data["HasError"] = true
data["ErrorMsg"] = errorMessage
if errorFieldName != "" {
data["Err_"+errorFieldName] = true
}
}
}
}
// SetForm set the form object
func SetForm(dataStore reqctx.ContextDataProvider, obj any) {
dataStore.GetData()["__form"] = obj
}
func IsFormSet(dataStore reqctx.RequestDataStore) bool {
return dataStore.GetData()["__form"] != nil
}
// GetForm returns the validate form information
func GetForm[T any](dataStore reqctx.RequestDataStore) T {
form, ok := dataStore.GetData()["__form"].(T)
if !ok {
panic(fmt.Errorf("bound form %T does not match the requested type %s", dataStore.GetData()["__form"], reflect.TypeFor[T]()))
}
return form
}
// Router defines a route based on chi's router
type Router struct {
chiRouter *chi.Mux
afterRouting []any
curGroupPrefix string
curMiddlewares []any
}
// NewRouter creates a new route
func NewRouter() *Router {
r := chi.NewRouter()
return &Router{chiRouter: r}
}
// BeforeRouting adds middlewares which will be executed before the request path gets routed
// It should only be used for framework-level global middlewares when it needs to change request method & path.
func (r *Router) BeforeRouting(middlewares ...any) {
for _, m := range middlewares {
if !isNilOrFuncNil(m) {
r.chiRouter.Use(toHandlerProvider(m))
}
}
}
// AfterRouting adds middlewares which will be executed after the request path gets routed
// It can see the routed path and resolved path parameters
func (r *Router) AfterRouting(middlewares ...any) {
r.afterRouting = append(r.afterRouting, middlewares...)
}
// Group mounts a sub-router along a "pattern" string.
func (r *Router) Group(pattern string, fn func(), middlewares ...any) {
previousGroupPrefix := r.curGroupPrefix
previousMiddlewares := r.curMiddlewares
r.curGroupPrefix += pattern
r.curMiddlewares = append(r.curMiddlewares, middlewares...)
fn()
r.curGroupPrefix = previousGroupPrefix
r.curMiddlewares = previousMiddlewares
}
func (r *Router) getPattern(pattern string) string {
newPattern := r.curGroupPrefix + pattern
if !strings.HasPrefix(newPattern, "/") {
newPattern = "/" + newPattern
}
if newPattern == "/" {
return newPattern
}
return strings.TrimSuffix(newPattern, "/")
}
func isNilOrFuncNil(v any) bool {
if v == nil {
return true
}
r := reflect.ValueOf(v)
return r.Kind() == reflect.Func && r.IsNil()
}
func wrapMiddlewareAppendPre(all []middlewareProvider, middlewares []any) []middlewareProvider {
for _, m := range middlewares {
if h, ok := m.(types.PreMiddlewareProvider); ok && h != nil {
all = append(all, toHandlerProvider(middlewareProvider(h)))
}
}
return all
}
func wrapMiddlewareAppendNormal(all []middlewareProvider, middlewares []any) []middlewareProvider {
for _, m := range middlewares {
if _, ok := m.(types.PreMiddlewareProvider); !ok && !isNilOrFuncNil(m) {
all = append(all, toHandlerProvider(m))
}
}
return all
}
func wrapMiddlewareAndHandler(useMiddlewares, curMiddlewares, h []any) (_ []middlewareProvider, _ http.HandlerFunc, hasPreMiddlewares bool) {
if len(h) == 0 {
panic("no endpoint handler provided")
}
if isNilOrFuncNil(h[len(h)-1]) {
panic("endpoint handler can't be nil")
}
handlerProviders := make([]middlewareProvider, 0, len(useMiddlewares)+len(curMiddlewares)+len(h)+1)
handlerProviders = wrapMiddlewareAppendPre(handlerProviders, useMiddlewares)
handlerProviders = wrapMiddlewareAppendPre(handlerProviders, curMiddlewares)
handlerProviders = wrapMiddlewareAppendPre(handlerProviders, h)
hasPreMiddlewares = len(handlerProviders) > 0
handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, useMiddlewares)
handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, curMiddlewares)
handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, h)
middlewares := handlerProviders[:len(handlerProviders)-1]
handlerFunc := handlerProviders[len(handlerProviders)-1](nil).ServeHTTP
mockPoint := RouterMockPoint(MockAfterMiddlewares)
if mockPoint != nil {
middlewares = append(middlewares, mockPoint)
}
return middlewares, handlerFunc, hasPreMiddlewares
}
// Methods adds the same handlers for multiple http "methods" (separated by ",").
// If any method is invalid, the lower level router will panic.
func (r *Router) Methods(methods, pattern string, h ...any) {
middlewares, handlerFunc, _ := wrapMiddlewareAndHandler(r.afterRouting, r.curMiddlewares, h)
fullPattern := r.getPattern(pattern)
if strings.Contains(methods, ",") {
methods := strings.SplitSeq(methods, ",")
for method := range methods {
r.chiRouter.With(middlewares...).Method(strings.TrimSpace(method), fullPattern, handlerFunc)
}
} else {
r.chiRouter.With(middlewares...).Method(methods, fullPattern, handlerFunc)
}
}
// Mount attaches another Router along "/pattern/*"
func (r *Router) Mount(pattern string, subRouter *Router) {
handlerProviders := make([]middlewareProvider, 0, len(r.afterRouting)+len(r.curMiddlewares))
handlerProviders = wrapMiddlewareAppendPre(handlerProviders, r.afterRouting)
handlerProviders = wrapMiddlewareAppendPre(handlerProviders, r.curMiddlewares)
handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, r.afterRouting)
handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, r.curMiddlewares)
r.chiRouter.With(handlerProviders...).Mount(r.getPattern(pattern), subRouter.chiRouter)
}
// Any delegate requests for all methods
func (r *Router) Any(pattern string, h ...any) {
middlewares, handlerFunc, _ := wrapMiddlewareAndHandler(r.afterRouting, r.curMiddlewares, h)
r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc)
}
// Delete delegate delete method
func (r *Router) Delete(pattern string, h ...any) {
r.Methods("DELETE", pattern, h...)
}
// Get delegate get method
func (r *Router) Get(pattern string, h ...any) {
r.Methods("GET", pattern, h...)
}
// Head delegate head method
func (r *Router) Head(pattern string, h ...any) {
r.Methods("HEAD", pattern, h...)
}
// Post delegate post method
func (r *Router) Post(pattern string, h ...any) {
r.Methods("POST", pattern, h...)
}
// Put delegate put method
func (r *Router) Put(pattern string, h ...any) {
r.Methods("PUT", pattern, h...)
}
// Patch delegate patch method
func (r *Router) Patch(pattern string, h ...any) {
r.Methods("PATCH", pattern, h...)
}
// ServeHTTP implements http.Handler
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO: need to move it to the top-level common middleware, otherwise each "Mount" will cause it to be executed multiple times, which is inefficient.
r.normalizeRequestPath(w, req, r.chiRouter)
}
// NotFound defines a handler to respond whenever a route could not be found.
func (r *Router) NotFound(h http.HandlerFunc) {
middlewares, handlerFunc, _ := wrapMiddlewareAndHandler(r.afterRouting, r.curMiddlewares, []any{h})
r.chiRouter.NotFound(func(w http.ResponseWriter, r *http.Request) {
executeMiddlewaresHandler(w, r, middlewares, handlerFunc)
})
}
func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) {
normalized := false
normalizedPath := req.URL.EscapedPath()
if normalizedPath == "" {
normalizedPath, normalized = "/", true
} else if normalizedPath != "/" {
normalized = strings.HasSuffix(normalizedPath, "/")
normalizedPath = strings.TrimRight(normalizedPath, "/")
}
removeRepeatedSlashes := strings.Contains(normalizedPath, "//")
normalized = normalized || removeRepeatedSlashes
// the following code block is a slow-path for replacing all repeated slashes "//" to one single "/"
// if the path doesn't have repeated slashes, then no need to execute it
if removeRepeatedSlashes {
buf := &strings.Builder{}
for i := 0; i < len(normalizedPath); i++ {
if i == 0 || normalizedPath[i-1] != '/' || normalizedPath[i] != '/' {
buf.WriteByte(normalizedPath[i])
}
}
normalizedPath = buf.String()
}
// If the config tells Gitea to use a sub-url path directly without reverse proxy,
// then we need to remove the sub-url path from the request URL path.
// But "/v2" is special for OCI container registry, it should always be in the root of the site.
if setting.UseSubURLPath {
remainingPath, ok := strings.CutPrefix(normalizedPath, setting.AppSubURL+"/")
if ok {
normalizedPath = "/" + remainingPath
} else if normalizedPath == setting.AppSubURL {
normalizedPath = "/"
} else if !strings.HasPrefix(normalizedPath+"/", "/v2/") && !public.IsViteDevRequest(req) {
// do not respond to other requests, to simulate a real sub-path environment
resp.Header().Add("Content-Type", "text/html; charset=utf-8")
resp.WriteHeader(http.StatusNotFound)
_, _ = htmlutil.HTMLPrintf(resp, `404 page not found, sub-path is: <a href="%s">%s</a>`, setting.AppSubURL, setting.AppSubURL)
return
}
normalized = true
}
// if the path is normalized, then fill it back to the request
if normalized {
decodedPath, err := url.PathUnescape(normalizedPath)
if err != nil {
http.Error(resp, "400 Bad Request: unable to unescape path "+normalizedPath, http.StatusBadRequest)
return
}
req.URL.RawPath = normalizedPath
req.URL.Path = decodedPath
}
next.ServeHTTP(resp, req)
}
// Combo delegates requests to Combo
func (r *Router) Combo(pattern string, h ...any) *Combo {
return &Combo{r, pattern, h}
}
// PathGroup creates a group of paths which could be matched by regexp.
// It is only designed to resolve some special cases which chi router can't handle.
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) {
g := &RouterPathGroup{r: r, pathParam: "*"}
fn(g)
r.Any(pattern, append(h, g.ServeHTTP)...)
}