forked from NYANDEV/forgejo
9413fd0274
(cherry picked from commit 7b0549cd70aa7cafec853e15b25270847c59850b) (cherry picked from commit 13e10a65d974c7b594681bfa36402a6144862116) (cherry picked from commit 65bdd73cf27895a9fb8db2a95ef4f5b08951481d) (cherry picked from commit 64eba8bb923176b4c286b1d0c83792f3c3005ca8) (cherry picked from commit 4c49b1a759abe3604afc1121e83c9a942016ad6a) (cherry picked from commit 93b4d0640683ea986657453b1fce49a00c861764) (cherry picked from commit e2bc5f36d958f4349160ec145719c302d4023cd0) (cherry picked from commit 2bee76f9dfa998c83ea4fe648997fad0b6224fa9) (cherry picked from commit 3d8a1b4a9fb9dc55bbd62fd8855ea85e58dc263f) (cherry picked from commit 99dd092cd02d7af8374acf454833ce1c05fd4fd9) (cherry picked from commit 0fdbd02204d533f907cd22c83c73bf0156ec4a88) (cherry picked from commit 70b277a183c0d85966fa84e9b054f164ae2d2a44) (cherry picked from commit 3eece7fbb4e67d970d8979d0d60a58ee2a195ea5) (cherry picked from commit 4838fc9e1145a74c56926de68854234604b5e38f) (cherry picked from commit b76ed541cf4d73702a83d6b96f8618b6f8c44393) (cherry picked from commit b1141cb3a1a0c602020ddff2021448a056cb3232) (cherry picked from commit 63f4f8a1cefe10ddc4a9a589e9ae2087b78ec3c6) (cherry picked from commit 67245f5b440c20bbd7fc039747f6b386cee3fbed) (cherry picked from commit 19f1633fa3dcf14275fd7fcb3bb549e20ef8f688) (cherry picked from commit 51c9663b4a4150d8ea3bdf1ef1eb157ead30046c)
428 lines
12 KiB
Go
428 lines
12 KiB
Go
// Copyright 2016 The Gogs Authors. All rights reserved.
|
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package context
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/auth"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/models/unit"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
mc "code.gitea.io/gitea/modules/cache"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/httpcache"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
|
|
"gitea.com/go-chi/cache"
|
|
)
|
|
|
|
// APIContext is a specific context for API service
|
|
type APIContext struct {
|
|
*Base
|
|
|
|
Cache cache.Cache
|
|
|
|
Doer *user_model.User // current signed-in user
|
|
IsSigned bool
|
|
IsBasicAuth bool
|
|
|
|
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
|
|
|
|
Repo *Repository
|
|
Org *APIOrganization
|
|
Package *Package
|
|
}
|
|
|
|
// Currently, we have the following common fields in error response:
|
|
// * message: the message for end users (it shouldn't be used for error type detection)
|
|
// if we need to indicate some errors, we should introduce some new fields like ErrorCode or ErrorType
|
|
// * url: the swagger document URL
|
|
|
|
// APIError is error format response
|
|
// swagger:response error
|
|
type APIError struct {
|
|
Message string `json:"message"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// APIValidationError is error format response related to input validation
|
|
// swagger:response validationError
|
|
type APIValidationError struct {
|
|
Message string `json:"message"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// APIInvalidTopicsError is error format response to invalid topics
|
|
// swagger:response invalidTopicsError
|
|
type APIInvalidTopicsError struct {
|
|
Message string `json:"message"`
|
|
InvalidTopics []string `json:"invalidTopics"`
|
|
}
|
|
|
|
// APIEmpty is an empty response
|
|
// swagger:response empty
|
|
type APIEmpty struct{}
|
|
|
|
// APIForbiddenError is a forbidden error response
|
|
// swagger:response forbidden
|
|
type APIForbiddenError struct {
|
|
APIError
|
|
}
|
|
|
|
// APINotFound is a not found empty response
|
|
// swagger:response notFound
|
|
type APINotFound struct{}
|
|
|
|
// APIConflict is a conflict empty response
|
|
// swagger:response conflict
|
|
type APIConflict struct{}
|
|
|
|
// APIRedirect is a redirect response
|
|
// swagger:response redirect
|
|
type APIRedirect struct{}
|
|
|
|
// APIString is a string response
|
|
// swagger:response string
|
|
type APIString string
|
|
|
|
// ServerError responds with error message, status is 500
|
|
func (ctx *APIContext) ServerError(title string, err error) {
|
|
ctx.Error(http.StatusInternalServerError, title, err)
|
|
}
|
|
|
|
// Error responds with an error message to client with given obj as the message.
|
|
// If status is 500, also it prints error to log.
|
|
func (ctx *APIContext) Error(status int, title string, obj any) {
|
|
var message string
|
|
if err, ok := obj.(error); ok {
|
|
message = err.Error()
|
|
} else {
|
|
message = fmt.Sprintf("%s", obj)
|
|
}
|
|
|
|
if status == http.StatusInternalServerError {
|
|
log.ErrorWithSkip(1, "%s: %s", title, message)
|
|
|
|
if setting.IsProd && !(ctx.Doer != nil && ctx.Doer.IsAdmin) {
|
|
message = ""
|
|
}
|
|
}
|
|
|
|
ctx.JSON(status, APIError{
|
|
Message: message,
|
|
URL: setting.API.SwaggerURL,
|
|
})
|
|
}
|
|
|
|
// InternalServerError responds with an error message to the client with the error as a message
|
|
// and the file and line of the caller.
|
|
func (ctx *APIContext) InternalServerError(err error) {
|
|
log.ErrorWithSkip(1, "InternalServerError: %v", err)
|
|
|
|
var message string
|
|
if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
|
|
message = err.Error()
|
|
}
|
|
|
|
ctx.JSON(http.StatusInternalServerError, APIError{
|
|
Message: message,
|
|
URL: setting.API.SwaggerURL,
|
|
})
|
|
}
|
|
|
|
type apiContextKeyType struct{}
|
|
|
|
var apiContextKey = apiContextKeyType{}
|
|
|
|
// GetAPIContext returns a context for API routes
|
|
func GetAPIContext(req *http.Request) *APIContext {
|
|
return req.Context().Value(apiContextKey).(*APIContext)
|
|
}
|
|
|
|
func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string {
|
|
page := NewPagination(total, pageSize, curPage, 0)
|
|
paginater := page.Paginater
|
|
links := make([]string, 0, 4)
|
|
|
|
if paginater.HasNext() {
|
|
u := *curURL
|
|
queries := u.Query()
|
|
queries.Set("page", fmt.Sprintf("%d", paginater.Next()))
|
|
u.RawQuery = queries.Encode()
|
|
|
|
links = append(links, fmt.Sprintf("<%s%s>; rel=\"next\"", setting.AppURL, u.RequestURI()[1:]))
|
|
}
|
|
if !paginater.IsLast() {
|
|
u := *curURL
|
|
queries := u.Query()
|
|
queries.Set("page", fmt.Sprintf("%d", paginater.TotalPages()))
|
|
u.RawQuery = queries.Encode()
|
|
|
|
links = append(links, fmt.Sprintf("<%s%s>; rel=\"last\"", setting.AppURL, u.RequestURI()[1:]))
|
|
}
|
|
if !paginater.IsFirst() {
|
|
u := *curURL
|
|
queries := u.Query()
|
|
queries.Set("page", "1")
|
|
u.RawQuery = queries.Encode()
|
|
|
|
links = append(links, fmt.Sprintf("<%s%s>; rel=\"first\"", setting.AppURL, u.RequestURI()[1:]))
|
|
}
|
|
if paginater.HasPrevious() {
|
|
u := *curURL
|
|
queries := u.Query()
|
|
queries.Set("page", fmt.Sprintf("%d", paginater.Previous()))
|
|
u.RawQuery = queries.Encode()
|
|
|
|
links = append(links, fmt.Sprintf("<%s%s>; rel=\"prev\"", setting.AppURL, u.RequestURI()[1:]))
|
|
}
|
|
return links
|
|
}
|
|
|
|
// SetLinkHeader sets pagination link header by given total number and page size.
|
|
func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
|
|
links := genAPILinks(ctx.Req.URL, total, pageSize, ctx.FormInt("page"))
|
|
|
|
if len(links) > 0 {
|
|
ctx.RespHeader().Set("Link", strings.Join(links, ","))
|
|
ctx.AppendAccessControlExposeHeaders("Link")
|
|
}
|
|
}
|
|
|
|
func getOtpHeader(header http.Header) string {
|
|
otpHeader := header.Get("X-Gitea-OTP")
|
|
if forgejoHeader := header.Get("X-Forgejo-OTP"); forgejoHeader != "" {
|
|
otpHeader = forgejoHeader
|
|
}
|
|
return otpHeader
|
|
}
|
|
|
|
// CheckForOTP validates OTP
|
|
func (ctx *APIContext) CheckForOTP() {
|
|
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
|
|
return // Skip 2FA
|
|
}
|
|
|
|
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
|
|
if err != nil {
|
|
if auth.IsErrTwoFactorNotEnrolled(err) {
|
|
return // No 2FA enrollment for this user
|
|
}
|
|
ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err)
|
|
return
|
|
}
|
|
ok, err := twofa.ValidateTOTP(getOtpHeader(ctx.Req.Header))
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
|
|
return
|
|
}
|
|
if !ok {
|
|
ctx.Error(http.StatusUnauthorized, "", nil)
|
|
return
|
|
}
|
|
}
|
|
|
|
// APIContexter returns apicontext as middleware
|
|
func APIContexter() func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
base, baseCleanUp := NewBaseContext(w, req)
|
|
ctx := &APIContext{
|
|
Base: base,
|
|
Cache: mc.GetCache(),
|
|
Repo: &Repository{PullRequest: &PullRequest{}},
|
|
Org: &APIOrganization{},
|
|
}
|
|
defer baseCleanUp()
|
|
|
|
ctx.Base.AppendContextValue(apiContextKey, ctx)
|
|
ctx.Base.AppendContextValueFunc(git.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
|
|
|
|
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
|
|
if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
|
if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
|
|
ctx.InternalServerError(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
|
|
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
|
|
|
next.ServeHTTP(ctx.Resp, ctx.Req)
|
|
})
|
|
}
|
|
}
|
|
|
|
// NotFound handles 404s for APIContext
|
|
// String will replace message, errors will be added to a slice
|
|
func (ctx *APIContext) NotFound(objs ...any) {
|
|
message := ctx.Tr("error.not_found")
|
|
var errors []string
|
|
for _, obj := range objs {
|
|
// Ignore nil
|
|
if obj == nil {
|
|
continue
|
|
}
|
|
|
|
if err, ok := obj.(error); ok {
|
|
errors = append(errors, err.Error())
|
|
} else {
|
|
message = obj.(string)
|
|
}
|
|
}
|
|
|
|
ctx.JSON(http.StatusNotFound, map[string]any{
|
|
"message": message,
|
|
"url": setting.API.SwaggerURL,
|
|
"errors": errors,
|
|
})
|
|
}
|
|
|
|
// ReferencesGitRepo injects the GitRepo into the Context
|
|
// you can optional skip the IsEmpty check
|
|
func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context.CancelFunc) {
|
|
return func(ctx *APIContext) (cancel context.CancelFunc) {
|
|
// Empty repository does not have reference information.
|
|
if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) {
|
|
return
|
|
}
|
|
|
|
// For API calls.
|
|
if ctx.Repo.GitRepo == nil {
|
|
repoPath := repo_model.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
|
|
gitRepo, err := git.OpenRepository(ctx, repoPath)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "RepoRef Invalid repo "+repoPath, err)
|
|
return
|
|
}
|
|
ctx.Repo.GitRepo = gitRepo
|
|
// We opened it, we should close it
|
|
return func() {
|
|
// If it's been set to nil then assume someone else has closed it.
|
|
if ctx.Repo.GitRepo != nil {
|
|
_ = ctx.Repo.GitRepo.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
return cancel
|
|
}
|
|
}
|
|
|
|
// RepoRefForAPI handles repository reference names when the ref name is not explicitly given
|
|
func RepoRefForAPI(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
ctx := GetAPIContext(req)
|
|
|
|
if ctx.Repo.GitRepo == nil {
|
|
ctx.InternalServerError(fmt.Errorf("no open git repo"))
|
|
return
|
|
}
|
|
|
|
if ref := ctx.FormTrim("ref"); len(ref) > 0 {
|
|
commit, err := ctx.Repo.GitRepo.GetCommit(ref)
|
|
if err != nil {
|
|
if git.IsErrNotExist(err) {
|
|
ctx.NotFound()
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
|
|
}
|
|
return
|
|
}
|
|
ctx.Repo.Commit = commit
|
|
ctx.Repo.TreePath = ctx.Params("*")
|
|
next.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
var err error
|
|
refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
|
|
|
|
if ctx.Repo.GitRepo.IsBranchExist(refName) {
|
|
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
|
|
if err != nil {
|
|
ctx.InternalServerError(err)
|
|
return
|
|
}
|
|
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
|
} else if ctx.Repo.GitRepo.IsTagExist(refName) {
|
|
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refName)
|
|
if err != nil {
|
|
ctx.InternalServerError(err)
|
|
return
|
|
}
|
|
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
|
} else if len(refName) == git.SHAFullLength {
|
|
ctx.Repo.CommitID = refName
|
|
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
|
|
if err != nil {
|
|
ctx.NotFound("GetCommit", err)
|
|
return
|
|
}
|
|
} else {
|
|
ctx.NotFound(fmt.Errorf("not exist: '%s'", ctx.Params("*")))
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
|
|
// HasAPIError returns true if error occurs in form validation.
|
|
func (ctx *APIContext) HasAPIError() bool {
|
|
hasErr, ok := ctx.Data["HasError"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
return hasErr.(bool)
|
|
}
|
|
|
|
// GetErrMsg returns error message in form validation.
|
|
func (ctx *APIContext) GetErrMsg() string {
|
|
msg, _ := ctx.Data["ErrorMsg"].(string)
|
|
if msg == "" {
|
|
msg = "invalid form data"
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// NotFoundOrServerError use error check function to determine if the error
|
|
// is about not found. It responds with 404 status code for not found error,
|
|
// or error context description for logging purpose of 500 server error.
|
|
func (ctx *APIContext) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
|
|
if errCheck(logErr) {
|
|
ctx.JSON(http.StatusNotFound, nil)
|
|
return
|
|
}
|
|
ctx.Error(http.StatusInternalServerError, "NotFoundOrServerError", logMsg)
|
|
}
|
|
|
|
// IsUserSiteAdmin returns true if current user is a site admin
|
|
func (ctx *APIContext) IsUserSiteAdmin() bool {
|
|
return ctx.IsSigned && ctx.Doer.IsAdmin
|
|
}
|
|
|
|
// IsUserRepoAdmin returns true if current user is admin in current repo
|
|
func (ctx *APIContext) IsUserRepoAdmin() bool {
|
|
return ctx.Repo.IsAdmin()
|
|
}
|
|
|
|
// IsUserRepoWriter returns true if current user has write privilege in current repo
|
|
func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool {
|
|
for _, unitType := range unitTypes {
|
|
if ctx.Repo.CanWrite(unitType) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|