summaryrefslogtreecommitdiffstats
path: root/routers/common
diff options
context:
space:
mode:
Diffstat (limited to 'routers/common')
-rw-r--r--routers/common/auth.go45
-rw-r--r--routers/common/compare.go21
-rw-r--r--routers/common/db.go59
-rw-r--r--routers/common/errpage.go56
-rw-r--r--routers/common/errpage_test.go39
-rw-r--r--routers/common/markup.go98
-rw-r--r--routers/common/middleware.go116
-rw-r--r--routers/common/middleware_test.go70
-rw-r--r--routers/common/redirect.go26
-rw-r--r--routers/common/serve.go43
10 files changed, 573 insertions, 0 deletions
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, `</html>`)
+ 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)
+}