first-commit
This commit is contained in:
71
routers/common/actions.go
Normal file
71
routers/common/actions.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func DownloadActionsRunJobLogsWithIndex(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobIndex int64) error {
|
||||
runJobs, err := actions_model.GetRunJobsByRunID(ctx, runID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetRunJobsByRunID: %w", err)
|
||||
}
|
||||
if err = runJobs.LoadRepos(ctx); err != nil {
|
||||
return fmt.Errorf("LoadRepos: %w", err)
|
||||
}
|
||||
if jobIndex < 0 || jobIndex >= int64(len(runJobs)) {
|
||||
return util.NewNotExistErrorf("job index is out of range: %d", jobIndex)
|
||||
}
|
||||
return DownloadActionsRunJobLogs(ctx, ctxRepo, runJobs[jobIndex])
|
||||
}
|
||||
|
||||
func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error {
|
||||
if curJob.Repo.ID != ctxRepo.ID {
|
||||
return util.NewNotExistErrorf("job not found")
|
||||
}
|
||||
|
||||
if curJob.TaskID == 0 {
|
||||
return util.NewNotExistErrorf("job not started")
|
||||
}
|
||||
|
||||
if err := curJob.LoadRun(ctx); err != nil {
|
||||
return fmt.Errorf("LoadRun: %w", err)
|
||||
}
|
||||
|
||||
task, err := actions_model.GetTaskByID(ctx, curJob.TaskID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetTaskByID: %w", err)
|
||||
}
|
||||
|
||||
if task.LogExpired {
|
||||
return util.NewNotExistErrorf("logs have been cleaned up")
|
||||
}
|
||||
|
||||
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenLogs: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
workflowName := curJob.Run.WorkflowID
|
||||
if p := strings.Index(workflowName, "."); p > 0 {
|
||||
workflowName = workflowName[0:p]
|
||||
}
|
||||
ctx.ServeContent(reader, &context.ServeHeaderOptions{
|
||||
Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID),
|
||||
ContentLength: &task.LogSize,
|
||||
ContentType: "text/plain",
|
||||
ContentTypeCharset: "utf-8",
|
||||
Disposition: "attachment",
|
||||
})
|
||||
return nil
|
||||
}
|
45
routers/common/auth.go
Normal file
45
routers/common/auth.go
Normal file
@@ -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
|
||||
}
|
90
routers/common/blockexpensive.go
Normal file
90
routers/common/blockexpensive.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func BlockExpensive() func(next http.Handler) http.Handler {
|
||||
if !setting.Service.BlockAnonymousAccessExpensive {
|
||||
return nil
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ret := determineRequestPriority(reqctx.FromContext(req.Context()))
|
||||
if !ret.SignedIn {
|
||||
if ret.Expensive || ret.LongPolling {
|
||||
http.Redirect(w, req, setting.AppSubURL+"/user/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isRoutePathExpensive(routePattern string) bool {
|
||||
if strings.HasPrefix(routePattern, "/user/") || strings.HasPrefix(routePattern, "/login/") {
|
||||
return false
|
||||
}
|
||||
|
||||
expensivePaths := []string{
|
||||
// code related
|
||||
"/{username}/{reponame}/archive/",
|
||||
"/{username}/{reponame}/blame/",
|
||||
"/{username}/{reponame}/commit/",
|
||||
"/{username}/{reponame}/commits/",
|
||||
"/{username}/{reponame}/graph",
|
||||
"/{username}/{reponame}/media/",
|
||||
"/{username}/{reponame}/raw/",
|
||||
"/{username}/{reponame}/src/",
|
||||
|
||||
// issue & PR related (no trailing slash)
|
||||
"/{username}/{reponame}/issues",
|
||||
"/{username}/{reponame}/{type:issues}",
|
||||
"/{username}/{reponame}/pulls",
|
||||
"/{username}/{reponame}/{type:pulls}",
|
||||
|
||||
// wiki
|
||||
"/{username}/{reponame}/wiki/",
|
||||
|
||||
// activity
|
||||
"/{username}/{reponame}/activity/",
|
||||
}
|
||||
for _, path := range expensivePaths {
|
||||
if strings.HasPrefix(routePattern, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isRoutePathForLongPolling(routePattern string) bool {
|
||||
return routePattern == "/user/events"
|
||||
}
|
||||
|
||||
func determineRequestPriority(reqCtx reqctx.RequestContext) (ret struct {
|
||||
SignedIn bool
|
||||
Expensive bool
|
||||
LongPolling bool
|
||||
},
|
||||
) {
|
||||
chiRoutePath := chi.RouteContext(reqCtx).RoutePattern()
|
||||
if _, ok := reqCtx.GetData()[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
|
||||
ret.SignedIn = true
|
||||
} else {
|
||||
ret.Expensive = isRoutePathExpensive(chiRoutePath)
|
||||
ret.LongPolling = isRoutePathForLongPolling(chiRoutePath)
|
||||
}
|
||||
return ret
|
||||
}
|
30
routers/common/blockexpensive_test.go
Normal file
30
routers/common/blockexpensive_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBlockExpensive(t *testing.T) {
|
||||
cases := []struct {
|
||||
expensive bool
|
||||
routePath string
|
||||
}{
|
||||
{false, "/user/xxx"},
|
||||
{false, "/login/xxx"},
|
||||
{true, "/{username}/{reponame}/archive/xxx"},
|
||||
{true, "/{username}/{reponame}/graph"},
|
||||
{true, "/{username}/{reponame}/src/xxx"},
|
||||
{true, "/{username}/{reponame}/wiki/xxx"},
|
||||
{true, "/{username}/{reponame}/activity/xxx"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expensive, isRoutePathExpensive(c.routePath), "routePath: %s", c.routePath)
|
||||
}
|
||||
|
||||
assert.True(t, isRoutePathForLongPolling("/user/events"))
|
||||
}
|
33
routers/common/codesearch.go
Normal file
33
routers/common/codesearch.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/indexer"
|
||||
code_indexer "code.gitea.io/gitea/modules/indexer/code"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func PrepareCodeSearch(ctx *context.Context) (ret struct {
|
||||
Keyword string
|
||||
Language string
|
||||
SearchMode indexer.SearchModeType
|
||||
},
|
||||
) {
|
||||
ret.Language = ctx.FormTrim("l")
|
||||
ret.Keyword = ctx.FormTrim("q")
|
||||
ret.SearchMode = indexer.SearchModeType(ctx.FormTrim("search_mode"))
|
||||
|
||||
ctx.Data["Keyword"] = ret.Keyword
|
||||
ctx.Data["Language"] = ret.Language
|
||||
ctx.Data["SelectedSearchMode"] = string(ret.SearchMode)
|
||||
if setting.Indexer.RepoIndexerEnabled {
|
||||
ctx.Data["SearchModes"] = code_indexer.SupportedSearchModes()
|
||||
} else {
|
||||
ctx.Data["SearchModes"] = indexer.GitGrepSupportedSearchModes()
|
||||
}
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
return ret
|
||||
}
|
21
routers/common/compare.go
Normal file
21
routers/common/compare.go
Normal file
@@ -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
|
||||
}
|
60
routers/common/db.go
Normal file
60
routers/common/db.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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"
|
||||
"code.gitea.io/gitea/services/versioned_migration"
|
||||
|
||||
"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 errors.New("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(ctx context.Context, x *xorm.Engine) error {
|
||||
if setting.Database.AutoMigration {
|
||||
return versioned_migration.Migrate(ctx, 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 versioned_migration.Migrate(ctx, x)
|
||||
} else if expected := migrations.ExpectedDBVersion(); 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 "gitea [--config /path/to/app.ini] migrate"`, current, expected)
|
||||
}
|
||||
return nil
|
||||
}
|
31
routers/common/deadline.go
Normal file
31
routers/common/deadline.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
func ParseDeadlineDateToEndOfDay(date string) (timeutil.TimeStamp, error) {
|
||||
if date == "" {
|
||||
return 0, nil
|
||||
}
|
||||
deadline, err := time.ParseInLocation("2006-01-02", date, setting.DefaultUILocation)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
|
||||
return timeutil.TimeStamp(deadline.Unix()), nil
|
||||
}
|
||||
|
||||
func ParseAPIDeadlineToEndOfDay(t *time.Time) (timeutil.TimeStamp, error) {
|
||||
if t == nil || t.IsZero() || t.Unix() == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
deadline := time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, setting.DefaultUILocation)
|
||||
return timeutil.TimeStamp(deadline.Unix()), nil
|
||||
}
|
55
routers/common/errpage.go
Normal file
55
routers/common/errpage.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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/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 templates.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(), &httpcache.CacheControlOptions{NoTransform: true})
|
||||
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, 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"))
|
||||
}
|
||||
}
|
38
routers/common/errpage_test.go
Normal file
38
routers/common/errpage_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRenderPanicErrorPage(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req := &http.Request{URL: &url.URL{}}
|
||||
req = req.WithContext(reqctx.NewRequestContextForTest(t.Context()))
|
||||
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)
|
||||
}
|
31
routers/common/lfs.go
Normal file
31
routers/common/lfs.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/lfs"
|
||||
)
|
||||
|
||||
const RouterMockPointCommonLFS = "common-lfs"
|
||||
|
||||
func AddOwnerRepoGitLFSRoutes(m *web.Router, middlewares ...any) {
|
||||
// shared by web and internal routers
|
||||
m.Group("/{username}/{reponame}/info/lfs", func() {
|
||||
m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler)
|
||||
m.Put("/objects/{oid}/{size}", lfs.UploadHandler)
|
||||
m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler)
|
||||
m.Get("/objects/{oid}", lfs.DownloadHandler)
|
||||
m.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler)
|
||||
m.Group("/locks", func() {
|
||||
m.Get("/", lfs.GetListLockHandler)
|
||||
m.Post("/", lfs.PostLockHandler)
|
||||
m.Post("/verify", lfs.VerifyLockHandler)
|
||||
m.Post("/{lid}/unlock", lfs.UnLockHandler)
|
||||
}, lfs.CheckAcceptMediaType)
|
||||
m.Any("/*", http.NotFound)
|
||||
}, append([]any{web.RouterMockPoint(RouterMockPointCommonLFS)}, middlewares...)...)
|
||||
}
|
107
routers/common/markup.go
Normal file
107
routers/common/markup.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"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"
|
||||
)
|
||||
|
||||
// RenderMarkup renders markup text for the /markup and /markdown endpoints
|
||||
func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, urlPathContext, filePath string) {
|
||||
// urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}"
|
||||
// filePath is the path of the file to render if the end user is trying to preview a repo file (mode == "file")
|
||||
// filePath will be used as RenderContext.RelativePath
|
||||
|
||||
// for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md"
|
||||
// and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
|
||||
|
||||
if mode == "" || mode == "markdown" {
|
||||
// raw markdown doesn't need any special handling
|
||||
baseLink := urlPathContext
|
||||
if baseLink == "" {
|
||||
baseLink = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
|
||||
}
|
||||
rctx := renderhelper.NewRenderContextSimpleDocument(ctx, baseLink).WithUseAbsoluteLink(true).
|
||||
WithMarkupType(markdown.MarkupName)
|
||||
if err := markdown.RenderRaw(rctx, strings.NewReader(text), ctx.Resp); err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Ideally, this handler should be called with RepoAssigment and get the related repo from context "/owner/repo/markup"
|
||||
// then render could use the repo to do various things (the permission check has passed)
|
||||
//
|
||||
// However, this handler is also exposed as "/markup" without any repo context,
|
||||
// then since there is no permission check, so we can't use the repo from "context" parameter,
|
||||
// in this case, only the "path" information could be used which doesn't cause security problems.
|
||||
var repoModel *repo.Repository
|
||||
if ctxRepo != nil {
|
||||
repoModel = ctxRepo.Repository
|
||||
}
|
||||
var repoOwnerName, repoName, refPath, treePath string
|
||||
repoLinkPath := strings.TrimPrefix(urlPathContext, setting.AppSubURL+"/")
|
||||
fields := strings.SplitN(repoLinkPath, "/", 5)
|
||||
if len(fields) == 5 && fields[2] == "src" && (fields[3] == "branch" || fields[3] == "commit" || fields[3] == "tag") {
|
||||
// absolute base prefix is something like "https://host/subpath/{user}/{repo}"
|
||||
repoOwnerName, repoName = fields[0], fields[1]
|
||||
treePath = path.Dir(filePath) // it is "doc" if filePath is "doc/CHANGE.md"
|
||||
refPath = strings.Join(fields[3:], "/") // it is "branch/features/feat-12/doc"
|
||||
refPath = strings.TrimSuffix(refPath, "/"+treePath) // now we get the correct branch path: "branch/features/feat-12"
|
||||
} else if fields = strings.SplitN(repoLinkPath, "/", 3); len(fields) == 2 {
|
||||
repoOwnerName, repoName = fields[0], fields[1]
|
||||
}
|
||||
|
||||
var rctx *markup.RenderContext
|
||||
switch mode {
|
||||
case "gfm": // legacy mode
|
||||
rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{
|
||||
DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName,
|
||||
CurrentRefPath: refPath, CurrentTreePath: treePath,
|
||||
})
|
||||
rctx = rctx.WithMarkupType(markdown.MarkupName)
|
||||
case "comment":
|
||||
rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{
|
||||
DeprecatedOwnerName: repoOwnerName,
|
||||
DeprecatedRepoName: repoName,
|
||||
FootnoteContextID: "preview",
|
||||
})
|
||||
rctx = rctx.WithMarkupType(markdown.MarkupName)
|
||||
case "wiki":
|
||||
rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
|
||||
rctx = rctx.WithMarkupType(markdown.MarkupName)
|
||||
case "file":
|
||||
rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{
|
||||
DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName,
|
||||
CurrentRefPath: refPath, CurrentTreePath: treePath,
|
||||
})
|
||||
rctx = rctx.WithMarkupType("").WithRelativePath(filePath) // render the repo file content by its extension
|
||||
default:
|
||||
ctx.HTTPError(http.StatusUnprocessableEntity, "Unknown mode: "+mode)
|
||||
return
|
||||
}
|
||||
rctx = rctx.WithUseAbsoluteLink(true)
|
||||
if err := markup.Render(rctx, strings.NewReader(text), ctx.Resp); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.HTTPError(http.StatusUnprocessableEntity, err.Error())
|
||||
} else {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
122
routers/common/middleware.go
Normal file
122
routers/common/middleware.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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/gtprof"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/routing"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"github.com/chi-middleware/proxy"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery
|
||||
func ProtocolMiddlewares() (handlers []any) {
|
||||
// the order is important
|
||||
handlers = append(handlers, ChiRoutePathHandler()) // make sure chi has correct paths
|
||||
handlers = append(handlers, RequestContextHandler()) // prepare the context and panic recovery
|
||||
|
||||
if setting.ReverseProxyLimit > 0 && len(setting.ReverseProxyTrustedProxies) > 0 {
|
||||
handlers = append(handlers, ForwardedHeadersHandler(setting.ReverseProxyLimit, setting.ReverseProxyTrustedProxies))
|
||||
}
|
||||
|
||||
if setting.IsRouteLogEnabled() {
|
||||
handlers = append(handlers, routing.NewLoggerHandler())
|
||||
}
|
||||
|
||||
if setting.IsAccessLogEnabled() {
|
||||
handlers = append(handlers, context.AccessLogger())
|
||||
}
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
func RequestContextHandler() func(h http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
|
||||
// this response writer might not be the same as the one in context.Base.Resp
|
||||
// because there might be a "gzip writer" in the middle, so the "written size" here is the compressed size
|
||||
respWriter := context.WrapResponseWriter(respOrig)
|
||||
|
||||
profDesc := fmt.Sprintf("HTTP: %s %s", req.Method, req.RequestURI)
|
||||
ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc)
|
||||
defer finished()
|
||||
|
||||
ctx, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanHTTP)
|
||||
req = req.WithContext(ctx)
|
||||
defer func() {
|
||||
chiCtx := chi.RouteContext(req.Context())
|
||||
span.SetAttributeString(gtprof.TraceAttrHTTPRoute, chiCtx.RoutePattern())
|
||||
span.End()
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
RenderPanicErrorPage(respWriter, req, err) // it should never panic
|
||||
}
|
||||
}()
|
||||
|
||||
ds := reqctx.GetRequestDataStore(ctx)
|
||||
req = req.WithContext(cache.WithCacheContext(ctx))
|
||||
ds.SetContextValue(httplib.RequestContextKey, req)
|
||||
ds.AddCleanUp(func() {
|
||||
if req.MultipartForm != nil {
|
||||
_ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
||||
}
|
||||
})
|
||||
next.ServeHTTP(respWriter, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ChiRoutePathHandler() func(h http.Handler) http.Handler {
|
||||
// make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
chiCtx := chi.RouteContext(req.Context())
|
||||
if req.URL.RawPath == "" {
|
||||
chiCtx.RoutePath = req.URL.EscapedPath()
|
||||
} else {
|
||||
chiCtx.RoutePath = req.URL.RawPath
|
||||
}
|
||||
next.ServeHTTP(resp, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ForwardedHeadersHandler(limit int, trustedProxies []string) func(h http.Handler) http.Handler {
|
||||
opt := proxy.NewForwardedHeadersOptions().WithForwardLimit(limit).ClearTrustedProxies()
|
||||
for _, n := range trustedProxies {
|
||||
if !strings.Contains(n, "/") {
|
||||
opt.AddTrustedProxy(n)
|
||||
} else {
|
||||
opt.AddTrustedNetwork(n)
|
||||
}
|
||||
}
|
||||
return proxy.ForwardedHeaders(opt)
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
83
routers/common/pagetmpl.go
Normal file
83
routers/common/pagetmpl.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
goctx "context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering
|
||||
type StopwatchTmplInfo struct {
|
||||
IssueLink string
|
||||
RepoSlug string
|
||||
IssueIndex int64
|
||||
Seconds int64
|
||||
}
|
||||
|
||||
func getActiveStopwatch(ctx *context.Context) *StopwatchTmplInfo {
|
||||
if ctx.Doer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, sw, issue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, goctx.Canceled) {
|
||||
log.Error("Unable to HasUserStopwatch for user:%-v: %v", ctx.Doer, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if sw == nil || sw.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &StopwatchTmplInfo{
|
||||
issue.Link(),
|
||||
issue.Repo.FullName(),
|
||||
issue.Index,
|
||||
sw.Seconds() + 1, // ensure time is never zero in ui
|
||||
}
|
||||
}
|
||||
|
||||
func notificationUnreadCount(ctx *context.Context) int64 {
|
||||
if ctx.Doer == nil {
|
||||
return 0
|
||||
}
|
||||
count, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
|
||||
UserID: ctx.Doer.ID,
|
||||
Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
|
||||
})
|
||||
if err != nil {
|
||||
if !errors.Is(err, goctx.Canceled) {
|
||||
log.Error("Unable to find notification for user:%-v: %v", ctx.Doer, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
type pageGlobalDataType struct {
|
||||
IsSigned bool
|
||||
IsSiteAdmin bool
|
||||
|
||||
GetNotificationUnreadCount func() int64
|
||||
GetActiveStopwatch func() *StopwatchTmplInfo
|
||||
}
|
||||
|
||||
func PageGlobalData(ctx *context.Context) {
|
||||
var data pageGlobalDataType
|
||||
data.IsSigned = ctx.Doer != nil
|
||||
data.IsSiteAdmin = ctx.Doer != nil && ctx.Doer.IsAdmin
|
||||
data.GetNotificationUnreadCount = sync.OnceValue(func() int64 { return notificationUnreadCount(ctx) })
|
||||
data.GetActiveStopwatch = sync.OnceValue(func() *StopwatchTmplInfo { return getActiveStopwatch(ctx) })
|
||||
ctx.Data["PageGlobalData"] = data
|
||||
}
|
145
routers/common/qos.go
Normal file
145
routers/common/qos.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"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"
|
||||
giteacontext "code.gitea.io/gitea/services/context"
|
||||
|
||||
"github.com/bohde/codel"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
const tplStatus503 templates.TplName = "status/503"
|
||||
|
||||
type Priority int
|
||||
|
||||
func (p Priority) String() string {
|
||||
switch p {
|
||||
case HighPriority:
|
||||
return "high"
|
||||
case DefaultPriority:
|
||||
return "default"
|
||||
case LowPriority:
|
||||
return "low"
|
||||
default:
|
||||
return fmt.Sprintf("%d", p)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
LowPriority = Priority(-10)
|
||||
DefaultPriority = Priority(0)
|
||||
HighPriority = Priority(10)
|
||||
)
|
||||
|
||||
// QoS implements quality of service for requests, based upon whether
|
||||
// or not the user is logged in. All traffic may get dropped, and
|
||||
// anonymous users are deprioritized.
|
||||
func QoS() func(next http.Handler) http.Handler {
|
||||
if !setting.Service.QoS.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
maxOutstanding := setting.Service.QoS.MaxInFlightRequests
|
||||
if maxOutstanding <= 0 {
|
||||
maxOutstanding = 10
|
||||
}
|
||||
|
||||
c := codel.NewPriority(codel.Options{
|
||||
// The maximum number of waiting requests.
|
||||
MaxPending: setting.Service.QoS.MaxWaitingRequests,
|
||||
// The maximum number of in-flight requests.
|
||||
MaxOutstanding: maxOutstanding,
|
||||
// The target latency that a blocked request should wait
|
||||
// for. After this, it might be dropped.
|
||||
TargetLatency: setting.Service.QoS.TargetWaitTime,
|
||||
})
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
|
||||
priority := requestPriority(ctx)
|
||||
|
||||
// Check if the request can begin processing.
|
||||
err := c.Acquire(ctx, int(priority))
|
||||
if err != nil {
|
||||
log.Error("QoS error, dropping request of priority %s: %v", priority, err)
|
||||
renderServiceUnavailable(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Release long-polling immediately, so they don't always
|
||||
// take up an in-flight request
|
||||
if strings.Contains(req.URL.Path, "/user/events") {
|
||||
c.Release()
|
||||
} else {
|
||||
defer c.Release()
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// requestPriority assigns a priority value for a request based upon
|
||||
// whether the user is logged in and how expensive the endpoint is
|
||||
func requestPriority(ctx context.Context) Priority {
|
||||
// If the user is logged in, assign high priority.
|
||||
data := middleware.GetContextData(ctx)
|
||||
if _, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
|
||||
return HighPriority
|
||||
}
|
||||
|
||||
rctx := chi.RouteContext(ctx)
|
||||
if rctx == nil {
|
||||
return DefaultPriority
|
||||
}
|
||||
|
||||
// If we're operating in the context of a repo, assign low priority
|
||||
routePattern := rctx.RoutePattern()
|
||||
if strings.HasPrefix(routePattern, "/{username}/{reponame}/") {
|
||||
return LowPriority
|
||||
}
|
||||
|
||||
return DefaultPriority
|
||||
}
|
||||
|
||||
// renderServiceUnavailable will render an HTTP 503 Service
|
||||
// Unavailable page, providing HTML if the client accepts it.
|
||||
func renderServiceUnavailable(w http.ResponseWriter, req *http.Request) {
|
||||
acceptsHTML := false
|
||||
for _, part := range req.Header["Accept"] {
|
||||
if strings.Contains(part, "text/html") {
|
||||
acceptsHTML = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the client doesn't accept HTML, then render a plain text response
|
||||
if !acceptsHTML {
|
||||
http.Error(w, "503 Service Unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
tmplCtx := giteacontext.TemplateContext{}
|
||||
tmplCtx["Locale"] = middleware.Locale(w, req)
|
||||
ctxData := middleware.GetContextData(req.Context())
|
||||
err := templates.HTMLRenderer().HTML(w, http.StatusServiceUnavailable, tplStatus503, ctxData, tmplCtx)
|
||||
if err != nil {
|
||||
log.Error("Error occurs again when rendering service unavailable page: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("Internal server error, please collect error logs and report to Gitea issue tracker"))
|
||||
}
|
||||
}
|
91
routers/common/qos_test.go
Normal file
91
routers/common/qos_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRequestPriority(t *testing.T) {
|
||||
type test struct {
|
||||
Name string
|
||||
User *user_model.User
|
||||
RoutePattern string
|
||||
Expected Priority
|
||||
}
|
||||
|
||||
cases := []test{
|
||||
{
|
||||
Name: "Logged In",
|
||||
User: &user_model.User{},
|
||||
Expected: HighPriority,
|
||||
},
|
||||
{
|
||||
Name: "Sign In",
|
||||
RoutePattern: "/user/login",
|
||||
Expected: DefaultPriority,
|
||||
},
|
||||
{
|
||||
Name: "Repo Home",
|
||||
RoutePattern: "/{username}/{reponame}",
|
||||
Expected: DefaultPriority,
|
||||
},
|
||||
{
|
||||
Name: "User Repo",
|
||||
RoutePattern: "/{username}/{reponame}/src/branch/main",
|
||||
Expected: LowPriority,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockContext(t, "")
|
||||
|
||||
if tc.User != nil {
|
||||
data := middleware.GetContextData(ctx)
|
||||
data[middleware.ContextDataKeySignedUser] = tc.User
|
||||
}
|
||||
|
||||
rctx := chi.RouteContext(ctx)
|
||||
rctx.RoutePatterns = []string{tc.RoutePattern}
|
||||
|
||||
assert.Exactly(t, tc.Expected, requestPriority(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderServiceUnavailable(t *testing.T) {
|
||||
t.Run("HTML", func(t *testing.T) {
|
||||
ctx, resp := contexttest.MockContext(t, "")
|
||||
ctx.Req.Header.Set("Accept", "text/html")
|
||||
|
||||
renderServiceUnavailable(resp, ctx.Req)
|
||||
assert.Equal(t, http.StatusServiceUnavailable, resp.Code)
|
||||
assert.Contains(t, resp.Header().Get("Content-Type"), "text/html")
|
||||
|
||||
body := resp.Body.String()
|
||||
assert.Contains(t, body, `lang="en-US"`)
|
||||
assert.Contains(t, body, "503 Service Unavailable")
|
||||
})
|
||||
|
||||
t.Run("plain", func(t *testing.T) {
|
||||
ctx, resp := contexttest.MockContext(t, "")
|
||||
ctx.Req.Header.Set("Accept", "text/plain")
|
||||
|
||||
renderServiceUnavailable(resp, ctx.Req)
|
||||
assert.Equal(t, http.StatusServiceUnavailable, resp.Code)
|
||||
assert.Contains(t, resp.Header().Get("Content-Type"), "text/plain")
|
||||
|
||||
body := resp.Body.String()
|
||||
assert.Contains(t, body, "503 Service Unavailable")
|
||||
})
|
||||
}
|
26
routers/common/redirect.go
Normal file
26
routers/common/redirect.go
Normal file
@@ -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.IsCurrentGiteaSiteURL(req.Context(), redirect) {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
resp.Header().Add("Location", redirect)
|
||||
resp.WriteHeader(http.StatusSeeOther)
|
||||
}
|
52
routers/common/serve.go
Normal file
52
routers/common/serve.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"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/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// ServeBlob download a git.Blob
|
||||
func ServeBlob(ctx *context.Base, repo *repo_model.Repository, 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)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = repo.LoadOwner(ctx)
|
||||
httplib.ServeContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, &httplib.ServeHeaderOptions{
|
||||
Filename: path.Base(filePath),
|
||||
CacheIsPublic: !repo.IsPrivate && repo.Owner != nil && repo.Owner.Visibility == structs.VisibleTypePublic,
|
||||
CacheDuration: setting.StaticCacheTime,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func ServeContentByReader(ctx *context.Base, filePath string, size int64, reader io.Reader) {
|
||||
httplib.ServeContentByReader(ctx.Req, ctx.Resp, size, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)})
|
||||
}
|
||||
|
||||
func ServeContentByReadSeeker(ctx *context.Base, filePath string, modTime *time.Time, reader io.ReadSeeker) {
|
||||
httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, modTime, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)})
|
||||
}
|
Reference in New Issue
Block a user