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:
wxiaoguang 2023-04-27 14:06:45 +08:00 committed by GitHub
parent 1c875ef5be
commit 92fd3fc4fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 264 additions and 253 deletions

View File

@ -343,6 +343,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
} }
ctx.Repo.Commit = commit ctx.Repo.Commit = commit
ctx.Repo.TreePath = ctx.Params("*") ctx.Repo.TreePath = ctx.Params("*")
next.ServeHTTP(w, req)
return return
} }

View File

@ -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 // Redirect redirects the request
func (ctx *Context) Redirect(location string, status ...int) { func (ctx *Context) Redirect(location string, status ...int) {
code := http.StatusSeeOther code := http.StatusSeeOther
@ -453,6 +464,15 @@ func (ctx *Context) Redirect(location string, status ...int) {
code = status[0] 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) http.Redirect(ctx.Resp, ctx.Req, location, code)
} }

View 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"))
}

View File

@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
"strings"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/routing" "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 // 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 // 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 { 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) funcInfo := routing.GetFuncInfo(handler)
fn := reflect.ValueOf(handler) fn := reflect.ValueOf(handler)
if fn.Type().Kind() != reflect.Func { if fn.Type().Kind() != reflect.Func {
panic(fmt.Sprintf("handler must be a function, but got %s", fn.Type())) 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 { provider := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
// wrap the response writer to check whether the response has been written // 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 provider(nil).ServeHTTP(nil, nil) // do a pre-check to make sure all arguments and return values are supported
return provider 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)
}
})
}
}

View File

@ -44,23 +44,13 @@ type Route struct {
// NewRoute creates a new route // NewRoute creates a new route
func NewRoute() *Route { func NewRoute() *Route {
r := chi.NewRouter() r := chi.NewRouter()
return &Route{ return &Route{R: r}
R: r,
curGroupPrefix: "",
curMiddlewares: []interface{}{},
}
} }
// Use supports two middlewares // Use supports two middlewares
func (r *Route) Use(middlewares ...interface{}) { func (r *Route) Use(middlewares ...interface{}) {
if r.curGroupPrefix != "" { for _, m := range middlewares {
// FIXME: this behavior is incorrect, should use "With" instead r.R.Use(toHandlerProvider(m))
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))
}
} }
} }
@ -116,9 +106,7 @@ func (r *Route) Methods(method, pattern string, h []any) {
// Mount attaches another Route along ./pattern/* // Mount attaches another Route along ./pattern/*
func (r *Route) Mount(pattern string, subR *Route) { func (r *Route) Mount(pattern string, subR *Route) {
middlewares := make([]interface{}, len(r.curMiddlewares)) subR.Use(r.curMiddlewares...)
copy(middlewares, r.curMiddlewares)
subR.Use(middlewares...)
r.R.Mount(r.getPattern(pattern), subR.R) r.R.Mount(r.getPattern(pattern), subR.R)
} }

View File

@ -15,24 +15,23 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/routing" "code.gitea.io/gitea/modules/web/routing"
"gitea.com/go-chi/session"
"github.com/chi-middleware/proxy" "github.com/chi-middleware/proxy"
chi "github.com/go-chi/chi/v5" chi "github.com/go-chi/chi/v5"
) )
// Middlewares returns common middlewares // ProtocolMiddlewares returns HTTP protocol related middlewares
func Middlewares() []func(http.Handler) http.Handler { func ProtocolMiddlewares() (handlers []any) {
handlers := []func(http.Handler) http.Handler{ handlers = append(handlers, func(next http.Handler) http.Handler {
func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
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
// 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()
req.URL.RawPath = req.URL.EscapedPath()
ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true) ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)
defer finished() defer finished()
next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx))) next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx)))
}) })
}, })
}
if setting.ReverseProxyLimit > 0 { if setting.ReverseProxyLimit > 0 {
opt := proxy.NewForwardedHeadersOptions(). opt := proxy.NewForwardedHeadersOptions().
@ -112,3 +111,17 @@ func stripSlashesMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(resp, req) 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,
})
}

View File

@ -177,20 +177,15 @@ func GlobalInitInstalled(ctx context.Context) {
func NormalRoutes(ctx context.Context) *web.Route { func NormalRoutes(ctx context.Context) *web.Route {
ctx, _ = templates.HTMLRenderer(ctx) ctx, _ = templates.HTMLRenderer(ctx)
r := web.NewRoute() r := web.NewRoute()
for _, middle := range common.Middlewares() { r.Use(common.ProtocolMiddlewares()...)
r.Use(middle)
}
r.Mount("/", web_routers.Routes(ctx)) r.Mount("/", web_routers.Routes(ctx))
r.Mount("/api/v1", apiv1.Routes(ctx)) r.Mount("/api/v1", apiv1.Routes(ctx))
r.Mount("/api/internal", private.Routes()) r.Mount("/api/internal", private.Routes())
if setting.Packages.Enabled { if setting.Packages.Enabled {
// Add endpoints to match common package manager APIs
// This implements package support for most package managers // This implements package support for most package managers
r.Mount("/api/packages", packages_router.CommonRoutes(ctx)) r.Mount("/api/packages", packages_router.CommonRoutes(ctx))
// This implements the OCI API (Note this is not preceded by /api but is instead /v2) // This implements the OCI API (Note this is not preceded by /api but is instead /v2)
r.Mount("/v2", packages_router.ContainerRoutes(ctx)) r.Mount("/v2", packages_router.ContainerRoutes(ctx))
} }

View File

@ -19,8 +19,6 @@ import (
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/web/healthcheck" "code.gitea.io/gitea/routers/web/healthcheck"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"gitea.com/go-chi/session"
) )
type dataStore map[string]interface{} 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 { func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
_, rnd := templates.HTMLRenderer(ctx)
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() { defer func() {
@ -69,6 +66,7 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
if !setting.IsProd { if !setting.IsProd {
store["ErrorMsg"] = combinedErr store["ErrorMsg"] = combinedErr
} }
_, rnd := templates.HTMLRenderer(ctx)
err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store)) err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store))
if err != nil { if err != nil {
log.Error("%v", err) log.Error("%v", err)
@ -83,34 +81,22 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
// Routes registers the installation routes // Routes registers the installation routes
func Routes(ctx goctx.Context) *web.Route { 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() r := web.NewRoute()
for _, middle := range common.Middlewares() { r.Use(common.Sessioner())
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(installRecovery(ctx)) r.Use(installRecovery(ctx))
r.Use(Init(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.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.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
r.Get("/post-install", InstallDone) r.Get("/post-install", InstallDone)
r.Get("/api/healthz", healthcheck.Check) r.Get("/api/healthz", healthcheck.Check)
r.NotFound(installNotFound) r.NotFound(installNotFound)
return r
base.Mount("", r)
return base
} }
func installNotFound(w http.ResponseWriter, req *http.Request) { func installNotFound(w http.ResponseWriter, req *http.Request) {

View File

@ -11,11 +11,14 @@ import (
) )
func TestRoutes(t *testing.T) { func TestRoutes(t *testing.T) {
// TODO: this test seems not really testing the handlers
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
routes := Routes(ctx) base := Routes(ctx)
assert.NotNil(t, routes) assert.NotNil(t, base)
assert.EqualValues(t, "/", routes.R.Routes()[0].Pattern) r := base.R.Routes()[1]
assert.Nil(t, routes.R.Routes()[0].SubRoutes) routes := r.SubRoutes.Routes()[0]
assert.Len(t, routes.R.Routes()[0].Handlers, 2) assert.EqualValues(t, "/", routes.Pattern)
assert.Nil(t, routes.SubRoutes)
assert.Len(t, routes.Handlers, 2)
} }

View File

@ -59,12 +59,7 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
return return
} }
http.Redirect( http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
w,
req,
u.String(),
http.StatusTemporaryRedirect,
)
}) })
} }
@ -122,9 +117,9 @@ func (d *dataStore) GetData() map[string]interface{} {
return *d 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. // 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) _, rnd := templates.HTMLRenderer(ctx)
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

49
routers/web/misc/misc.go Normal file
View 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)
}
}

View File

@ -18,7 +18,9 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting" "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 { func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx.Doer.ID) orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx.Doer.ID)
if err != nil { if err != nil {

View File

@ -30,6 +30,8 @@ import (
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "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" issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
@ -815,3 +817,51 @@ func ShowGPGKeys(ctx *context.Context) {
writer.Close() writer.Close()
ctx.PlainTextBytes(http.StatusOK, buf.Bytes()) 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)
}
}
}

View File

@ -6,16 +6,10 @@ package web
import ( import (
gocontext "context" gocontext "context"
"net/http" "net/http"
"os"
"path"
"strings"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/context" "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/log"
"code.gitea.io/gitea/modules/metrics" "code.gitea.io/gitea/modules/metrics"
"code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/public"
@ -26,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/routing" "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/admin"
"code.gitea.io/gitea/routers/web/auth" "code.gitea.io/gitea/routers/web/auth"
"code.gitea.io/gitea/routers/web/devtest" "code.gitea.io/gitea/routers/web/devtest"
@ -48,7 +43,6 @@ import (
_ "code.gitea.io/gitea/modules/session" // to registers all internal adapters _ "code.gitea.io/gitea/modules/session" // to registers all internal adapters
"gitea.com/go-chi/captcha" "gitea.com/go-chi/captcha"
"gitea.com/go-chi/session"
"github.com/NYTimes/gziphandler" "github.com/NYTimes/gziphandler"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors" "github.com/go-chi/cors"
@ -103,45 +97,18 @@ func buildAuthGroup() *auth_service.Group {
func Routes(ctx gocontext.Context) *web.Route { func Routes(ctx gocontext.Context) *web.Route {
routes := web.NewRoute() routes := web.NewRoute()
routes.Use(web.MiddlewareWithPrefix("/assets/", CorsHandler(), public.AssetsHandlerFunc("/assets/"))) 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/"))
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.RouteMethods("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) 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("/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 ctx, _ = templates.HTMLRenderer(ctx)
routes.Head("/", func(w http.ResponseWriter, req *http.Request) { common := []any{
w.WriteHeader(http.StatusOK) common.Sessioner(),
}) RecoveryWith500Page(ctx),
}
// 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{}{}
if setting.EnableGzip { if setting.EnableGzip {
h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize)) h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize))
@ -157,42 +124,18 @@ func Routes(ctx gocontext.Context) *web.Route {
} }
if setting.HasRobotsTxt { if setting.HasRobotsTxt {
routes.Get("/robots.txt", append(common, func(w http.ResponseWriter, req *http.Request) { routes.Get("/robots.txt", append(common, misc.RobotsTxt)...)
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)
})...)
} }
// prometheus metrics endpoint - do not need to go through contexter // prometheus metrics endpoint - do not need to go through contexter
if setting.Metrics.Enabled { if setting.Metrics.Enabled {
c := metrics.NewCollector() prometheus.MustRegister(metrics.NewCollector())
prometheus.MustRegister(c)
routes.Get("/metrics", append(common, Metrics)...) routes.Get("/metrics", append(common, Metrics)...)
} }
routes.Get("/ssh_info", func(rw http.ResponseWriter, req *http.Request) { routes.Get("/ssh_info", misc.SSHInfo)
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("/api/healthz", healthcheck.Check) routes.Get("/api/healthz", healthcheck.Check)
// Removed: toolbox.Toolboxer middleware will provide debug information which seems unnecessary
common = append(common, context.Contexter(ctx)) common = append(common, context.Contexter(ctx))
group := buildAuthGroup() group := buildAuthGroup()
@ -207,7 +150,7 @@ func Routes(ctx gocontext.Context) *web.Route {
common = append(common, middleware.GetHead) common = append(common, middleware.GetHead)
if setting.API.EnableSwagger { 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 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) common = append(common, goGet)
others := web.NewRoute() others := web.NewRoute()
for _, middle := range common { others.Use(common...)
others.Use(middle) registerRoutes(others)
}
RegisterRoutes(others)
routes.Mount("", others) routes.Mount("", others)
return routes return routes
} }
// RegisterRoutes register routes // registerRoutes register routes
func RegisterRoutes(m *web.Route) { func registerRoutes(m *web.Route) {
reqSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: true}) reqSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: true})
ignSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView}) 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}) 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("/nodeinfo", NodeInfoLinks)
m.Get("/webfinger", WebfingerQuery) m.Get("/webfinger", WebfingerQuery)
}, federationEnabled) }, federationEnabled)
m.Get("/change-password", func(w http.ResponseWriter, req *http.Request) { m.Get("/change-password", func(ctx *context.Context) {
http.Redirect(w, req, "/user/settings/account", http.StatusTemporaryRedirect) ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}) })
}) })
@ -664,53 +604,7 @@ func RegisterRoutes(m *web.Route) {
// ***** END: Admin ***** // ***** END: Admin *****
m.Group("", func() { m.Group("", func() {
m.Get("/favicon.ico", func(ctx *context.Context) { m.Get("/{username}", user.UsernameSubRoute)
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("/attachments/{uuid}", repo.GetAttachment) m.Get("/attachments/{uuid}", repo.GetAttachment)
}, ignSignIn) }, ignSignIn)
@ -1233,21 +1127,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/releases", func() { m.Group("/releases", func() {
m.Get("/edit/*", repo.EditRelease) m.Get("/edit/*", repo.EditRelease)
m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost)
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, func(ctx *context.Context) { }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache)
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())
})
}, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader) }, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader)
// to maintain compatibility with old attachments // to maintain compatibility with old attachments
@ -1326,18 +1206,10 @@ func RegisterRoutes(m *web.Route) {
m.Group("/wiki", func() { m.Group("/wiki", func() {
m.Combo("/"). m.Combo("/").
Get(repo.Wiki). Get(repo.Wiki).
Post(context.RepoMustNotBeArchived(), Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost)
reqSignIn,
reqRepoWikiWriter,
web.Bind(forms.NewWikiForm{}),
repo.WikiPost)
m.Combo("/*"). m.Combo("/*").
Get(repo.Wiki). Get(repo.Wiki).
Post(context.RepoMustNotBeArchived(), Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost)
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}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
m.Get("/commit/{sha:[a-f0-9]{7,40}}.{ext:patch|diff}", repo.RawDiff) m.Get("/commit/{sha:[a-f0-9]{7,40}}.{ext:patch|diff}", repo.RawDiff)
}, repo.MustEnableWiki, func(ctx *context.Context) { }, repo.MustEnableWiki, func(ctx *context.Context) {
@ -1468,8 +1340,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("", func() { m.Group("", func() {
m.Get("/forks", repo.Forks) m.Get("/forks", repo.Forks)
}, context.RepoRef(), reqRepoCodeReader) }, context.RepoRef(), reqRepoCodeReader)
m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}", m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
}, ignSignIn, context.RepoAssignment, context.UnitTypes()) }, ignSignIn, context.RepoAssignment, context.UnitTypes())
m.Post("/{username}/{reponame}/lastcommit/*", ignSignInAndCsrf, context.RepoAssignment, context.UnitTypes(), context.RepoRefByType(context.RepoRefCommit), reqRepoCodeReader, repo.LastCommit) m.Post("/{username}/{reponame}/lastcommit/*", ignSignInAndCsrf, context.RepoAssignment, context.UnitTypes(), context.RepoRefByType(context.RepoRefCommit), reqRepoCodeReader, repo.LastCommit)