Refactor auth middleware (#36848)

Principles: let the caller decide what it needs, but not let the
framework (middleware) guess what it should do.

Then a lot of hacky code can be removed. And some FIXMEs can be fixed.

This PR introduces a new kind of middleware: "PreMiddleware", it will be
executed before all other middlewares on the same routing level, then a
route can declare its options for other middlewares.

By the way, allow the workflow badge to be accessed by Basic or OAuth2
auth.

Fixes: https://github.com/go-gitea/gitea/pull/36830
Fixes: https://github.com/go-gitea/gitea/issues/36859
This commit is contained in:
wxiaoguang
2026-03-08 17:59:46 +08:00
committed by GitHub
parent a0996cb229
commit 3f1ef703d5
25 changed files with 338 additions and 444 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ func init() {
func ArtifactsRoutes(prefix string) *web.Router {
m := web.NewRouter()
m.Use(ArtifactContexter())
m.AfterRouting(ArtifactContexter())
r := artifactRoutes{
prefix: prefix,
+3 -3
View File
@@ -94,7 +94,7 @@ func verifyAuth(r *web.Router, authMethods []auth.Method) {
}
authGroup := auth.NewGroup(authMethods...)
r.Use(func(ctx *context.Context) {
r.AfterRouting(func(ctx *context.Context) {
var err error
ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil {
@@ -111,7 +111,7 @@ func verifyAuth(r *web.Router, authMethods []auth.Method) {
func CommonRoutes() *web.Router {
r := web.NewRouter()
r.Use(context.PackageContexter())
r.AfterRouting(context.PackageContexter())
verifyAuth(r, []auth.Method{
&auth.OAuth2{},
@@ -533,7 +533,7 @@ func CommonRoutes() *web.Router {
func ContainerRoutes() *web.Router {
r := web.NewRouter()
r.Use(context.PackageContexter())
r.AfterRouting(context.PackageContexter())
verifyAuth(r, []auth.Method{
&auth.Basic{},
+8 -13
View File
@@ -77,7 +77,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@@ -756,13 +755,9 @@ func buildAuthGroup() *auth.Group {
&auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
)
if setting.Service.EnableReverseProxyAuthAPI {
group.Add(&auth.ReverseProxy{})
group.Add(&auth.ReverseProxy{}) // TODO: does it still make sense to support reverse proxy auth in API?
}
if setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext()) {
group.Add(&auth.SSPI{}) // it MUST be the last, see the comment of SSPI
}
// others: API doesn't support SSPI auth because the caller should use token
return group
}
@@ -872,9 +867,9 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) {
func Routes() *web.Router {
m := web.NewRouter()
m.Use(securityHeaders())
m.BeforeRouting(securityHeaders())
if setting.CORSConfig.Enabled {
m.Use(cors.Handler(cors.Options{
m.BeforeRouting(cors.Handler(cors.Options{
AllowedOrigins: setting.CORSConfig.AllowDomain,
AllowedMethods: setting.CORSConfig.Methods,
AllowCredentials: setting.CORSConfig.AllowCredentials,
@@ -882,14 +877,14 @@ func Routes() *web.Router {
MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
}))
}
m.Use(context.APIContexter())
m.Use(checkDeprecatedAuthMethods)
m.AfterRouting(context.APIContexter())
m.AfterRouting(checkDeprecatedAuthMethods)
// Get user from session if logged in.
m.Use(apiAuth(buildAuthGroup()))
m.AfterRouting(apiAuth(buildAuthGroup()))
m.Use(verifyAuthWithOptions(&common.VerifyOptions{
m.AfterRouting(verifyAuthWithOptions(&common.VerifyOptions{
SignInRequired: setting.Service.RequireSignInViewStrict,
}))
+7 -7
View File
@@ -53,18 +53,18 @@ func renderServerErrorPage(w http.ResponseWriter, req *http.Request, respCode in
_, _ = io.Copy(w, outBuf)
}
// RenderPanicErrorPage renders a 500 page, and it never panics
func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) {
combinedErr := fmt.Sprintf("%v\n%s", err, log.Stack(2))
log.Error("PANIC: %s", combinedErr)
// renderPanicErrorPage renders a 500 page with the recovered panic value, it handles the stack trace, and it never panics
func renderPanicErrorPage(w http.ResponseWriter, req *http.Request, recovered any) {
combinedErr := fmt.Errorf("%v\n%s", recovered, log.Stack(2))
log.Error("PANIC: %v", combinedErr)
defer func() {
if err := recover(); err != nil {
log.Error("Panic occurs again when rendering error page: %v. Stack:\n%s", err, log.Stack(2))
log.Error("Panic occurs again when rendering error page: %v. Stack:\n%s", combinedErr, log.Stack(2))
}
}()
routing.UpdatePanicError(req.Context(), err)
routing.UpdatePanicError(req.Context(), combinedErr)
plainMsg := "Internal Server Error"
ctxData := middleware.GetContextData(req.Context())
@@ -72,7 +72,7 @@ func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) {
// Otherwise, the 500-page may cause new panics, eg: cache.GetContextWithData, it makes the developer&users couldn't find the original panic.
user, _ := ctxData[middleware.ContextDataKeySignedUser].(*user_model.User)
if !setting.IsProd || (user != nil && user.IsAdmin) {
plainMsg = "PANIC: " + combinedErr
plainMsg = "PANIC: " + combinedErr.Error()
ctxData["ErrorMsg"] = plainMsg
}
renderServerErrorPage(w, req, http.StatusInternalServerError, tplStatus500, ctxData, plainMsg)
+1 -1
View File
@@ -22,7 +22,7 @@ func TestRenderPanicErrorPage(t *testing.T) {
w := httptest.NewRecorder()
req := &http.Request{URL: &url.URL{}, Header: http.Header{"Accept": []string{"text/html"}}}
req = req.WithContext(reqctx.NewRequestContextForTest(t.Context()))
RenderPanicErrorPage(w, req, errors.New("fake panic error (for test only)"))
renderPanicErrorPage(w, req, errors.New("fake panic error (for test only)"))
respContent := w.Body.String()
assert.Contains(t, respContent, `class="page-content status-page-500"`)
assert.Contains(t, respContent, `</html>`)
+4 -4
View File
@@ -5,13 +5,13 @@ package common
import (
"fmt"
"log"
"net/http"
"strings"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/routing"
@@ -63,8 +63,8 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
}()
defer func() {
if err := recover(); err != nil {
RenderPanicErrorPage(respWriter, req, err) // it should never panic
if recovered := recover(); recovered != nil {
renderPanicErrorPage(respWriter, req, recovered) // it should never panic, and it handles the stack trace internally
}
}()
@@ -130,7 +130,7 @@ func MustInitSessioner() func(next http.Handler) http.Handler {
Domain: setting.SessionConfig.Domain,
})
if err != nil {
log.Fatalf("common.Sessioner failed: %v", err)
log.Fatal("common.Sessioner failed: %v", err)
}
return middleware
}
+3 -2
View File
@@ -180,8 +180,9 @@ func InitWebInstalled(ctx context.Context) {
// NormalRoutes represents non install routes
func NormalRoutes() *web.Router {
r := web.NewRouter()
r.Use(common.ProtocolMiddlewares()...)
r.Use(common.MaintenanceModeHandler())
r.BeforeRouting(common.ProtocolMiddlewares()...)
r.AfterRouting(common.MaintenanceModeHandler())
r.Mount("/", web_routers.Routes())
r.Mount("/api/v1", apiv1.Routes())
+3 -2
View File
@@ -20,11 +20,12 @@ import (
// Routes registers the installation routes
func Routes() *web.Router {
base := web.NewRouter()
base.Use(common.ProtocolMiddlewares()...)
base.BeforeRouting(common.ProtocolMiddlewares()...)
base.Methods("GET, HEAD", "/assets/*", public.FileHandlerFunc())
r := web.NewRouter()
r.Use(common.MustInitSessioner(), installContexter())
r.AfterRouting(common.MustInitSessioner(), installContexter())
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)
+3 -3
View File
@@ -54,11 +54,11 @@ func bind[T any](_ T) any {
// These APIs will be invoked by internal commands for example `gitea serv` and etc.
func Routes() *web.Router {
r := web.NewRouter()
r.Use(context.PrivateContexter())
r.Use(authInternal)
r.AfterRouting(context.PrivateContexter())
r.AfterRouting(authInternal)
// Log the real ip address of the request from SSH is really helpful for diagnosing sometimes.
// Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers.
r.Use(chi_middleware.RealIP)
r.AfterRouting(chi_middleware.RealIP)
r.Get("/dummy", misc.DummyOK)
r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent)
+2 -5
View File
@@ -6,12 +6,9 @@ package web
import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/context"
)
func addOwnerRepoGitHTTPRouters(m *web.Router) {
// Some users want to use "web-based git client" to access Gitea's repositories,
// so the CORS handler and OPTIONS method are used.
func addOwnerRepoGitHTTPRouters(m *web.Router, middlewares ...any) {
m.Group("/{username}/{reponame}", func() {
m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack)
@@ -25,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) {
m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
}, repo.HTTPGitEnabledHandler, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb())
}, middlewares...)
}
+93 -50
View File
@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/metrics"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/structs"
@@ -90,32 +91,64 @@ func optionsCorsHandler() func(next http.Handler) http.Handler {
}
}
// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
// in the session (if there is a user id stored in session other plugins might return the user
// object for that id).
//
// The Session plugin is expected to be executed second, in order to skip authentication
// for users that have already signed in.
func buildAuthGroup() *auth_service.Group {
group := auth_service.NewGroup()
group.Add(&auth_service.OAuth2{}) // FIXME: this should be removed and only applied in download and oauth related routers
group.Add(&auth_service.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers
if setting.Service.EnableReverseProxyAuth {
group.Add(&auth_service.ReverseProxy{}) // reverse-proxy should before Session, otherwise the header will be ignored if user has login
}
group.Add(&auth_service.Session{})
if setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext()) {
group.Add(&auth_service.SSPI{}) // it MUST be the last, see the comment of SSPI
}
return group
type AuthMiddleware struct {
AllowOAuth2 web.PreMiddlewareProvider
AllowBasic web.PreMiddlewareProvider
MiddlewareHandler func(*context.Context)
}
func webAuth(authMethod auth_service.Method) func(*context.Context) {
return func(ctx *context.Context) {
ar, err := common.AuthShared(ctx.Base, ctx.Session, authMethod)
func newWebAuthMiddleware() *AuthMiddleware {
type keyAllowOAuth2 struct{}
type keyAllowBasic struct{}
webAuth := &AuthMiddleware{}
middlewareSetContextValue := func(key, val any) web.PreMiddlewareProvider {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dataStore := reqctx.GetRequestDataStore(r.Context())
dataStore.SetContextValue(key, val)
next.ServeHTTP(w, r)
})
}
}
webAuth.AllowBasic = middlewareSetContextValue(keyAllowBasic{}, true)
webAuth.AllowOAuth2 = middlewareSetContextValue(keyAllowOAuth2{}, true)
enableSSPI := setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext())
webAuth.MiddlewareHandler = func(ctx *context.Context) {
allowBasic := ctx.GetContextValue(keyAllowBasic{}) == true
allowOAuth2 := ctx.GetContextValue(keyAllowOAuth2{}) == true
group := auth_service.NewGroup()
// Most auth methods should ignore the user id stored in the session.
// If the auth succeeds, it must use the user id from the auth method to make sure the new login succeeds.
if allowOAuth2 {
group.Add(&auth_service.OAuth2{})
}
if allowBasic {
group.Add(&auth_service.Basic{})
}
// Sessionless means the route's auth can be done without web ui, then it doesn't need to create a session
// For example: accessing git via http, access rss feeds, downloading attachments, etc
isSessionless := allowOAuth2 || allowBasic
if setting.Service.EnableReverseProxyAuth {
// reverse-proxy should before Session, otherwise the header will be ignored if user has login
group.Add(&auth_service.ReverseProxy{CreateSession: !isSessionless})
}
// The Session plugin will skip authentication for users that have already signed in.
group.Add(&auth_service.Session{})
if enableSSPI {
// it MUST be the last, see the comment of SSPI
group.Add(&auth_service.SSPI{CreateSession: !isSessionless})
}
ar, err := common.AuthShared(ctx.Base, ctx.Session, group)
if err != nil {
log.Error("Failed to verify user: %v", err)
ctx.HTTPError(http.StatusUnauthorized, "Failed to authenticate user")
@@ -129,6 +162,7 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) {
_ = ctx.Session.Delete("uid")
}
}
return webAuth
}
// verifyAuthWithOptions checks authentication according to options
@@ -223,6 +257,9 @@ const RouterMockPointBeforeWebRoutes = "before-web-routes"
func Routes() *web.Router {
routes := web.NewRouter()
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
routes.BeforeRouting(chi_middleware.GetHead)
routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
routes.Methods("GET, HEAD, OPTIONS", "/assets/*", optionsCorsHandler(), public.FileHandlerFunc())
routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
@@ -260,10 +297,8 @@ func Routes() *web.Router {
mid = append(mid, common.MustInitSessioner(), context.Contexter())
// Get user from session if logged in.
mid = append(mid, webAuth(buildAuthGroup()))
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
mid = append(mid, chi_middleware.GetHead)
webAuth := newWebAuthMiddleware()
mid = append(mid, webAuth.MiddlewareHandler)
if setting.API.EnableSwagger {
// Note: The route is here but no in API routes because it renders a web page
@@ -272,10 +307,12 @@ func Routes() *web.Router {
mid = append(mid, goGet)
mid = append(mid, common.PageGlobalData)
mid = append(mid, common.BlockExpensive(), common.QoS(), web.RouterMockPoint(RouterMockPointBeforeWebRoutes))
webRoutes := web.NewRouter()
webRoutes.Use(mid...)
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS(), web.RouterMockPoint(RouterMockPointBeforeWebRoutes))
webRoutes.AfterRouting(mid...)
registerWebRoutes(webRoutes, webAuth)
routes.Mount("", webRoutes)
return routes
}
@@ -288,7 +325,7 @@ func Routes() *web.Router {
var optSignInFromAnyOrigin = verifyAuthWithOptions(&common.VerifyOptions{DisableCrossOriginProtection: true})
// registerWebRoutes register routes
func registerWebRoutes(m *web.Router) {
func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
// required to be signed in or signed out
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
@@ -565,7 +602,7 @@ func registerWebRoutes(m *web.Router) {
m.Methods("POST, OPTIONS", "/access_token", web.Bind(forms.AccessTokenForm{}), auth.AccessTokenOAuth)
m.Methods("GET, OPTIONS", "/keys", auth.OIDCKeys)
m.Methods("POST, OPTIONS", "/introspect", web.Bind(forms.IntrospectTokenForm{}), auth.IntrospectOAuth)
}, optionsCorsHandler(), optSignInFromAnyOrigin)
}, optionsCorsHandler(), webAuth.AllowOAuth2, optSignInFromAnyOrigin)
}, oauth2Enabled)
m.Group("/user/settings", func() {
@@ -816,8 +853,9 @@ func registerWebRoutes(m *web.Router) {
// ***** END: Admin *****
m.Group("", func() {
m.Get("/{username}", user.UsernameSubRoute)
m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), repo.GetAttachment)
// it handles "username.rss" in the handler, so allow basic auth as other rss/atom routes
m.Get("/{username}", webAuth.AllowBasic, user.UsernameSubRoute)
m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
}, optSignIn)
m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.ActionUserFollow)
@@ -1188,7 +1226,7 @@ func registerWebRoutes(m *web.Router) {
// end "/{username}/{reponame}/settings"
// user/org home, including rss feeds like "/{username}/{reponame}.rss"
m.Get("/{username}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home)
m.Get("/{username}/{reponame}", optSignIn, webAuth.AllowBasic, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home)
m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup)
@@ -1389,8 +1427,8 @@ func registerWebRoutes(m *web.Router) {
m.Group("/{username}/{reponame}", func() { // repo tags
m.Group("/tags", func() {
m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.TagsList)
m.Get(".rss", feedEnabled, repo.TagsListFeedRSS)
m.Get(".atom", feedEnabled, repo.TagsListFeedAtom)
m.Get(".rss", webAuth.AllowBasic, feedEnabled, repo.TagsListFeedRSS)
m.Get(".atom", webAuth.AllowBasic, feedEnabled, repo.TagsListFeedAtom)
m.Get("/list", repo.GetTagList)
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed))
m.Post("/tags/delete", reqSignIn, reqRepoCodeWriter, context.RepoMustNotBeArchived(), repo.DeleteTag)
@@ -1400,13 +1438,13 @@ func registerWebRoutes(m *web.Router) {
m.Group("/{username}/{reponame}", func() { // repo releases
m.Group("/releases", func() {
m.Get("", repo.Releases)
m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS)
m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom)
m.Get(".rss", webAuth.AllowBasic, feedEnabled, repo.ReleasesFeedRSS)
m.Get(".atom", webAuth.AllowBasic, feedEnabled, repo.ReleasesFeedAtom)
m.Get("/tag/*", repo.SingleRelease)
m.Get("/latest", repo.LatestRelease)
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed))
m.Get("/releases/attachments/{uuid}", repo.GetAttachment)
m.Get("/releases/download/{vTag}/{fileName}", repo.RedirectDownload)
m.Get("/releases/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
m.Get("/releases/download/{vTag}/{fileName}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.RedirectDownload)
m.Group("/releases", func() {
m.Get("/new", repo.NewRelease)
m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
@@ -1423,7 +1461,7 @@ func registerWebRoutes(m *web.Router) {
// end "/{username}/{reponame}": repo releases
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
m.Get("/attachments/{uuid}", repo.GetAttachment)
m.Get("/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
}, optSignIn, context.RepoAssignment)
// end "/{username}/{reponame}": compatibility with old attachments
@@ -1492,7 +1530,7 @@ func registerWebRoutes(m *web.Router) {
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
})
m.Group("/workflows/{workflow_name}", func() {
m.Get("/badge.svg", actions.GetWorkflowBadge)
m.Get("/badge.svg", webAuth.AllowBasic, webAuth.AllowOAuth2, actions.GetWorkflowBadge)
})
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
// end "/{username}/{reponame}/actions"
@@ -1578,7 +1616,7 @@ func registerWebRoutes(m *web.Router) {
m.Group("/archive", func() {
m.Get("/*", repo.Download)
m.Post("/*", repo.InitiateDownload)
}, repo.MustBeNotEmpty, dlSourceEnabled)
}, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.MustBeNotEmpty, dlSourceEnabled)
m.Group("/branches", func() {
m.Get("/list", repo.GetBranchesList)
@@ -1591,7 +1629,7 @@ func registerWebRoutes(m *web.Router) {
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.SingleDownloadOrLFS)
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.SingleDownloadOrLFS)
m.Get("/*", context.RepoRefByType(""), repo.SingleDownloadOrLFS) // "/*" route is deprecated, and kept for backward compatibility
}, repo.MustBeNotEmpty)
}, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.MustBeNotEmpty)
m.Group("/raw", func() {
m.Get("/blob/{sha}", repo.DownloadByID)
@@ -1599,7 +1637,7 @@ func registerWebRoutes(m *web.Router) {
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.SingleDownload)
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.SingleDownload)
m.Get("/*", context.RepoRefByType(""), repo.SingleDownload) // "/*" route is deprecated, and kept for backward compatibility
}, repo.MustBeNotEmpty)
}, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.MustBeNotEmpty)
m.Group("/render", func() {
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RenderFile)
@@ -1632,8 +1670,8 @@ func registerWebRoutes(m *web.Router) {
m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, context.RepoRefByDefaultBranch(), repo.CherryPick)
}, repo.MustBeNotEmpty)
m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeedRSS)
m.Get("/atom/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeedAtom)
m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), webAuth.AllowBasic, feedEnabled, feed.RenderBranchFeedRSS)
m.Get("/atom/branch/*", context.RepoRefByType(git.RefTypeBranch), webAuth.AllowBasic, feedEnabled, feed.RenderBranchFeedAtom)
m.Group("/src", func() {
m.Get("", func(ctx *context.Context) { ctx.Redirect(ctx.Repo.RepoLink) }) // there is no "{owner}/{repo}/src" page, so redirect to "{owner}/{repo}" to avoid 404
@@ -1660,9 +1698,14 @@ func registerWebRoutes(m *web.Router) {
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
}, optSignIn, context.RepoAssignment)
common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, repo.CorsHandler(), optSignInFromAnyOrigin) // "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters
// git lfs uses its own jwt key, and it handles the token & auth by itself, it conflicts with the general "OAuth2" auth method
// pattern: "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters
common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, webAuth.AllowBasic, repo.CorsHandler(), optSignInFromAnyOrigin)
addOwnerRepoGitHTTPRouters(m) // "/{username}/{reponame}/{git-paths}": git http support
// Some users want to use "web-based git client" to access Gitea's repositories,
// so the CORS handler and OPTIONS method are used.
// pattern: "/{username}/{reponame}/{git-paths}": git http support
addOwnerRepoGitHTTPRouters(m, repo.HTTPGitEnabledHandler, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb())
m.Group("/notifications", func() {
m.Get("", user.Notifications)