This commit is contained in:
@@ -9,10 +9,11 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/web/routing"
|
||||
"code.gitea.io/gitea/modules/web/types"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/web/routing"
|
||||
"gitea.dev/modules/web/types"
|
||||
)
|
||||
|
||||
var responseStatusProviders = map[reflect.Type]func(req *http.Request) types.ResponseStatusProvider{}
|
||||
@@ -131,8 +132,8 @@ type middlewareProvider = func(next http.Handler) http.Handler
|
||||
|
||||
func executeMiddlewaresHandler(w http.ResponseWriter, r *http.Request, middlewares []middlewareProvider, endpoint http.HandlerFunc) {
|
||||
handler := endpoint
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
handler = middlewares[i](handler).ServeHTTP
|
||||
for _, middleware := range slices.Backward(middlewares) {
|
||||
handler = middleware(handler).ServeHTTP
|
||||
}
|
||||
handler(w, r)
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
"gitea.dev/modules/translation"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/validation"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
)
|
||||
@@ -29,7 +29,7 @@ func AssignForm(form any, data map[string]any) {
|
||||
typ := reflect.TypeOf(form)
|
||||
val := reflect.ValueOf(form)
|
||||
|
||||
for typ.Kind() == reflect.Ptr {
|
||||
for typ.Kind() == reflect.Pointer {
|
||||
typ = typ.Elem()
|
||||
val = val.Elem()
|
||||
}
|
||||
@@ -78,82 +78,97 @@ func GetInclude(field reflect.StructField) string {
|
||||
return getRuleBody(field, "Include(")
|
||||
}
|
||||
|
||||
// Validate validate
|
||||
func ReportValidationError(errs binding.Errors, data map[string]any, fieldName, classification, errorMsg string) binding.Errors {
|
||||
errs.Add([]string{fieldName}, classification, errorMsg)
|
||||
|
||||
data["HasError"] = true
|
||||
data["ErrorMsg"] = fieldName + ": " + errorMsg
|
||||
data["Err_"+fieldName] = true
|
||||
// there is already a reported validation error, so no need to generate default error messages in Validate()
|
||||
data["HasErrorFormValidation"] = true
|
||||
return errs
|
||||
}
|
||||
|
||||
func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors {
|
||||
if errs.Len() == 0 {
|
||||
// try to restore the form's values as much as possible,
|
||||
// especially for RenderWithErrDeprecated to re-render the form with errors
|
||||
AssignForm(f, data)
|
||||
|
||||
if errs.Len() == 0 || data["HasErrorFormValidation"] == true {
|
||||
return errs
|
||||
}
|
||||
|
||||
// if HasError=true, then must set default error message
|
||||
// because still a lot of places use `ctx.Data["ErrorMsg"].(string)` even if the error fields can't be found
|
||||
data["HasError"] = true
|
||||
// If the field with name errs[0].FieldNames[0] is not found in form
|
||||
// somehow, some code later on will panic on Data["ErrorMsg"].(string).
|
||||
// So initialize it to some default.
|
||||
data["ErrorMsg"] = l.Tr("form.unknown_error")
|
||||
AssignForm(f, data)
|
||||
data["ErrorMsg"] = l.TrString("form.unknown_error")
|
||||
|
||||
typ := reflect.TypeOf(f)
|
||||
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
if typ.Kind() == reflect.Pointer {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
if field, ok := typ.FieldByName(errs[0].FieldNames[0]); ok {
|
||||
fieldName := field.Tag.Get("form")
|
||||
if fieldName != "-" {
|
||||
data["Err_"+field.Name] = true
|
||||
|
||||
trName := field.Tag.Get("locale")
|
||||
if len(trName) == 0 {
|
||||
trName = l.TrString("form." + field.Name)
|
||||
} else {
|
||||
trName = l.TrString(trName)
|
||||
}
|
||||
|
||||
switch errs[0].Classification {
|
||||
case binding.ERR_REQUIRED:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.require_error")
|
||||
case binding.ERR_ALPHA_DASH:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error")
|
||||
case binding.ERR_ALPHA_DASH_DOT:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error")
|
||||
case validation.ErrGitRefName:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error")
|
||||
case binding.ERR_SIZE:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field))
|
||||
case binding.ERR_MIN_SIZE:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field))
|
||||
case binding.ERR_MAX_SIZE:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field))
|
||||
case binding.ERR_EMAIL:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.email_error")
|
||||
case binding.ERR_URL:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message)
|
||||
case binding.ERR_INCLUDE:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field))
|
||||
case validation.ErrGlobPattern:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message)
|
||||
case validation.ErrRegexPattern:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message)
|
||||
case validation.ErrUsername:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.username_error")
|
||||
case validation.ErrInvalidGroupTeamMap:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
|
||||
case validation.ErrInvalidBadgeSlug:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.invalid_slug_error")
|
||||
default:
|
||||
msg := errs[0].Classification
|
||||
if msg != "" && errs[0].Message != "" {
|
||||
msg += ": "
|
||||
}
|
||||
|
||||
msg += errs[0].Message
|
||||
if msg == "" {
|
||||
msg = l.TrString("form.unknown_error")
|
||||
}
|
||||
data["ErrorMsg"] = trName + ": " + msg
|
||||
}
|
||||
return errs
|
||||
}
|
||||
field, fieldExists := typ.FieldByName(errs[0].FieldNames[0])
|
||||
if !fieldExists {
|
||||
return errs
|
||||
}
|
||||
|
||||
if field.Tag.Get("form") == "-" {
|
||||
return errs
|
||||
}
|
||||
|
||||
data["Err_"+field.Name] = true
|
||||
|
||||
trName := field.Tag.Get("locale")
|
||||
if len(trName) == 0 {
|
||||
trName = l.TrString("form." + field.Name)
|
||||
} else {
|
||||
trName = l.TrString(trName)
|
||||
}
|
||||
|
||||
switch errs[0].Classification {
|
||||
case binding.ERR_REQUIRED:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.require_error")
|
||||
case binding.ERR_ALPHA_DASH:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error")
|
||||
case binding.ERR_ALPHA_DASH_DOT:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error")
|
||||
case validation.ErrGitRefName:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error")
|
||||
case binding.ERR_SIZE:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field))
|
||||
case binding.ERR_MIN_SIZE:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field))
|
||||
case binding.ERR_MAX_SIZE:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field))
|
||||
case binding.ERR_EMAIL:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.email_error")
|
||||
case binding.ERR_URL:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message)
|
||||
case binding.ERR_INCLUDE:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field))
|
||||
case validation.ErrGlobPattern:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message)
|
||||
case validation.ErrRegexPattern:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message)
|
||||
case validation.ErrUsername:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.username_error")
|
||||
case validation.ErrInvalidGroupTeamMap:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
|
||||
case validation.ErrInvalidBadgeSlug:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.invalid_slug_error")
|
||||
default:
|
||||
msg := errs[0].Classification
|
||||
if msg != "" && errs[0].Message != "" {
|
||||
msg += ": "
|
||||
}
|
||||
|
||||
msg += errs[0].Message
|
||||
if msg == "" {
|
||||
msg = l.TrString("form.unknown_error")
|
||||
}
|
||||
data["ErrorMsg"] = trName + ": " + msg
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,6 +35,11 @@ func DeleteRedirectToCookie(resp http.ResponseWriter) {
|
||||
}
|
||||
|
||||
func RedirectLinkUserLogin(req *http.Request) string {
|
||||
if req.Header.Get("X-Gitea-Fetch-Action") != "" {
|
||||
// when building the redirect link for a fetch request, the current link might be a partial page,
|
||||
// so we only redirect to the login page without redirect_to parameter
|
||||
return setting.AppSubURL + "/user/login"
|
||||
}
|
||||
return setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(setting.AppSubURL+req.URL.RequestURI())
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/public"
|
||||
"gitea.dev/modules/reqctx"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
const ContextDataKeySignedUser = "SignedUser"
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"gitea.dev/modules/reqctx"
|
||||
)
|
||||
|
||||
// Flash represents a one time data transfer between two requests.
|
||||
|
||||
@@ -6,12 +6,30 @@ package middleware
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/translation/i18n"
|
||||
"gitea.dev/modules/translation"
|
||||
"gitea.dev/modules/translation/i18n"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// maxAcceptLanguageLen bounds the Accept-Language header before it reaches
|
||||
// language.ParseAcceptLanguage. That parser has quadratic-time behavior on long
|
||||
// malformed inputs, and its built-in guard only counts "-" separators while the
|
||||
// scanner treats "_" as an alias for "-", so a "_"-heavy header slips past the
|
||||
// guard and burns CPU. Only the leading (highest-priority) languages are used, so
|
||||
// truncating a longer header is safe.
|
||||
const maxAcceptLanguageLen = 200
|
||||
|
||||
// parseAcceptLanguage parses the Accept-Language header after bounding its length
|
||||
// to avoid a quadratic-time DoS on attacker-controlled input.
|
||||
func parseAcceptLanguage(header string) []language.Tag {
|
||||
if len(header) > maxAcceptLanguageLen {
|
||||
header = header[:maxAcceptLanguageLen]
|
||||
}
|
||||
tags, _, _ := language.ParseAcceptLanguage(header)
|
||||
return tags
|
||||
}
|
||||
|
||||
// Locale handle locale
|
||||
func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale {
|
||||
// 1. Check URL arguments.
|
||||
@@ -35,7 +53,7 @@ func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale {
|
||||
// 3. Get language information from 'Accept-Language'.
|
||||
// The first element in the list is chosen to be the default language automatically.
|
||||
if len(lang) == 0 {
|
||||
tags, _, _ := language.ParseAcceptLanguage(req.Header.Get("Accept-Language"))
|
||||
tags := parseAcceptLanguage(req.Header.Get("Accept-Language"))
|
||||
tag := translation.Match(tags...)
|
||||
lang = tag.String()
|
||||
}
|
||||
@@ -51,9 +69,3 @@ func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale {
|
||||
func SetLocaleCookie(resp http.ResponseWriter, lang string, maxAge int) {
|
||||
SetSiteCookie(resp, "lang", lang, maxAge)
|
||||
}
|
||||
|
||||
// DeleteLocaleCookie convenience function to delete the locale cookie consistently
|
||||
// Setting the lang cookie will trigger the middleware to reset the language to previous state.
|
||||
func DeleteLocaleCookie(resp http.ResponseWriter) {
|
||||
SetSiteCookie(resp, "lang", "", -1)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseAcceptLanguage(t *testing.T) {
|
||||
// a normal header is parsed and its leading language preserved
|
||||
tags := parseAcceptLanguage("de-DE,de;q=0.9,en;q=0.8")
|
||||
assert.NotEmpty(t, tags)
|
||||
assert.Equal(t, "de-DE", tags[0].String())
|
||||
|
||||
// an oversized "_"-separated header would drive ParseAcceptLanguage into its
|
||||
// quadratic-time path (the built-in guard only counts "-"); the length bound
|
||||
// keeps the input passed to the parser small so it cannot be used for a DoS.
|
||||
malicious := strings.Repeat("_aaaaaaaaa", 1<<16) // ~640 KiB, zero "-" characters
|
||||
assert.Greater(t, len(malicious), maxAcceptLanguageLen)
|
||||
tags = parseAcceptLanguage(malicious)
|
||||
// no panic / hang, and nothing meaningful is parsed out of the garbage
|
||||
assert.Empty(t, tags)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ package web
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
// MockAfterMiddlewares is a general mock point, it's between middlewares and the handler
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -9,22 +9,16 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"gitea.dev/modules/htmlutil"
|
||||
"gitea.dev/modules/reqctx"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/web/middleware"
|
||||
"gitea.dev/modules/web/types"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// PreMiddlewareProvider is a special middleware provider which will be executed
|
||||
// before other middlewares on the same "routing" level (AfterRouting/Group/Methods/Any, but not BeforeRouting).
|
||||
// A route can do something (e.g.: set middleware options) at the place where it is declared,
|
||||
// and the code will be executed before other middlewares which are added before the declaration.
|
||||
// Use cases: mark a route with some meta info, set some options for middlewares, etc.
|
||||
type PreMiddlewareProvider func(next http.Handler) http.Handler
|
||||
|
||||
// Bind binding an obj to a handler's context data
|
||||
func Bind[T any](_ T) http.HandlerFunc {
|
||||
return func(resp http.ResponseWriter, req *http.Request) {
|
||||
@@ -112,7 +106,7 @@ func isNilOrFuncNil(v any) bool {
|
||||
|
||||
func wrapMiddlewareAppendPre(all []middlewareProvider, middlewares []any) []middlewareProvider {
|
||||
for _, m := range middlewares {
|
||||
if h, ok := m.(PreMiddlewareProvider); ok && h != nil {
|
||||
if h, ok := m.(types.PreMiddlewareProvider); ok && h != nil {
|
||||
all = append(all, toHandlerProvider(middlewareProvider(h)))
|
||||
}
|
||||
}
|
||||
@@ -121,7 +115,7 @@ func wrapMiddlewareAppendPre(all []middlewareProvider, middlewares []any) []midd
|
||||
|
||||
func wrapMiddlewareAppendNormal(all []middlewareProvider, middlewares []any) []middlewareProvider {
|
||||
for _, m := range middlewares {
|
||||
if _, ok := m.(PreMiddlewareProvider); !ok && !isNilOrFuncNil(m) {
|
||||
if _, ok := m.(types.PreMiddlewareProvider); !ok && !isNilOrFuncNil(m) {
|
||||
all = append(all, toHandlerProvider(m))
|
||||
}
|
||||
}
|
||||
@@ -266,7 +260,7 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques
|
||||
// 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)
|
||||
_, _ = resp.Write([]byte(htmlutil.HTMLFormat(`404 page not found, sub-path is: <a href="%s">%s</a>`, setting.AppSubURL, setting.AppSubURL)))
|
||||
_, _ = htmlutil.HTMLPrintf(resp, `404 page not found, sub-path is: <a href="%s">%s</a>`, setting.AppSubURL, setting.AppSubURL)
|
||||
return
|
||||
}
|
||||
normalized = true
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web/types"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -312,12 +313,12 @@ func TestPreMiddlewareProvider(t *testing.T) {
|
||||
root := NewRouter()
|
||||
root.BeforeRouting(h("before-root"))
|
||||
root.AfterRouting(h("root"))
|
||||
root.Get("/a/1", h("mid"), PreMiddlewareProvider(p("pre-root")), h("end1"))
|
||||
root.Get("/a/1", h("mid"), types.PreMiddlewareProvider(p("pre-root")), h("end1"))
|
||||
|
||||
sub := NewRouter()
|
||||
sub.BeforeRouting(h("before-sub"))
|
||||
sub.AfterRouting(h("sub"))
|
||||
sub.Get("/2", h("mid"), PreMiddlewareProvider(p("pre-sub")), h("end2"))
|
||||
sub.Get("/2", h("mid"), types.PreMiddlewareProvider(p("pre-sub")), h("end2"))
|
||||
sub.NotFound(h("not-found"))
|
||||
|
||||
root.Mount("/a", sub)
|
||||
|
||||
@@ -7,15 +7,21 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"gitea.dev/modules/gtprof"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/reqctx"
|
||||
"gitea.dev/modules/web/types"
|
||||
)
|
||||
|
||||
type contextKeyType struct{}
|
||||
|
||||
var contextKey contextKeyType
|
||||
|
||||
func getRequestRecord(ctx context.Context) *requestRecord {
|
||||
record, _ := ctx.Value(contextKey).(*requestRecord)
|
||||
return record
|
||||
}
|
||||
|
||||
// RecordFuncInfo records a func info into context
|
||||
func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) {
|
||||
end = func() {}
|
||||
@@ -24,7 +30,7 @@ func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) {
|
||||
traceSpan, end = gtprof.GetTracer().StartInContext(reqCtx, "http.func")
|
||||
traceSpan.SetAttributeString("func", funcInfo.shortName)
|
||||
}
|
||||
if record, ok := ctx.Value(contextKey).(*requestRecord); ok {
|
||||
if record := getRequestRecord(ctx); record != nil {
|
||||
record.lock.Lock()
|
||||
record.funcInfo = funcInfo
|
||||
record.lock.Unlock()
|
||||
@@ -32,22 +38,39 @@ func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) {
|
||||
return end
|
||||
}
|
||||
|
||||
// MarkLongPolling marks the request is a long-polling request, and the logger may output different message for it
|
||||
func MarkLongPolling(resp http.ResponseWriter, req *http.Request) {
|
||||
record, ok := req.Context().Value(contextKey).(*requestRecord)
|
||||
if !ok {
|
||||
return
|
||||
func GetRequestRecordInfo(reqCtx context.Context) (ret struct {
|
||||
HasRecord bool
|
||||
IsLongPolling bool
|
||||
},
|
||||
) {
|
||||
record := getRequestRecord(reqCtx)
|
||||
if record == nil {
|
||||
return ret
|
||||
}
|
||||
ret.HasRecord = true
|
||||
record.lock.RLock()
|
||||
ret.IsLongPolling = record.isLongPolling
|
||||
record.lock.RUnlock()
|
||||
return ret
|
||||
}
|
||||
|
||||
record.lock.Lock()
|
||||
record.isLongPolling = true
|
||||
record.logLevel = log.TRACE
|
||||
record.lock.Unlock()
|
||||
// MarkLongPolling marks the request is a long-polling request, and the logger may output different message for it
|
||||
func MarkLongPolling() types.PreMiddlewareProvider {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
record := getRequestRecord(req.Context()) // it must exist
|
||||
record.lock.Lock()
|
||||
record.isLongPolling = true
|
||||
record.logLevel = log.TRACE
|
||||
record.lock.Unlock()
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func MarkLogLevelTrace(resp http.ResponseWriter, req *http.Request) {
|
||||
record, ok := req.Context().Value(contextKey).(*requestRecord)
|
||||
if !ok {
|
||||
record := getRequestRecord(req.Context())
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,8 +81,8 @@ func MarkLogLevelTrace(resp http.ResponseWriter, req *http.Request) {
|
||||
|
||||
// UpdatePanicError updates a context's error info, a panic may be recovered by other middlewares, but we still need to know that.
|
||||
func UpdatePanicError(ctx context.Context, err error) {
|
||||
record, ok := ctx.Value(contextKey).(*requestRecord)
|
||||
if !ok {
|
||||
record := getRequestRecord(ctx)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ func copyFuncInfo(l *FuncInfo) *FuncInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// shortenFilename generates a short source code filename from a full package path, eg: "code.gitea.io/routers/common/logger_context.go" => "common/logger_context.go"
|
||||
// shortenFilename generates a short source code filename from a full package path, eg: "gitea.dev/routers/common/logger_context.go" => "common/logger_context.go"
|
||||
func shortenFilename(filename, fallback string) string {
|
||||
if filename == "" {
|
||||
return fallback
|
||||
|
||||
@@ -17,7 +17,7 @@ func Test_shortenFilename(t *testing.T) {
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"code.gitea.io/routers/common/logger_context.go",
|
||||
"gitea.dev/routers/common/logger_context.go",
|
||||
"NO_FALLBACK",
|
||||
"common/logger_context.go",
|
||||
},
|
||||
|
||||
@@ -7,23 +7,10 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/web/types"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/web/types"
|
||||
)
|
||||
|
||||
// NewLoggerHandler is a handler that will log routing to the router log taking account of
|
||||
// routing information
|
||||
func NewLoggerHandler() func(next http.Handler) http.Handler {
|
||||
manager := requestRecordsManager{
|
||||
requestRecords: map[uint64]*requestRecord{},
|
||||
}
|
||||
manager.startSlowQueryDetector(3 * time.Second)
|
||||
|
||||
logger := log.GetLogger("router")
|
||||
manager.print = logPrinter(logger)
|
||||
return manager.handler
|
||||
}
|
||||
|
||||
var (
|
||||
startMessage = log.NewColoredValue("started ", log.DEBUG.ColorAttributes()...)
|
||||
slowMessage = log.NewColoredValue("slow ", log.WARN.ColorAttributes()...)
|
||||
@@ -89,7 +76,7 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) {
|
||||
}
|
||||
|
||||
var status int
|
||||
if v, ok := record.responseWriter.(types.ResponseStatusProvider); ok {
|
||||
if v, ok := record.respWriter.(types.ResponseStatusProvider); ok {
|
||||
status = v.WrittenStatus()
|
||||
}
|
||||
logLevel := record.logLevel
|
||||
|
||||
@@ -6,13 +6,12 @@ package routing
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/process"
|
||||
)
|
||||
|
||||
// Event indicates when the printer is triggered
|
||||
@@ -29,26 +28,21 @@ const (
|
||||
EndEvent
|
||||
)
|
||||
|
||||
// Printer is used to output the log for a request
|
||||
type Printer func(trigger Event, record *requestRecord)
|
||||
// logPrinterFunc is used to output the log for a request
|
||||
type logPrinterFunc func(trigger Event, record *requestRecord)
|
||||
|
||||
type requestRecordsManager struct {
|
||||
print Printer
|
||||
|
||||
lock sync.Mutex
|
||||
|
||||
requestRecords map[uint64]*requestRecord
|
||||
count uint64
|
||||
type loggerRequestManager struct {
|
||||
logPrint logPrinterFunc
|
||||
reqRecords sync.Map // it only contains the active requests which haven't been detected as "slow"
|
||||
}
|
||||
|
||||
func (manager *requestRecordsManager) startSlowQueryDetector(threshold time.Duration) {
|
||||
func (manager *loggerRequestManager) startSlowQueryDetector(threshold time.Duration) {
|
||||
go graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
|
||||
ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Service: SlowQueryDetector", process.SystemProcessType, true)
|
||||
defer finished()
|
||||
// This go-routine checks all active requests every second.
|
||||
// If a request has been running for a long time (eg: /user/events), we also print a log with "still-executing" message
|
||||
// After the "still-executing" log is printed, the record will be removed from the map to prevent from duplicated logs in future
|
||||
|
||||
// We do not care about accurate duration here. It just does the check periodically, 0.5s or 1.5s are all OK.
|
||||
t := time.NewTicker(time.Second)
|
||||
for {
|
||||
@@ -58,69 +52,39 @@ func (manager *requestRecordsManager) startSlowQueryDetector(threshold time.Dura
|
||||
case <-t.C:
|
||||
now := time.Now()
|
||||
|
||||
var slowRequests []*requestRecord
|
||||
|
||||
// find all slow requests with lock
|
||||
manager.lock.Lock()
|
||||
for index, record := range manager.requestRecords {
|
||||
if now.Sub(record.startTime) < threshold {
|
||||
continue
|
||||
}
|
||||
|
||||
slowRequests = append(slowRequests, record)
|
||||
delete(manager.requestRecords, index)
|
||||
}
|
||||
manager.lock.Unlock()
|
||||
|
||||
// print logs for slow requests
|
||||
for _, record := range slowRequests {
|
||||
manager.print(StillExecutingEvent, record)
|
||||
}
|
||||
manager.reqRecords.Range(func(key, value any) bool {
|
||||
index, record := key.(uint64), value.(*requestRecord)
|
||||
if now.Sub(record.startTime) >= threshold {
|
||||
manager.logPrint(StillExecutingEvent, record)
|
||||
manager.reqRecords.Delete(index)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *requestRecordsManager) handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
record := &requestRecord{
|
||||
startTime: time.Now(),
|
||||
request: req,
|
||||
responseWriter: w,
|
||||
func (manager *loggerRequestManager) handleRequestRecord(record *requestRecord) func() {
|
||||
manager.reqRecords.Store(record.index, record)
|
||||
manager.logPrint(StartEvent, record)
|
||||
|
||||
return func() {
|
||||
// just in case there is a panic. now the panics are all recovered in middleware.go
|
||||
localPanicErr := recover()
|
||||
if localPanicErr != nil {
|
||||
record.lock.Lock()
|
||||
record.panicError = fmt.Errorf("%v\n%s", localPanicErr, log.Stack(2))
|
||||
record.lock.Unlock()
|
||||
}
|
||||
|
||||
// generate a record index an insert into the map
|
||||
manager.lock.Lock()
|
||||
record.index = manager.count
|
||||
manager.count++
|
||||
manager.requestRecords[record.index] = record
|
||||
manager.lock.Unlock()
|
||||
manager.reqRecords.Delete(record.index)
|
||||
manager.logPrint(EndEvent, record)
|
||||
|
||||
defer func() {
|
||||
// just in case there is a panic. now the panics are all recovered in middleware.go
|
||||
localPanicErr := recover()
|
||||
if localPanicErr != nil {
|
||||
record.lock.Lock()
|
||||
record.panicError = fmt.Errorf("%v\n%s", localPanicErr, log.Stack(2))
|
||||
record.lock.Unlock()
|
||||
}
|
||||
|
||||
// remove from the record map
|
||||
manager.lock.Lock()
|
||||
delete(manager.requestRecords, record.index)
|
||||
manager.lock.Unlock()
|
||||
|
||||
// log the end of the request
|
||||
manager.print(EndEvent, record)
|
||||
|
||||
if localPanicErr != nil {
|
||||
// the panic wasn't recovered before us, so we should pass it up, and let the framework handle the panic error
|
||||
panic(localPanicErr)
|
||||
}
|
||||
}()
|
||||
|
||||
req = req.WithContext(context.WithValue(req.Context(), contextKey, record))
|
||||
manager.print(StartEvent, record)
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
if localPanicErr != nil {
|
||||
// the panic wasn't recovered before us, so we should pass it up, and let the framework handle the panic error
|
||||
panic(localPanicErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
// NewRequestInfoHandler is a handler that saves request info into request context.
|
||||
// If router logger is enabled, it will also print request logs and detect slow requests.
|
||||
func NewRequestInfoHandler() func(next http.Handler) http.Handler {
|
||||
var reqLogger *loggerRequestManager
|
||||
if setting.IsRouteLogEnabled() {
|
||||
reqLogger = &loggerRequestManager{
|
||||
logPrint: logPrinter(log.GetLogger("router")),
|
||||
}
|
||||
reqLogger.startSlowQueryDetector(3 * time.Second)
|
||||
}
|
||||
var requestCounter atomic.Uint64
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
record := &requestRecord{
|
||||
index: requestCounter.Add(1),
|
||||
startTime: time.Now(),
|
||||
respWriter: w,
|
||||
}
|
||||
req = req.WithContext(context.WithValue(req.Context(), contextKey, record))
|
||||
record.request = req
|
||||
if reqLogger != nil {
|
||||
end := reqLogger.handleRequestRecord(record)
|
||||
defer end()
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,24 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
type requestRecord struct {
|
||||
// index of the record in the records map
|
||||
index uint64
|
||||
|
||||
// immutable fields
|
||||
startTime time.Time
|
||||
request *http.Request
|
||||
responseWriter http.ResponseWriter
|
||||
index uint64 // unique number (per process) for the request
|
||||
startTime time.Time
|
||||
request *http.Request
|
||||
respWriter http.ResponseWriter
|
||||
|
||||
// mutex
|
||||
lock sync.RWMutex
|
||||
|
||||
// mutable fields
|
||||
// below are mutable fields
|
||||
funcInfo *FuncInfo
|
||||
// * for "mark as long polling"
|
||||
isLongPolling bool
|
||||
logLevel log.Level
|
||||
funcInfo *FuncInfo
|
||||
panicError error
|
||||
// * for router logger
|
||||
logLevel log.Level
|
||||
panicError error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package types
|
||||
|
||||
import "net/http"
|
||||
|
||||
// PreMiddlewareProvider is a special middleware provider which will be executed
|
||||
// before other middlewares on the same "routing" level (AfterRouting/Group/Methods/Any, but not BeforeRouting).
|
||||
// A route can do something (e.g.: set middleware options) at the place where it is declared,
|
||||
// and the code will be executed before other middlewares which are added before the declaration.
|
||||
// Use cases: mark a route with some meta info, set some options for middlewares, etc.
|
||||
type PreMiddlewareProvider func(next http.Handler) http.Handler
|
||||
Reference in New Issue
Block a user