diff options
Diffstat (limited to 'modules/web')
-rw-r--r-- | modules/web/middleware/binding.go | 4 | ||||
-rw-r--r-- | modules/web/middleware/flash.go | 25 | ||||
-rw-r--r-- | modules/web/middleware/request.go | 14 | ||||
-rw-r--r-- | modules/web/routemock_test.go | 22 | ||||
-rw-r--r-- | modules/web/router.go | 4 | ||||
-rw-r--r-- | modules/web/router_path.go | 43 | ||||
-rw-r--r-- | modules/web/router_test.go | 88 | ||||
-rw-r--r-- | modules/web/routing/context.go | 11 | ||||
-rw-r--r-- | modules/web/routing/logger.go | 32 |
9 files changed, 152 insertions, 91 deletions
diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 43e1bbc70e..ee4eca976e 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -50,7 +50,7 @@ func AssignForm(form any, data map[string]any) { } func getRuleBody(field reflect.StructField, prefix string) string { - for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { + for rule := range strings.SplitSeq(field.Tag.Get("binding"), ";") { if strings.HasPrefix(rule, prefix) { return rule[len(prefix) : len(rule)-1] } @@ -78,7 +78,7 @@ func GetInclude(field reflect.StructField) string { return getRuleBody(field, "Include(") } -// Validate validate TODO: +// Validate validate func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors { if errs.Len() == 0 { return errs diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go index 0caaa8c036..0e848c7902 100644 --- a/modules/web/middleware/flash.go +++ b/modules/web/middleware/flash.go @@ -6,6 +6,7 @@ package middleware import ( "fmt" "html/template" + "net/http" "net/url" "code.gitea.io/gitea/modules/reqctx" @@ -65,3 +66,27 @@ func (f *Flash) Success(msg any, current ...bool) { f.SuccessMsg = flashMsgStringOrHTML(msg) f.set("success", f.SuccessMsg, current...) } + +func ParseCookieFlashMessage(val string) *Flash { + if vals, _ := url.ParseQuery(val); len(vals) > 0 { + return &Flash{ + Values: vals, + ErrorMsg: vals.Get("error"), + SuccessMsg: vals.Get("success"), + InfoMsg: vals.Get("info"), + WarningMsg: vals.Get("warning"), + } + } + return nil +} + +func GetSiteCookieFlashMessage(dataStore reqctx.RequestDataStore, req *http.Request, cookieName string) (string, *Flash) { + // Get the last flash message from cookie + lastFlashCookie := GetSiteCookie(req, cookieName) + lastFlashMsg := ParseCookieFlashMessage(lastFlashCookie) + if lastFlashMsg != nil { + lastFlashMsg.DataStore = dataStore + return lastFlashCookie, lastFlashMsg + } + return lastFlashCookie, nil +} diff --git a/modules/web/middleware/request.go b/modules/web/middleware/request.go deleted file mode 100644 index 0bb155df70..0000000000 --- a/modules/web/middleware/request.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package middleware - -import ( - "net/http" - "strings" -) - -// IsAPIPath returns true if the specified URL is an API path -func IsAPIPath(req *http.Request) bool { - return strings.HasPrefix(req.URL.Path, "/api/") -} diff --git a/modules/web/routemock_test.go b/modules/web/routemock_test.go index 89cfaacdd1..a0949bf622 100644 --- a/modules/web/routemock_test.go +++ b/modules/web/routemock_test.go @@ -30,13 +30,13 @@ func TestRouteMock(t *testing.T) { // normal request recorder := httptest.NewRecorder() - req, err := http.NewRequest("GET", "http://localhost:8000/foo", nil) + req, err := http.NewRequest(http.MethodGet, "http://localhost:8000/foo", nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.Len(t, recorder.Header(), 3) - assert.EqualValues(t, "m1", recorder.Header().Get("X-Test-Middleware1")) - assert.EqualValues(t, "m2", recorder.Header().Get("X-Test-Middleware2")) - assert.EqualValues(t, "h", recorder.Header().Get("X-Test-Handler")) + assert.Equal(t, "m1", recorder.Header().Get("X-Test-Middleware1")) + assert.Equal(t, "m2", recorder.Header().Get("X-Test-Middleware2")) + assert.Equal(t, "h", recorder.Header().Get("X-Test-Handler")) RouteMockReset() // mock at "mock-point" @@ -45,12 +45,12 @@ func TestRouteMock(t *testing.T) { resp.WriteHeader(http.StatusOK) }) recorder = httptest.NewRecorder() - req, err = http.NewRequest("GET", "http://localhost:8000/foo", nil) + req, err = http.NewRequest(http.MethodGet, "http://localhost:8000/foo", nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.Len(t, recorder.Header(), 2) - assert.EqualValues(t, "m1", recorder.Header().Get("X-Test-Middleware1")) - assert.EqualValues(t, "a", recorder.Header().Get("X-Test-MockPoint")) + assert.Equal(t, "m1", recorder.Header().Get("X-Test-Middleware1")) + assert.Equal(t, "a", recorder.Header().Get("X-Test-MockPoint")) RouteMockReset() // mock at MockAfterMiddlewares @@ -59,12 +59,12 @@ func TestRouteMock(t *testing.T) { resp.WriteHeader(http.StatusOK) }) recorder = httptest.NewRecorder() - req, err = http.NewRequest("GET", "http://localhost:8000/foo", nil) + req, err = http.NewRequest(http.MethodGet, "http://localhost:8000/foo", nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.Len(t, recorder.Header(), 3) - assert.EqualValues(t, "m1", recorder.Header().Get("X-Test-Middleware1")) - assert.EqualValues(t, "m2", recorder.Header().Get("X-Test-Middleware2")) - assert.EqualValues(t, "b", recorder.Header().Get("X-Test-MockPoint")) + assert.Equal(t, "m1", recorder.Header().Get("X-Test-Middleware1")) + assert.Equal(t, "m2", recorder.Header().Get("X-Test-Middleware2")) + assert.Equal(t, "b", recorder.Header().Get("X-Test-MockPoint")) RouteMockReset() } diff --git a/modules/web/router.go b/modules/web/router.go index da06b955b1..5812ff69d4 100644 --- a/modules/web/router.go +++ b/modules/web/router.go @@ -125,8 +125,8 @@ func (r *Router) Methods(methods, pattern string, h ...any) { middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) fullPattern := r.getPattern(pattern) if strings.Contains(methods, ",") { - methods := strings.Split(methods, ",") - for _, method := range methods { + methods := strings.SplitSeq(methods, ",") + for method := range methods { r.chiRouter.With(middlewares...).Method(strings.TrimSpace(method), fullPattern, handlerFunc) } } else { diff --git a/modules/web/router_path.go b/modules/web/router_path.go index b59948581a..ce041eedab 100644 --- a/modules/web/router_path.go +++ b/modules/web/router_path.go @@ -4,9 +4,9 @@ package web import ( - "fmt" "net/http" "regexp" + "slices" "strings" "code.gitea.io/gitea/modules/container" @@ -37,11 +37,21 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req) } +type RouterPathGroupPattern struct { + re *regexp.Regexp + params []routerPathParam + middlewares []any +} + // MatchPath matches the request method, and uses regexp to match the path. -// The pattern uses "<...>" to define path parameters, for example: "/<name>" (different from chi router) -// It is only designed to resolve some special cases which chi router can't handle. +// The pattern uses "<...>" to define path parameters, for example, "/<name>" (different from chi router) +// It is only designed to resolve some special cases that 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 (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) { + g.MatchPattern(methods, g.PatternRegexp(pattern), h...) +} + +func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupPattern, h ...any) { g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...)) } @@ -97,29 +107,35 @@ func isValidMethod(name string) bool { return false } -func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { - middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) +func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, h ...any) *routerPathMatcher { + middlewares, handlerFunc := wrapMiddlewareAndHandler(patternRegexp.middlewares, h) p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} - for _, method := range strings.Split(methods, ",") { + for method := range strings.SplitSeq(methods, ",") { method = strings.TrimSpace(method) if !isValidMethod(method) { - panic(fmt.Sprintf("invalid HTTP method: %s", method)) + panic("invalid HTTP method: " + method) } p.methods.Add(method) } + p.re, p.params = patternRegexp.re, patternRegexp.params + return p +} + +func patternRegexp(pattern string, h ...any) *RouterPathGroupPattern { + p := &RouterPathGroupPattern{middlewares: slices.Clone(h)} re := []byte{'^'} lastEnd := 0 for lastEnd < len(pattern) { start := strings.IndexByte(pattern[lastEnd:], '<') if start == -1 { - re = append(re, pattern[lastEnd:]...) + re = append(re, regexp.QuoteMeta(pattern[lastEnd:])...) break } end := strings.IndexByte(pattern[lastEnd+start:], '>') if end == -1 { - panic(fmt.Sprintf("invalid pattern: %s", pattern)) + panic("invalid pattern: " + pattern) } - re = append(re, pattern[lastEnd:lastEnd+start]...) + re = append(re, regexp.QuoteMeta(pattern[lastEnd:lastEnd+start])...) partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") lastEnd += start + end + 1 @@ -141,7 +157,10 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher p.params = append(p.params, param) } re = append(re, '$') - reStr := string(re) - p.re = regexp.MustCompile(reStr) + p.re = regexp.MustCompile(string(re)) return p } + +func (g *RouterPathGroup) PatternRegexp(pattern string, h ...any) *RouterPathGroupPattern { + return patternRegexp(pattern, h...) +} diff --git a/modules/web/router_test.go b/modules/web/router_test.go index 582980a27a..1cee2b879b 100644 --- a/modules/web/router_test.go +++ b/modules/web/router_test.go @@ -34,7 +34,7 @@ func TestPathProcessor(t *testing.T) { testProcess := func(pattern, uri string, expectedPathParams map[string]string) { chiCtx := chi.NewRouteContext() chiCtx.RouteMethod = "GET" - p := newRouterPathMatcher("GET", pattern, http.NotFound) + p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound) assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri) } @@ -51,23 +51,25 @@ func TestPathProcessor(t *testing.T) { } func TestRouter(t *testing.T) { - buff := bytes.NewBufferString("") + buff := &bytes.Buffer{} recorder := httptest.NewRecorder() recorder.Body = buff type resultStruct struct { - method string - pathParams map[string]string - handlerMark string + method string + pathParams map[string]string + handlerMarks []string } - var res resultStruct + var res resultStruct h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { mark := util.OptionalArg(optMark, "") return func(resp http.ResponseWriter, req *http.Request) { res.method = req.Method res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context())) - res.handlerMark = mark + if mark != "" { + res.handlerMarks = append(res.handlerMarks, mark) + } } } @@ -77,6 +79,8 @@ func TestRouter(t *testing.T) { if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { h(stop)(resp, req) resp.WriteHeader(http.StatusOK) + } else if mark != "" { + res.handlerMarks = append(res.handlerMarks, mark) } } } @@ -108,7 +112,7 @@ func TestRouter(t *testing.T) { m.Delete("", h()) }) m.PathGroup("/*", func(g *RouterPathGroup) { - g.MatchPath("GET", `/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2"), h("match-path")) + g.MatchPattern("GET", g.PatternRegexp(`/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2")), stopMark("s3"), h("match-path")) }, stopMark("s1")) }) }) @@ -121,36 +125,36 @@ func TestRouter(t *testing.T) { req, err := http.NewRequest(methodPathFields[0], methodPathFields[1], nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) - assert.EqualValues(t, expected, res) + assert.Equal(t, expected, res) }) } t.Run("RootRouter", func(t *testing.T) { - testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"}) + testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/"}}) testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, - handlerMark: "list-issues-b", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, + handlerMarks: []string{"list-issues-b"}, }) testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, - handlerMark: "view-issue", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, + handlerMarks: []string{"view-issue"}, }) testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, - handlerMark: "hijack", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, + handlerMarks: []string{"hijack"}, }) testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{ - method: "POST", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, - handlerMark: "update-issue", + method: "POST", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, + handlerMarks: []string{"update-issue"}, }) }) t.Run("Sub Router", func(t *testing.T) { - testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"}) + testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/api/v1"}}) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, @@ -179,31 +183,37 @@ func TestRouter(t *testing.T) { t.Run("MatchPath", func(t *testing.T) { testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, - handlerMark: "match-path", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3", "match-path"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, - handlerMark: "match-path", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3", "match-path"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{ - method: "GET", - pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, - handlerMark: "not-found:/api/v1", + method: "GET", + pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, + handlerMarks: []string{"s1", "not-found:/api/v1"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, - handlerMark: "s1", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, + handlerMarks: []string{"s1"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, - handlerMark: "s2", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2"}, + }) + + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s3", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3"}, }) }) } @@ -224,7 +234,7 @@ func TestRouteNormalizePath(t *testing.T) { actualPaths.Path = req.URL.Path }) - req, err := http.NewRequest("GET", reqPath, nil) + req, err := http.NewRequest(http.MethodGet, reqPath, nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.Equal(t, expectedPaths, actualPaths, "req path = %q", reqPath) diff --git a/modules/web/routing/context.go b/modules/web/routing/context.go index fbf371b839..d3eb98f83d 100644 --- a/modules/web/routing/context.go +++ b/modules/web/routing/context.go @@ -6,6 +6,9 @@ package routing import ( "context" "net/http" + + "code.gitea.io/gitea/modules/gtprof" + "code.gitea.io/gitea/modules/reqctx" ) type contextKeyType struct{} @@ -14,10 +17,12 @@ var contextKey contextKeyType // RecordFuncInfo records a func info into context func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) { - // TODO: reqCtx := reqctx.FromContext(ctx), add trace support end = func() {} - - // save the func info into the context record + if reqCtx := reqctx.FromContext(ctx); reqCtx != nil { + var traceSpan *gtprof.TraceSpan + traceSpan, end = gtprof.GetTracer().StartInContext(reqCtx, "http.func") + traceSpan.SetAttributeString("func", funcInfo.shortName) + } if record, ok := ctx.Value(contextKey).(*requestRecord); ok { record.lock.Lock() record.funcInfo = funcInfo diff --git a/modules/web/routing/logger.go b/modules/web/routing/logger.go index 5f3a7592af..3bca9b3420 100644 --- a/modules/web/routing/logger.go +++ b/modules/web/routing/logger.go @@ -35,6 +35,19 @@ var ( ) func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { + const callerName = "HTTPRequest" + logTrace := func(fmt string, args ...any) { + logger.Log(2, &log.Event{Level: log.TRACE, Caller: callerName}, fmt, args...) + } + logInfo := func(fmt string, args ...any) { + logger.Log(2, &log.Event{Level: log.INFO, Caller: callerName}, fmt, args...) + } + logWarn := func(fmt string, args ...any) { + logger.Log(2, &log.Event{Level: log.WARN, Caller: callerName}, fmt, args...) + } + logError := func(fmt string, args ...any) { + logger.Log(2, &log.Event{Level: log.ERROR, Caller: callerName}, fmt, args...) + } return func(trigger Event, record *requestRecord) { if trigger == StartEvent { if !logger.LevelEnabled(log.TRACE) { @@ -44,7 +57,7 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { } // when a request starts, we have no information about the handler function information, we only have the request path req := record.request - logger.Trace("router: %s %v %s for %s", startMessage, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr) + logTrace("router: %s %v %s for %s", startMessage, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr) return } @@ -60,9 +73,9 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { if trigger == StillExecutingEvent { message := slowMessage - logf := logger.Warn + logf := logWarn if isLongPolling { - logf = logger.Info + logf = logInfo message = pollingMessage } logf("router: %s %v %s for %s, elapsed %v @ %s", @@ -75,7 +88,7 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { } if panicErr != nil { - logger.Warn("router: %s %v %s for %s, panic in %v @ %s, err=%v", + logWarn("router: %s %v %s for %s, panic in %v @ %s, err=%v", failedMessage, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr, log.ColoredTime(time.Since(record.startTime)), @@ -89,13 +102,16 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { if v, ok := record.responseWriter.(types.ResponseStatusProvider); ok { status = v.WrittenStatus() } - logf := logger.Info - if strings.HasPrefix(req.RequestURI, "/assets/") { - logf = logger.Trace + logf := logInfo + // lower the log level for some specific requests, in most cases these logs are not useful + if strings.HasPrefix(req.RequestURI, "/assets/") /* static assets */ || + req.RequestURI == "/user/events" /* Server-Sent Events (SSE) handler */ || + req.RequestURI == "/api/actions/runner.v1.RunnerService/FetchTask" /* Actions Runner polling */ { + logf = logTrace } message := completedMessage if isUnknownHandler { - logf = logger.Error + logf = logError message = unknownHandlerMessage } |