From dd136858f1ea40ad3c94191d647487fa4f31926c Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 18 Oct 2024 20:33:49 +0200 Subject: Adding upstream version 9.0.0. Signed-off-by: Daniel Baumann --- routers/common/auth.go | 45 +++++++++++++++ routers/common/compare.go | 21 +++++++ routers/common/db.go | 59 +++++++++++++++++++ routers/common/errpage.go | 56 ++++++++++++++++++ routers/common/errpage_test.go | 39 +++++++++++++ routers/common/markup.go | 98 ++++++++++++++++++++++++++++++++ routers/common/middleware.go | 116 ++++++++++++++++++++++++++++++++++++++ routers/common/middleware_test.go | 70 +++++++++++++++++++++++ routers/common/redirect.go | 26 +++++++++ routers/common/serve.go | 43 ++++++++++++++ 10 files changed, 573 insertions(+) create mode 100644 routers/common/auth.go create mode 100644 routers/common/compare.go create mode 100644 routers/common/db.go create mode 100644 routers/common/errpage.go create mode 100644 routers/common/errpage_test.go create mode 100644 routers/common/markup.go create mode 100644 routers/common/middleware.go create mode 100644 routers/common/middleware_test.go create mode 100644 routers/common/redirect.go create mode 100644 routers/common/serve.go (limited to 'routers/common') diff --git a/routers/common/auth.go b/routers/common/auth.go new file mode 100644 index 0000000..115d65e --- /dev/null +++ b/routers/common/auth.go @@ -0,0 +1,45 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/web/middleware" + auth_service "code.gitea.io/gitea/services/auth" + "code.gitea.io/gitea/services/context" +) + +type AuthResult struct { + Doer *user_model.User + IsBasicAuth bool +} + +func AuthShared(ctx *context.Base, sessionStore auth_service.SessionStore, authMethod auth_service.Method) (ar AuthResult, err error) { + ar.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, sessionStore) + if err != nil { + return ar, err + } + if ar.Doer != nil { + if ctx.Locale.Language() != ar.Doer.Language { + ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) + } + ar.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth_service.BasicMethodName + + ctx.Data["IsSigned"] = true + ctx.Data[middleware.ContextDataKeySignedUser] = ar.Doer + ctx.Data["SignedUserID"] = ar.Doer.ID + ctx.Data["IsAdmin"] = ar.Doer.IsAdmin + } else { + ctx.Data["SignedUserID"] = int64(0) + } + return ar, nil +} + +// VerifyOptions contains required or check options +type VerifyOptions struct { + SignInRequired bool + SignOutRequired bool + AdminRequired bool + DisableCSRF bool +} diff --git a/routers/common/compare.go b/routers/common/compare.go new file mode 100644 index 0000000..4d1cc2f --- /dev/null +++ b/routers/common/compare.go @@ -0,0 +1,21 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" +) + +// CompareInfo represents the collected results from ParseCompareInfo +type CompareInfo struct { + HeadUser *user_model.User + HeadRepo *repo_model.Repository + HeadGitRepo *git.Repository + CompareInfo *git.CompareInfo + BaseBranch string + HeadBranch string + DirectComparison bool +} diff --git a/routers/common/db.go b/routers/common/db.go new file mode 100644 index 0000000..d7dcfa0 --- /dev/null +++ b/routers/common/db.go @@ -0,0 +1,59 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "context" + "fmt" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/migrations" + system_model "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/setting/config" + + "xorm.io/xorm" +) + +// InitDBEngine In case of problems connecting to DB, retry connection. Eg, PGSQL in Docker Container on Synology +func InitDBEngine(ctx context.Context) (err error) { + log.Info("Beginning ORM engine initialization.") + for i := 0; i < setting.Database.DBConnectRetries; i++ { + select { + case <-ctx.Done(): + return fmt.Errorf("Aborted due to shutdown:\nin retry ORM engine initialization") + default: + } + log.Info("ORM engine initialization attempt #%d/%d...", i+1, setting.Database.DBConnectRetries) + if err = db.InitEngineWithMigration(ctx, migrateWithSetting); err == nil { + break + } else if i == setting.Database.DBConnectRetries-1 { + return err + } + log.Error("ORM engine initialization attempt #%d/%d failed. Error: %v", i+1, setting.Database.DBConnectRetries, err) + log.Info("Backing off for %d seconds", int64(setting.Database.DBConnectBackoff/time.Second)) + time.Sleep(setting.Database.DBConnectBackoff) + } + config.SetDynGetter(system_model.NewDatabaseDynKeyGetter()) + return nil +} + +func migrateWithSetting(x *xorm.Engine) error { + if setting.Database.AutoMigration { + return migrations.Migrate(x) + } + + if current, err := migrations.GetCurrentDBVersion(x); err != nil { + return err + } else if current < 0 { + // execute migrations when the database isn't initialized even if AutoMigration is false + return migrations.Migrate(x) + } else if expected := migrations.ExpectedVersion(); current != expected { + log.Fatal(`"database.AUTO_MIGRATION" is disabled, but current database version %d is not equal to the expected version %d.`+ + `You can set "database.AUTO_MIGRATION" to true or migrate manually by running "forgejo [--config /path/to/app.ini] migrate"`, current, expected) + } + return nil +} diff --git a/routers/common/errpage.go b/routers/common/errpage.go new file mode 100644 index 0000000..402ca44 --- /dev/null +++ b/routers/common/errpage.go @@ -0,0 +1,56 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "fmt" + "net/http" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/modules/web/routing" + "code.gitea.io/gitea/services/context" +) + +const tplStatus500 base.TplName = "status/500" + +// 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) + + defer func() { + if err := recover(); err != nil { + log.Error("Panic occurs again when rendering error page: %v. Stack:\n%s", err, log.Stack(2)) + } + }() + + routing.UpdatePanicError(req.Context(), err) + + httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform") + w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) + + tmplCtx := context.TemplateContext{} + tmplCtx["Locale"] = middleware.Locale(w, req) + ctxData := middleware.GetContextData(req.Context()) + + // This recovery handler could be called without Gitea's web context, so we shouldn't touch that context too much. + // 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) { + ctxData["ErrorMsg"] = "PANIC: " + combinedErr + } + + err = templates.HTMLRenderer().HTML(w, http.StatusInternalServerError, string(tplStatus500), ctxData, tmplCtx) + if err != nil { + log.Error("Error occurs again when rendering error page: %v", err) + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Internal server error, please collect error logs and report to Gitea issue tracker")) + } +} diff --git a/routers/common/errpage_test.go b/routers/common/errpage_test.go new file mode 100644 index 0000000..4fd63ba --- /dev/null +++ b/routers/common/errpage_test.go @@ -0,0 +1,39 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/web/middleware" + + "github.com/stretchr/testify/assert" +) + +func TestRenderPanicErrorPage(t *testing.T) { + w := httptest.NewRecorder() + req := &http.Request{URL: &url.URL{}} + req = req.WithContext(middleware.WithContextData(context.Background())) + 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, ``) + assert.Contains(t, respContent, `lang="en-US"`) // make sure the locale work + + // the 500 page doesn't have normal pages footer, it makes it easier to distinguish a normal page and a failed page. + // especially when a sub-template causes page error, the HTTP response code is still 200, + // the different "footer" is the only way to know whether a page is fully rendered without error. + assert.False(t, test.IsNormalPageCompleted(respContent)) +} + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/routers/common/markup.go b/routers/common/markup.go new file mode 100644 index 0000000..2d5638e --- /dev/null +++ b/routers/common/markup.go @@ -0,0 +1,98 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "fmt" + "net/http" + "strings" + + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + + "mvdan.cc/xurls/v2" +) + +// RenderMarkup renders markup text for the /markup and /markdown endpoints +func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPrefix, filePath string, wiki bool) { + var markupType string + relativePath := "" + + if len(text) == 0 { + _, _ = ctx.Write([]byte("")) + return + } + + switch mode { + case "markdown": + // Raw markdown + if err := markdown.RenderRaw(&markup.RenderContext{ + Ctx: ctx, + Links: markup.Links{ + AbsolutePrefix: true, + Base: urlPrefix, + }, + }, strings.NewReader(text), ctx.Resp); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + } + return + case "comment": + // Comment as markdown + markupType = markdown.MarkupName + case "gfm": + // Github Flavored Markdown as document + markupType = markdown.MarkupName + case "file": + // File as document based on file extension + markupType = "" + relativePath = filePath + default: + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode)) + return + } + + if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { + // check if urlPrefix is already set to a URL + linkRegex, _ := xurls.StrictMatchingScheme("https?://") + m := linkRegex.FindStringIndex(urlPrefix) + if m == nil { + urlPrefix = util.URLJoin(setting.AppURL, urlPrefix) + } + } + + meta := map[string]string{} + if repo != nil && repo.Repository != nil { + if mode == "comment" { + meta = repo.Repository.ComposeMetas(ctx) + } else { + meta = repo.Repository.ComposeDocumentMetas(ctx) + } + } + if mode != "comment" { + meta["mode"] = "document" + } + + if err := markup.Render(&markup.RenderContext{ + Ctx: ctx, + Links: markup.Links{ + AbsolutePrefix: true, + Base: urlPrefix, + }, + Metas: meta, + IsWiki: wiki, + Type: markupType, + RelativePath: relativePath, + }, strings.NewReader(text), ctx.Resp); err != nil { + if markup.IsErrUnsupportedRenderExtension(err) { + ctx.Error(http.StatusUnprocessableEntity, err.Error()) + } else { + ctx.Error(http.StatusInternalServerError, err.Error()) + } + return + } +} diff --git a/routers/common/middleware.go b/routers/common/middleware.go new file mode 100644 index 0000000..59e59b8 --- /dev/null +++ b/routers/common/middleware.go @@ -0,0 +1,116 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "fmt" + "net/http" + "strings" + + "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/modules/web/routing" + "code.gitea.io/gitea/services/context" + + "code.forgejo.org/go-chi/session" + "github.com/chi-middleware/proxy" + chi "github.com/go-chi/chi/v5" +) + +// ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery +func ProtocolMiddlewares() (handlers []any) { + // first, normalize the URL path + handlers = append(handlers, stripSlashesMiddleware) + + // prepare the ContextData and panic recovery + handlers = append(handlers, func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + defer func() { + if err := recover(); err != nil { + RenderPanicErrorPage(resp, req, err) // it should never panic + } + }() + req = req.WithContext(middleware.WithContextData(req.Context())) + next.ServeHTTP(resp, req) + }) + }) + + // wrap the request and response, use the process context and add it to the process manager + handlers = append(handlers, func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true) + defer finished() + next.ServeHTTP(context.WrapResponseWriter(resp), req.WithContext(cache.WithCacheContext(ctx))) + }) + }) + + if setting.ReverseProxyLimit > 0 { + opt := proxy.NewForwardedHeadersOptions(). + WithForwardLimit(setting.ReverseProxyLimit). + ClearTrustedProxies() + for _, n := range setting.ReverseProxyTrustedProxies { + if !strings.Contains(n, "/") { + opt.AddTrustedProxy(n) + } else { + opt.AddTrustedNetwork(n) + } + } + handlers = append(handlers, proxy.ForwardedHeaders(opt)) + } + + if setting.IsRouteLogEnabled() { + handlers = append(handlers, routing.NewLoggerHandler()) + } + + if setting.IsAccessLogEnabled() { + handlers = append(handlers, context.AccessLogger()) + } + + return handlers +} + +func stripSlashesMiddleware(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() + + urlPath := req.URL.RawPath + rctx := chi.RouteContext(req.Context()) + if rctx != nil && rctx.RoutePath != "" { + urlPath = rctx.RoutePath + } + + sanitizedPath := &strings.Builder{} + prevWasSlash := false + for _, chr := range strings.TrimRight(urlPath, "/") { + if chr != '/' || !prevWasSlash { + sanitizedPath.WriteRune(chr) + } + prevWasSlash = chr == '/' + } + + if rctx == nil { + req.URL.Path = sanitizedPath.String() + } else { + rctx.RoutePath = sanitizedPath.String() + } + next.ServeHTTP(resp, req) + }) +} + +func Sessioner() func(next http.Handler) http.Handler { + return session.Sessioner(session.Options{ + Provider: setting.SessionConfig.Provider, + ProviderConfig: setting.SessionConfig.ProviderConfig, + CookieName: setting.SessionConfig.CookieName, + CookiePath: setting.SessionConfig.CookiePath, + Gclifetime: setting.SessionConfig.Gclifetime, + Maxlifetime: setting.SessionConfig.Maxlifetime, + Secure: setting.SessionConfig.Secure, + SameSite: setting.SessionConfig.SameSite, + Domain: setting.SessionConfig.Domain, + }) +} diff --git a/routers/common/middleware_test.go b/routers/common/middleware_test.go new file mode 100644 index 0000000..f16b937 --- /dev/null +++ b/routers/common/middleware_test.go @@ -0,0 +1,70 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package common + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStripSlashesMiddleware(t *testing.T) { + type test struct { + name string + expectedPath string + inputPath string + } + + tests := []test{ + { + name: "path with multiple slashes", + inputPath: "https://github.com///go-gitea//gitea.git", + expectedPath: "/go-gitea/gitea.git", + }, + { + name: "path with no slashes", + inputPath: "https://github.com/go-gitea/gitea.git", + expectedPath: "/go-gitea/gitea.git", + }, + { + name: "path with slashes in the middle", + inputPath: "https://git.data.coop//halfd/new-website.git", + expectedPath: "/halfd/new-website.git", + }, + { + name: "path with slashes in the middle", + inputPath: "https://git.data.coop//halfd/new-website.git", + expectedPath: "/halfd/new-website.git", + }, + { + name: "path with slashes in the end", + inputPath: "/user2//repo1/", + expectedPath: "/user2/repo1", + }, + { + name: "path with slashes and query params", + inputPath: "/repo//migrate?service_type=3", + expectedPath: "/repo/migrate", + }, + { + name: "path with encoded slash", + inputPath: "/user2/%2F%2Frepo1", + expectedPath: "/user2/%2F%2Frepo1", + }, + } + + for _, tt := range tests { + testMiddleware := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, tt.expectedPath, r.URL.Path) + }) + + // pass the test middleware to validate the changes + handlerToTest := stripSlashesMiddleware(testMiddleware) + // create a mock request to use + req := httptest.NewRequest("GET", tt.inputPath, nil) + // call the handler using a mock response recorder + handlerToTest.ServeHTTP(httptest.NewRecorder(), req) + } +} diff --git a/routers/common/redirect.go b/routers/common/redirect.go new file mode 100644 index 0000000..9bf2025 --- /dev/null +++ b/routers/common/redirect.go @@ -0,0 +1,26 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "net/http" + + "code.gitea.io/gitea/modules/httplib" +) + +// FetchRedirectDelegate helps the "fetch" requests to redirect to the correct location +func FetchRedirectDelegate(resp http.ResponseWriter, req *http.Request) { + // When use "fetch" to post requests and the response is a redirect, browser's "location.href = uri" has limitations. + // 1. change "location" from old "/foo" to new "/foo#hash", the browser will not reload the page. + // 2. when use "window.reload()", the hash is not respected, the newly loaded page won't scroll to the hash target. + // The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2", + // then frontend needs this delegate to redirect to the new location with hash correctly. + redirect := req.PostFormValue("redirect") + if httplib.IsRiskyRedirectURL(redirect) { + resp.WriteHeader(http.StatusBadRequest) + return + } + resp.Header().Add("Location", redirect) + resp.WriteHeader(http.StatusSeeOther) +} diff --git a/routers/common/serve.go b/routers/common/serve.go new file mode 100644 index 0000000..446908d --- /dev/null +++ b/routers/common/serve.go @@ -0,0 +1,43 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "io" + "time" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/services/context" +) + +// ServeBlob download a git.Blob +func ServeBlob(ctx *context.Base, filePath string, blob *git.Blob, lastModified *time.Time) error { + if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { + return nil + } + + dataRc, err := blob.DataAsync() + if err != nil { + return err + } + defer func() { + if err = dataRc.Close(); err != nil { + log.Error("ServeBlob: Close: %v", err) + } + }() + + httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, blob.Size(), dataRc) + return nil +} + +func ServeContentByReader(ctx *context.Base, filePath string, size int64, reader io.Reader) { + httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, size, reader) +} + +func ServeContentByReadSeeker(ctx *context.Base, filePath string, modTime *time.Time, reader io.ReadSeeker) { + httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, filePath, modTime, reader) +} -- cgit v1.2.3