mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	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 <teabot@gitea.io>
This commit is contained in:
		| @@ -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 | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										40
									
								
								modules/context/context_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								modules/context/context_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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")) | ||||
| } | ||||
| @@ -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) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -44,24 +44,14 @@ 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)) | ||||
| 	} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Group mounts a sub-Router along a `pattern` string. | ||||
| @@ -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) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -15,14 +15,14 @@ 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 { | ||||
| // 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() | ||||
| @@ -31,8 +31,7 @@ func Middlewares() []func(http.Handler) http.Handler { | ||||
| 			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, | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -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)) | ||||
| 	} | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
							
								
								
									
										49
									
								
								routers/web/misc/misc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								routers/web/misc/misc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 	} | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user