From 92fd3fc4fd369b6a8c0a022a32a80dec2340223a Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 27 Apr 2023 14:06:45 +0800 Subject: [PATCH] Refactor "route" related code, fix Safari cookie bug (#24330) Fix #24176 Clean some misuses of route package, clean some legacy FIXMEs --------- Co-authored-by: Giteabot --- modules/context/api.go | 1 + modules/context/context.go | 20 ++++ modules/context/context_test.go | 40 +++++++ modules/web/handler.go | 38 ++----- modules/web/route.go | 20 +--- routers/common/middleware.go | 39 ++++--- routers/init.go | 7 +- routers/install/routes.go | 32 ++---- routers/install/routes_test.go | 13 ++- routers/web/base.go | 11 +- routers/web/misc/misc.go | 49 +++++++++ routers/web/repo/repo.go | 18 ++++ routers/web/user/home.go | 50 +++++++++ routers/web/web.go | 179 +++++--------------------------- 14 files changed, 264 insertions(+), 253 deletions(-) create mode 100644 modules/context/context_test.go create mode 100644 routers/web/misc/misc.go diff --git a/modules/context/api.go b/modules/context/api.go index d405c9972..ae245ec1c 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -343,6 +343,7 @@ func RepoRefForAPI(next http.Handler) http.Handler { } ctx.Repo.Commit = commit ctx.Repo.TreePath = ctx.Params("*") + next.ServeHTTP(w, req) return } diff --git a/modules/context/context.go b/modules/context/context.go index f262c7cce..702da8a96 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -446,6 +446,17 @@ func (ctx *Context) JSON(status int, content interface{}) { } } +func removeSessionCookieHeader(w http.ResponseWriter) { + cookies := w.Header()["Set-Cookie"] + w.Header().Del("Set-Cookie") + for _, cookie := range cookies { + if strings.HasPrefix(cookie, setting.SessionConfig.CookieName+"=") { + continue + } + w.Header().Add("Set-Cookie", cookie) + } +} + // Redirect redirects the request func (ctx *Context) Redirect(location string, status ...int) { code := http.StatusSeeOther @@ -453,6 +464,15 @@ func (ctx *Context) Redirect(location string, status ...int) { code = status[0] } + if strings.Contains(location, "://") || strings.HasPrefix(location, "//") { + // Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path + // 1. the first request to "/my-path" contains cookie + // 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking) + // 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser + // 4. then the browser accepts the empty session, then the user is logged out + // So in this case, we should remove the session cookie from the response header + removeSessionCookieHeader(ctx.Resp) + } http.Redirect(ctx.Resp, ctx.Req, location, code) } diff --git a/modules/context/context_test.go b/modules/context/context_test.go new file mode 100644 index 000000000..e1460c1fd --- /dev/null +++ b/modules/context/context_test.go @@ -0,0 +1,40 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +type mockResponseWriter struct { + header http.Header +} + +func (m *mockResponseWriter) Header() http.Header { + return m.header +} + +func (m *mockResponseWriter) Write(bytes []byte) (int, error) { + panic("implement me") +} + +func (m *mockResponseWriter) WriteHeader(statusCode int) { + panic("implement me") +} + +func TestRemoveSessionCookieHeader(t *testing.T) { + w := &mockResponseWriter{} + w.header = http.Header{} + w.header.Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String()) + w.header.Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String()) + assert.Len(t, w.Header().Values("Set-Cookie"), 2) + removeSessionCookieHeader(w) + assert.Len(t, w.Header().Values("Set-Cookie"), 1) + assert.Contains(t, "other=bar", w.Header().Get("Set-Cookie")) +} diff --git a/modules/web/handler.go b/modules/web/handler.go index 8a44673f1..bfb83820c 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "reflect" - "strings" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/web/routing" @@ -131,16 +130,22 @@ func hasResponseBeenWritten(argsIn []reflect.Value) bool { // toHandlerProvider converts a handler to a handler provider // A handler provider is a function that takes a "next" http.Handler, it can be used as a middleware func toHandlerProvider(handler any) func(next http.Handler) http.Handler { - if hp, ok := handler.(func(next http.Handler) http.Handler); ok { - return hp - } - funcInfo := routing.GetFuncInfo(handler) fn := reflect.ValueOf(handler) if fn.Type().Kind() != reflect.Func { panic(fmt.Sprintf("handler must be a function, but got %s", fn.Type())) } + if hp, ok := handler.(func(next http.Handler) http.Handler); ok { + return func(next http.Handler) http.Handler { + h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + h.ServeHTTP(resp, req) + }) + } + } + provider := func(next http.Handler) http.Handler { return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { // wrap the response writer to check whether the response has been written @@ -175,26 +180,3 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler { provider(nil).ServeHTTP(nil, nil) // do a pre-check to make sure all arguments and return values are supported return provider } - -// MiddlewareWithPrefix wraps a handler function at a prefix, and make it as a middleware -// TODO: this design is incorrect, the asset handler should not be a middleware -func MiddlewareWithPrefix(pathPrefix string, middleware func(handler http.Handler) http.Handler, handlerFunc http.HandlerFunc) func(next http.Handler) http.Handler { - funcInfo := routing.GetFuncInfo(handlerFunc) - handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - handlerFunc(resp, req) - }) - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - if !strings.HasPrefix(req.URL.Path, pathPrefix) { - next.ServeHTTP(resp, req) - return - } - if middleware != nil { - middleware(handler).ServeHTTP(resp, req) - } else { - handler.ServeHTTP(resp, req) - } - }) - } -} diff --git a/modules/web/route.go b/modules/web/route.go index fe3588084..6fd60f4ca 100644 --- a/modules/web/route.go +++ b/modules/web/route.go @@ -44,23 +44,13 @@ type Route struct { // NewRoute creates a new route func NewRoute() *Route { r := chi.NewRouter() - return &Route{ - R: r, - curGroupPrefix: "", - curMiddlewares: []interface{}{}, - } + return &Route{R: r} } // Use supports two middlewares func (r *Route) Use(middlewares ...interface{}) { - if r.curGroupPrefix != "" { - // FIXME: this behavior is incorrect, should use "With" instead - r.curMiddlewares = append(r.curMiddlewares, middlewares...) - } else { - // FIXME: another misuse, the "Use" with empty middlewares is called after "Mount" - for _, m := range middlewares { - r.R.Use(toHandlerProvider(m)) - } + for _, m := range middlewares { + r.R.Use(toHandlerProvider(m)) } } @@ -116,9 +106,7 @@ func (r *Route) Methods(method, pattern string, h []any) { // Mount attaches another Route along ./pattern/* func (r *Route) Mount(pattern string, subR *Route) { - middlewares := make([]interface{}, len(r.curMiddlewares)) - copy(middlewares, r.curMiddlewares) - subR.Use(middlewares...) + subR.Use(r.curMiddlewares...) r.R.Mount(r.getPattern(pattern), subR.R) } diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 2abdcb583..28ecf934e 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -15,24 +15,23 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/routing" + "gitea.com/go-chi/session" "github.com/chi-middleware/proxy" chi "github.com/go-chi/chi/v5" ) -// Middlewares returns common middlewares -func Middlewares() []func(http.Handler) http.Handler { - handlers := []func(http.Handler) http.Handler{ - func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - // First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL - req.URL.RawPath = req.URL.EscapedPath() +// ProtocolMiddlewares returns HTTP protocol related middlewares +func ProtocolMiddlewares() (handlers []any) { + handlers = append(handlers, func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + // First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL + req.URL.RawPath = req.URL.EscapedPath() - ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true) - defer finished() - next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx))) - }) - }, - } + ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true) + defer finished() + next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx))) + }) + }) if setting.ReverseProxyLimit > 0 { opt := proxy.NewForwardedHeadersOptions(). @@ -112,3 +111,17 @@ func stripSlashesMiddleware(next http.Handler) http.Handler { next.ServeHTTP(resp, req) }) } + +func Sessioner() func(next http.Handler) http.Handler { + return session.Sessioner(session.Options{ + Provider: setting.SessionConfig.Provider, + ProviderConfig: setting.SessionConfig.ProviderConfig, + CookieName: setting.SessionConfig.CookieName, + CookiePath: setting.SessionConfig.CookiePath, + Gclifetime: setting.SessionConfig.Gclifetime, + Maxlifetime: setting.SessionConfig.Maxlifetime, + Secure: setting.SessionConfig.Secure, + SameSite: setting.SessionConfig.SameSite, + Domain: setting.SessionConfig.Domain, + }) +} diff --git a/routers/init.go b/routers/init.go index af768abbf..2c26bb5b0 100644 --- a/routers/init.go +++ b/routers/init.go @@ -177,20 +177,15 @@ func GlobalInitInstalled(ctx context.Context) { func NormalRoutes(ctx context.Context) *web.Route { ctx, _ = templates.HTMLRenderer(ctx) r := web.NewRoute() - for _, middle := range common.Middlewares() { - r.Use(middle) - } + r.Use(common.ProtocolMiddlewares()...) r.Mount("/", web_routers.Routes(ctx)) r.Mount("/api/v1", apiv1.Routes(ctx)) r.Mount("/api/internal", private.Routes()) if setting.Packages.Enabled { - // Add endpoints to match common package manager APIs - // This implements package support for most package managers r.Mount("/api/packages", packages_router.CommonRoutes(ctx)) - // This implements the OCI API (Note this is not preceded by /api but is instead /v2) r.Mount("/v2", packages_router.ContainerRoutes(ctx)) } diff --git a/routers/install/routes.go b/routers/install/routes.go index f52539ec1..9c7420c59 100644 --- a/routers/install/routes.go +++ b/routers/install/routes.go @@ -19,8 +19,6 @@ import ( "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/web/healthcheck" "code.gitea.io/gitea/services/forms" - - "gitea.com/go-chi/session" ) type dataStore map[string]interface{} @@ -30,7 +28,6 @@ func (d *dataStore) GetData() map[string]interface{} { } func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler { - _, rnd := templates.HTMLRenderer(ctx) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { defer func() { @@ -69,6 +66,7 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler { if !setting.IsProd { store["ErrorMsg"] = combinedErr } + _, rnd := templates.HTMLRenderer(ctx) err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store)) if err != nil { log.Error("%v", err) @@ -83,34 +81,22 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler { // Routes registers the installation routes func Routes(ctx goctx.Context) *web.Route { + base := web.NewRoute() + base.Use(common.ProtocolMiddlewares()...) + base.RouteMethods("/assets/*", "GET, HEAD", public.AssetsHandlerFunc("/assets/")) + r := web.NewRoute() - for _, middle := range common.Middlewares() { - r.Use(middle) - } - - r.Use(web.MiddlewareWithPrefix("/assets/", nil, public.AssetsHandlerFunc("/assets/"))) - - r.Use(session.Sessioner(session.Options{ - Provider: setting.SessionConfig.Provider, - ProviderConfig: setting.SessionConfig.ProviderConfig, - CookieName: setting.SessionConfig.CookieName, - CookiePath: setting.SessionConfig.CookiePath, - Gclifetime: setting.SessionConfig.Gclifetime, - Maxlifetime: setting.SessionConfig.Maxlifetime, - Secure: setting.SessionConfig.Secure, - SameSite: setting.SessionConfig.SameSite, - Domain: setting.SessionConfig.Domain, - })) - + r.Use(common.Sessioner()) r.Use(installRecovery(ctx)) r.Use(Init(ctx)) r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall) r.Get("/post-install", InstallDone) r.Get("/api/healthz", healthcheck.Check) - r.NotFound(installNotFound) - return r + + base.Mount("", r) + return base } func installNotFound(w http.ResponseWriter, req *http.Request) { diff --git a/routers/install/routes_test.go b/routers/install/routes_test.go index 35b5e5e91..e3d2a4246 100644 --- a/routers/install/routes_test.go +++ b/routers/install/routes_test.go @@ -11,11 +11,14 @@ import ( ) func TestRoutes(t *testing.T) { + // TODO: this test seems not really testing the handlers ctx, cancel := context.WithCancel(context.Background()) defer cancel() - routes := Routes(ctx) - assert.NotNil(t, routes) - assert.EqualValues(t, "/", routes.R.Routes()[0].Pattern) - assert.Nil(t, routes.R.Routes()[0].SubRoutes) - assert.Len(t, routes.R.Routes()[0].Handlers, 2) + base := Routes(ctx) + assert.NotNil(t, base) + r := base.R.Routes()[1] + routes := r.SubRoutes.Routes()[0] + assert.EqualValues(t, "/", routes.Pattern) + assert.Nil(t, routes.SubRoutes) + assert.Len(t, routes.Handlers, 2) } diff --git a/routers/web/base.go b/routers/web/base.go index 79991d89d..b5d7a737b 100644 --- a/routers/web/base.go +++ b/routers/web/base.go @@ -59,12 +59,7 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor return } - http.Redirect( - w, - req, - u.String(), - http.StatusTemporaryRedirect, - ) + http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect) }) } @@ -122,9 +117,9 @@ func (d *dataStore) GetData() map[string]interface{} { return *d } -// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so. +// RecoveryWith500Page returns a middleware that recovers from any panics and writes a 500 and a log if so. // This error will be created with the gitea 500 page. -func Recovery(ctx goctx.Context) func(next http.Handler) http.Handler { +func RecoveryWith500Page(ctx goctx.Context) func(next http.Handler) http.Handler { _, rnd := templates.HTMLRenderer(ctx) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go new file mode 100644 index 000000000..582179990 --- /dev/null +++ b/routers/web/misc/misc.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package misc + +import ( + "net/http" + "os" + "path" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +func SSHInfo(rw http.ResponseWriter, req *http.Request) { + if !git.SupportProcReceive { + rw.WriteHeader(http.StatusNotFound) + return + } + rw.Header().Set("content-type", "text/json;charset=UTF-8") + _, err := rw.Write([]byte(`{"type":"gitea","version":1}`)) + if err != nil { + log.Error("fail to write result: err: %v", err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + rw.WriteHeader(http.StatusOK) +} + +func DummyOK(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func RobotsTxt(w http.ResponseWriter, req *http.Request) { + filePath := path.Join(setting.CustomPath, "robots.txt") + fi, err := os.Stat(filePath) + if err == nil && httpcache.HandleTimeCache(req, w, fi) { + return + } + http.ServeFile(w, req, filePath) +} + +func StaticRedirect(target string) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, path.Join(setting.StaticURLPrefix, target), http.StatusMovedPermanently) + } +} diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 5a97c5190..c4b5351ec 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -18,7 +18,9 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" @@ -59,6 +61,22 @@ func MustBeAbleToUpload(ctx *context.Context) { } } +func CommitInfoCache(ctx *context.Context) { + var err error + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + if err != nil { + ctx.ServerError("GetBranchCommit", err) + return + } + ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() + if err != nil { + ctx.ServerError("GetCommitsCount", err) + return + } + ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount + ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) +} + func checkContextUser(ctx *context.Context, uid int64) *user_model.User { orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx.Doer.ID) if err != nil { diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 1f7737904..1af56e24b 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -30,6 +30,8 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/web/feed" + context_service "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" @@ -815,3 +817,51 @@ func ShowGPGKeys(ctx *context.Context) { writer.Close() ctx.PlainTextBytes(http.StatusOK, buf.Bytes()) } + +func UsernameSubRoute(ctx *context.Context) { + // WORKAROUND to support usernames with "." in it + // https://github.com/go-chi/chi/issues/781 + username := ctx.Params("username") + reloadParam := func(suffix string) (success bool) { + ctx.SetParams("username", strings.TrimSuffix(username, suffix)) + context_service.UserAssignmentWeb()(ctx) + return !ctx.Written() + } + switch { + case strings.HasSuffix(username, ".png"): + if reloadParam(".png") { + AvatarByUserName(ctx) + } + case strings.HasSuffix(username, ".keys"): + if reloadParam(".keys") { + ShowSSHKeys(ctx) + } + case strings.HasSuffix(username, ".gpg"): + if reloadParam(".gpg") { + ShowGPGKeys(ctx) + } + case strings.HasSuffix(username, ".rss"): + if !setting.Other.EnableFeed { + ctx.Error(http.StatusNotFound) + return + } + if reloadParam(".rss") { + context_service.UserAssignmentWeb()(ctx) + feed.ShowUserFeedRSS(ctx) + } + case strings.HasSuffix(username, ".atom"): + if !setting.Other.EnableFeed { + ctx.Error(http.StatusNotFound) + return + } + if reloadParam(".atom") { + feed.ShowUserFeedAtom(ctx) + } + default: + context_service.UserAssignmentWeb()(ctx) + if !ctx.Written() { + ctx.Data["EnableFeed"] = setting.Other.EnableFeed + Profile(ctx) + } + } +} diff --git a/routers/web/web.go b/routers/web/web.go index 779499889..746b098bd 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -6,16 +6,10 @@ package web import ( gocontext "context" "net/http" - "os" - "path" - "strings" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/metrics" "code.gitea.io/gitea/modules/public" @@ -26,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/routing" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/web/admin" "code.gitea.io/gitea/routers/web/auth" "code.gitea.io/gitea/routers/web/devtest" @@ -48,7 +43,6 @@ import ( _ "code.gitea.io/gitea/modules/session" // to registers all internal adapters "gitea.com/go-chi/captcha" - "gitea.com/go-chi/session" "github.com/NYTimes/gziphandler" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" @@ -103,45 +97,18 @@ func buildAuthGroup() *auth_service.Group { func Routes(ctx gocontext.Context) *web.Route { routes := web.NewRoute() - routes.Use(web.MiddlewareWithPrefix("/assets/", CorsHandler(), public.AssetsHandlerFunc("/assets/"))) - - sessioner := session.Sessioner(session.Options{ - Provider: setting.SessionConfig.Provider, - ProviderConfig: setting.SessionConfig.ProviderConfig, - CookieName: setting.SessionConfig.CookieName, - CookiePath: setting.SessionConfig.CookiePath, - Gclifetime: setting.SessionConfig.Gclifetime, - Maxlifetime: setting.SessionConfig.Maxlifetime, - Secure: setting.SessionConfig.Secure, - SameSite: setting.SessionConfig.SameSite, - Domain: setting.SessionConfig.Domain, - }) - routes.Use(sessioner) - - ctx, _ = templates.HTMLRenderer(ctx) - - routes.Use(Recovery(ctx)) - - // We use r.Route here over r.Use because this prevents requests that are not for avatars having to go through this additional handler + routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler + routes.RouteMethods("/assets/*", "GET, HEAD", CorsHandler(), public.AssetsHandlerFunc("/assets/")) routes.RouteMethods("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) routes.RouteMethods("/repo-avatars/*", "GET, HEAD", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars)) + routes.RouteMethods("/apple-touch-icon.png", "GET, HEAD", misc.StaticRedirect("/assets/img/apple-touch-icon.png")) + routes.RouteMethods("/favicon.ico", "GET, HEAD", misc.StaticRedirect("/assets/img/favicon.png")) - // for health check - doesn't need to be passed through gzip handler - routes.Head("/", func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(http.StatusOK) - }) - - // this png is very likely to always be below the limit for gzip so it doesn't need to pass through gzip - routes.Get("/apple-touch-icon.png", func(w http.ResponseWriter, req *http.Request) { - http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "/assets/img/apple-touch-icon.png"), http.StatusPermanentRedirect) - }) - - // redirect default favicon to the path of the custom favicon with a default as a fallback - routes.Get("/favicon.ico", func(w http.ResponseWriter, req *http.Request) { - http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "/assets/img/favicon.png"), http.StatusMovedPermanently) - }) - - common := []interface{}{} + ctx, _ = templates.HTMLRenderer(ctx) + common := []any{ + common.Sessioner(), + RecoveryWith500Page(ctx), + } if setting.EnableGzip { h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize)) @@ -157,42 +124,18 @@ func Routes(ctx gocontext.Context) *web.Route { } if setting.HasRobotsTxt { - routes.Get("/robots.txt", append(common, func(w http.ResponseWriter, req *http.Request) { - filePath := path.Join(setting.CustomPath, "robots.txt") - fi, err := os.Stat(filePath) - if err == nil && httpcache.HandleTimeCache(req, w, fi) { - return - } - http.ServeFile(w, req, filePath) - })...) + routes.Get("/robots.txt", append(common, misc.RobotsTxt)...) } // prometheus metrics endpoint - do not need to go through contexter if setting.Metrics.Enabled { - c := metrics.NewCollector() - prometheus.MustRegister(c) - + prometheus.MustRegister(metrics.NewCollector()) routes.Get("/metrics", append(common, Metrics)...) } - routes.Get("/ssh_info", func(rw http.ResponseWriter, req *http.Request) { - if !git.SupportProcReceive { - rw.WriteHeader(http.StatusNotFound) - return - } - rw.Header().Set("content-type", "text/json;charset=UTF-8") - _, err := rw.Write([]byte(`{"type":"gitea","version":1}`)) - if err != nil { - log.Error("fail to write result: err: %v", err) - rw.WriteHeader(http.StatusInternalServerError) - return - } - rw.WriteHeader(http.StatusOK) - }) - + routes.Get("/ssh_info", misc.SSHInfo) routes.Get("/api/healthz", healthcheck.Check) - // Removed: toolbox.Toolboxer middleware will provide debug information which seems unnecessary common = append(common, context.Contexter(ctx)) group := buildAuthGroup() @@ -207,7 +150,7 @@ func Routes(ctx gocontext.Context) *web.Route { common = append(common, middleware.GetHead) if setting.API.EnableSwagger { - // Note: The route moved from apiroutes because it's in fact want to render a web page + // Note: The route is here but no in API routes because it renders a web page routes.Get("/api/swagger", append(common, misc.Swagger)...) // Render V1 by default } @@ -217,17 +160,14 @@ func Routes(ctx gocontext.Context) *web.Route { common = append(common, goGet) others := web.NewRoute() - for _, middle := range common { - others.Use(middle) - } - - RegisterRoutes(others) + others.Use(common...) + registerRoutes(others) routes.Mount("", others) return routes } -// RegisterRoutes register routes -func RegisterRoutes(m *web.Route) { +// registerRoutes register routes +func registerRoutes(m *web.Route) { reqSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: true}) ignSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView}) ignExploreSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView}) @@ -354,8 +294,8 @@ func RegisterRoutes(m *web.Route) { m.Get("/nodeinfo", NodeInfoLinks) m.Get("/webfinger", WebfingerQuery) }, federationEnabled) - m.Get("/change-password", func(w http.ResponseWriter, req *http.Request) { - http.Redirect(w, req, "/user/settings/account", http.StatusTemporaryRedirect) + m.Get("/change-password", func(ctx *context.Context) { + ctx.Redirect(setting.AppSubURL + "/user/settings/account") }) }) @@ -664,53 +604,7 @@ func RegisterRoutes(m *web.Route) { // ***** END: Admin ***** m.Group("", func() { - m.Get("/favicon.ico", func(ctx *context.Context) { - ctx.SetServeHeaders(&context.ServeHeaderOptions{ - Filename: "favicon.png", - }) - http.ServeFile(ctx.Resp, ctx.Req, path.Join(setting.StaticRootPath, "public/img/favicon.png")) - }) - m.Get("/{username}", func(ctx *context.Context) { - // WORKAROUND to support usernames with "." in it - // https://github.com/go-chi/chi/issues/781 - username := ctx.Params("username") - reloadParam := func(suffix string) (success bool) { - ctx.SetParams("username", strings.TrimSuffix(username, suffix)) - context_service.UserAssignmentWeb()(ctx) - return !ctx.Written() - } - switch { - case strings.HasSuffix(username, ".png"): - if reloadParam(".png") { - user.AvatarByUserName(ctx) - } - case strings.HasSuffix(username, ".keys"): - if reloadParam(".keys") { - user.ShowSSHKeys(ctx) - } - case strings.HasSuffix(username, ".gpg"): - if reloadParam(".gpg") { - user.ShowGPGKeys(ctx) - } - case strings.HasSuffix(username, ".rss"): - feedEnabled(ctx) - if !ctx.Written() && reloadParam(".rss") { - context_service.UserAssignmentWeb()(ctx) - feed.ShowUserFeedRSS(ctx) - } - case strings.HasSuffix(username, ".atom"): - feedEnabled(ctx) - if !ctx.Written() && reloadParam(".atom") { - feed.ShowUserFeedAtom(ctx) - } - default: - context_service.UserAssignmentWeb()(ctx) - if !ctx.Written() { - ctx.Data["EnableFeed"] = setting.Other.EnableFeed - user.Profile(ctx) - } - } - }) + m.Get("/{username}", user.UsernameSubRoute) m.Get("/attachments/{uuid}", repo.GetAttachment) }, ignSignIn) @@ -1233,21 +1127,7 @@ func RegisterRoutes(m *web.Route) { m.Group("/releases", func() { m.Get("/edit/*", repo.EditRelease) m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) - }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, func(ctx *context.Context) { - var err error - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.ServerError("GetBranchCommit", err) - return - } - ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() - if err != nil { - ctx.ServerError("GetCommitsCount", err) - return - } - ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount - ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) - }) + }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache) }, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader) // to maintain compatibility with old attachments @@ -1326,18 +1206,10 @@ func RegisterRoutes(m *web.Route) { m.Group("/wiki", func() { m.Combo("/"). Get(repo.Wiki). - Post(context.RepoMustNotBeArchived(), - reqSignIn, - reqRepoWikiWriter, - web.Bind(forms.NewWikiForm{}), - repo.WikiPost) + Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) m.Combo("/*"). Get(repo.Wiki). - Post(context.RepoMustNotBeArchived(), - reqSignIn, - reqRepoWikiWriter, - web.Bind(forms.NewWikiForm{}), - repo.WikiPost) + Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) m.Get("/commit/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:[a-f0-9]{7,40}}.{ext:patch|diff}", repo.RawDiff) }, repo.MustEnableWiki, func(ctx *context.Context) { @@ -1468,8 +1340,7 @@ func RegisterRoutes(m *web.Route) { m.Group("", func() { m.Get("/forks", repo.Forks) }, context.RepoRef(), reqRepoCodeReader) - m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}", - repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff) + m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff) }, ignSignIn, context.RepoAssignment, context.UnitTypes()) m.Post("/{username}/{reponame}/lastcommit/*", ignSignInAndCsrf, context.RepoAssignment, context.UnitTypes(), context.RepoRefByType(context.RepoRefCommit), reqRepoCodeReader, repo.LastCommit)