code.gitea.io/gitea@v1.19.3/modules/context/api.go (about)

     1  // Copyright 2016 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package context
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/models/auth"
    15  	repo_model "code.gitea.io/gitea/models/repo"
    16  	"code.gitea.io/gitea/modules/cache"
    17  	"code.gitea.io/gitea/modules/git"
    18  	"code.gitea.io/gitea/modules/httpcache"
    19  	"code.gitea.io/gitea/modules/log"
    20  	"code.gitea.io/gitea/modules/setting"
    21  	"code.gitea.io/gitea/modules/web/middleware"
    22  )
    23  
    24  // APIContext is a specific context for API service
    25  type APIContext struct {
    26  	*Context
    27  	Org *APIOrganization
    28  }
    29  
    30  // Currently, we have the following common fields in error response:
    31  // * message: the message for end users (it shouldn't be used for error type detection)
    32  //            if we need to indicate some errors, we should introduce some new fields like ErrorCode or ErrorType
    33  // * url:     the swagger document URL
    34  
    35  // APIError is error format response
    36  // swagger:response error
    37  type APIError struct {
    38  	Message string `json:"message"`
    39  	URL     string `json:"url"`
    40  }
    41  
    42  // APIValidationError is error format response related to input validation
    43  // swagger:response validationError
    44  type APIValidationError struct {
    45  	Message string `json:"message"`
    46  	URL     string `json:"url"`
    47  }
    48  
    49  // APIInvalidTopicsError is error format response to invalid topics
    50  // swagger:response invalidTopicsError
    51  type APIInvalidTopicsError struct {
    52  	Message       string   `json:"message"`
    53  	InvalidTopics []string `json:"invalidTopics"`
    54  }
    55  
    56  // APIEmpty is an empty response
    57  // swagger:response empty
    58  type APIEmpty struct{}
    59  
    60  // APIForbiddenError is a forbidden error response
    61  // swagger:response forbidden
    62  type APIForbiddenError struct {
    63  	APIError
    64  }
    65  
    66  // APINotFound is a not found empty response
    67  // swagger:response notFound
    68  type APINotFound struct{}
    69  
    70  // APIConflict is a conflict empty response
    71  // swagger:response conflict
    72  type APIConflict struct{}
    73  
    74  // APIRedirect is a redirect response
    75  // swagger:response redirect
    76  type APIRedirect struct{}
    77  
    78  // APIString is a string response
    79  // swagger:response string
    80  type APIString string
    81  
    82  // ServerError responds with error message, status is 500
    83  func (ctx *APIContext) ServerError(title string, err error) {
    84  	ctx.Error(http.StatusInternalServerError, title, err)
    85  }
    86  
    87  // Error responds with an error message to client with given obj as the message.
    88  // If status is 500, also it prints error to log.
    89  func (ctx *APIContext) Error(status int, title string, obj interface{}) {
    90  	var message string
    91  	if err, ok := obj.(error); ok {
    92  		message = err.Error()
    93  	} else {
    94  		message = fmt.Sprintf("%s", obj)
    95  	}
    96  
    97  	if status == http.StatusInternalServerError {
    98  		log.ErrorWithSkip(1, "%s: %s", title, message)
    99  
   100  		if setting.IsProd && !(ctx.Doer != nil && ctx.Doer.IsAdmin) {
   101  			message = ""
   102  		}
   103  	}
   104  
   105  	ctx.JSON(status, APIError{
   106  		Message: message,
   107  		URL:     setting.API.SwaggerURL,
   108  	})
   109  }
   110  
   111  // InternalServerError responds with an error message to the client with the error as a message
   112  // and the file and line of the caller.
   113  func (ctx *APIContext) InternalServerError(err error) {
   114  	log.ErrorWithSkip(1, "InternalServerError: %v", err)
   115  
   116  	var message string
   117  	if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
   118  		message = err.Error()
   119  	}
   120  
   121  	ctx.JSON(http.StatusInternalServerError, APIError{
   122  		Message: message,
   123  		URL:     setting.API.SwaggerURL,
   124  	})
   125  }
   126  
   127  type apiContextKeyType struct{}
   128  
   129  var apiContextKey = apiContextKeyType{}
   130  
   131  // WithAPIContext set up api context in request
   132  func WithAPIContext(req *http.Request, ctx *APIContext) *http.Request {
   133  	return req.WithContext(context.WithValue(req.Context(), apiContextKey, ctx))
   134  }
   135  
   136  // GetAPIContext returns a context for API routes
   137  func GetAPIContext(req *http.Request) *APIContext {
   138  	return req.Context().Value(apiContextKey).(*APIContext)
   139  }
   140  
   141  func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string {
   142  	page := NewPagination(total, pageSize, curPage, 0)
   143  	paginater := page.Paginater
   144  	links := make([]string, 0, 4)
   145  
   146  	if paginater.HasNext() {
   147  		u := *curURL
   148  		queries := u.Query()
   149  		queries.Set("page", fmt.Sprintf("%d", paginater.Next()))
   150  		u.RawQuery = queries.Encode()
   151  
   152  		links = append(links, fmt.Sprintf("<%s%s>; rel=\"next\"", setting.AppURL, u.RequestURI()[1:]))
   153  	}
   154  	if !paginater.IsLast() {
   155  		u := *curURL
   156  		queries := u.Query()
   157  		queries.Set("page", fmt.Sprintf("%d", paginater.TotalPages()))
   158  		u.RawQuery = queries.Encode()
   159  
   160  		links = append(links, fmt.Sprintf("<%s%s>; rel=\"last\"", setting.AppURL, u.RequestURI()[1:]))
   161  	}
   162  	if !paginater.IsFirst() {
   163  		u := *curURL
   164  		queries := u.Query()
   165  		queries.Set("page", "1")
   166  		u.RawQuery = queries.Encode()
   167  
   168  		links = append(links, fmt.Sprintf("<%s%s>; rel=\"first\"", setting.AppURL, u.RequestURI()[1:]))
   169  	}
   170  	if paginater.HasPrevious() {
   171  		u := *curURL
   172  		queries := u.Query()
   173  		queries.Set("page", fmt.Sprintf("%d", paginater.Previous()))
   174  		u.RawQuery = queries.Encode()
   175  
   176  		links = append(links, fmt.Sprintf("<%s%s>; rel=\"prev\"", setting.AppURL, u.RequestURI()[1:]))
   177  	}
   178  	return links
   179  }
   180  
   181  // SetLinkHeader sets pagination link header by given total number and page size.
   182  func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
   183  	links := genAPILinks(ctx.Req.URL, total, pageSize, ctx.FormInt("page"))
   184  
   185  	if len(links) > 0 {
   186  		ctx.RespHeader().Set("Link", strings.Join(links, ","))
   187  		ctx.AppendAccessControlExposeHeaders("Link")
   188  	}
   189  }
   190  
   191  // CheckForOTP validates OTP
   192  func (ctx *APIContext) CheckForOTP() {
   193  	if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
   194  		return // Skip 2FA
   195  	}
   196  
   197  	otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
   198  	twofa, err := auth.GetTwoFactorByUID(ctx.Context.Doer.ID)
   199  	if err != nil {
   200  		if auth.IsErrTwoFactorNotEnrolled(err) {
   201  			return // No 2FA enrollment for this user
   202  		}
   203  		ctx.Context.Error(http.StatusInternalServerError)
   204  		return
   205  	}
   206  	ok, err := twofa.ValidateTOTP(otpHeader)
   207  	if err != nil {
   208  		ctx.Context.Error(http.StatusInternalServerError)
   209  		return
   210  	}
   211  	if !ok {
   212  		ctx.Context.Error(http.StatusUnauthorized)
   213  		return
   214  	}
   215  }
   216  
   217  // APIContexter returns apicontext as middleware
   218  func APIContexter() func(http.Handler) http.Handler {
   219  	return func(next http.Handler) http.Handler {
   220  		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   221  			locale := middleware.Locale(w, req)
   222  			ctx := APIContext{
   223  				Context: &Context{
   224  					Resp:   NewResponse(w),
   225  					Data:   map[string]interface{}{},
   226  					Locale: locale,
   227  					Cache:  cache.GetCache(),
   228  					Repo: &Repository{
   229  						PullRequest: &PullRequest{},
   230  					},
   231  					Org: &Organization{},
   232  				},
   233  				Org: &APIOrganization{},
   234  			}
   235  			defer ctx.Close()
   236  
   237  			ctx.Req = WithAPIContext(WithContext(req, ctx.Context), &ctx)
   238  
   239  			// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
   240  			if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
   241  				if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
   242  					ctx.InternalServerError(err)
   243  					return
   244  				}
   245  			}
   246  
   247  			httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
   248  			ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
   249  
   250  			ctx.Data["Context"] = &ctx
   251  
   252  			next.ServeHTTP(ctx.Resp, ctx.Req)
   253  
   254  			// Handle adding signedUserName to the context for the AccessLogger
   255  			usernameInterface := ctx.Data["SignedUserName"]
   256  			identityPtrInterface := ctx.Req.Context().Value(signedUserNameStringPointerKey)
   257  			if usernameInterface != nil && identityPtrInterface != nil {
   258  				username := usernameInterface.(string)
   259  				identityPtr := identityPtrInterface.(*string)
   260  				if identityPtr != nil && username != "" {
   261  					*identityPtr = username
   262  				}
   263  			}
   264  		})
   265  	}
   266  }
   267  
   268  // NotFound handles 404s for APIContext
   269  // String will replace message, errors will be added to a slice
   270  func (ctx *APIContext) NotFound(objs ...interface{}) {
   271  	message := ctx.Tr("error.not_found")
   272  	var errors []string
   273  	for _, obj := range objs {
   274  		// Ignore nil
   275  		if obj == nil {
   276  			continue
   277  		}
   278  
   279  		if err, ok := obj.(error); ok {
   280  			errors = append(errors, err.Error())
   281  		} else {
   282  			message = obj.(string)
   283  		}
   284  	}
   285  
   286  	ctx.JSON(http.StatusNotFound, map[string]interface{}{
   287  		"message": message,
   288  		"url":     setting.API.SwaggerURL,
   289  		"errors":  errors,
   290  	})
   291  }
   292  
   293  // ReferencesGitRepo injects the GitRepo into the Context
   294  // you can optional skip the IsEmpty check
   295  func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context.CancelFunc) {
   296  	return func(ctx *APIContext) (cancel context.CancelFunc) {
   297  		// Empty repository does not have reference information.
   298  		if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) {
   299  			return
   300  		}
   301  
   302  		// For API calls.
   303  		if ctx.Repo.GitRepo == nil {
   304  			repoPath := repo_model.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
   305  			gitRepo, err := git.OpenRepository(ctx, repoPath)
   306  			if err != nil {
   307  				ctx.Error(http.StatusInternalServerError, "RepoRef Invalid repo "+repoPath, err)
   308  				return
   309  			}
   310  			ctx.Repo.GitRepo = gitRepo
   311  			// We opened it, we should close it
   312  			return func() {
   313  				// If it's been set to nil then assume someone else has closed it.
   314  				if ctx.Repo.GitRepo != nil {
   315  					ctx.Repo.GitRepo.Close()
   316  				}
   317  			}
   318  		}
   319  
   320  		return cancel
   321  	}
   322  }
   323  
   324  // RepoRefForAPI handles repository reference names when the ref name is not explicitly given
   325  func RepoRefForAPI(next http.Handler) http.Handler {
   326  	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   327  		ctx := GetAPIContext(req)
   328  
   329  		if ctx.Repo.GitRepo == nil {
   330  			ctx.InternalServerError(fmt.Errorf("no open git repo"))
   331  			return
   332  		}
   333  
   334  		if ref := ctx.FormTrim("ref"); len(ref) > 0 {
   335  			commit, err := ctx.Repo.GitRepo.GetCommit(ref)
   336  			if err != nil {
   337  				if git.IsErrNotExist(err) {
   338  					ctx.NotFound()
   339  				} else {
   340  					ctx.Error(http.StatusInternalServerError, "GetCommit", err)
   341  				}
   342  				return
   343  			}
   344  			ctx.Repo.Commit = commit
   345  			ctx.Repo.TreePath = ctx.Params("*")
   346  			return
   347  		}
   348  
   349  		var err error
   350  		refName := getRefName(ctx.Context, RepoRefAny)
   351  
   352  		if ctx.Repo.GitRepo.IsBranchExist(refName) {
   353  			ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
   354  			if err != nil {
   355  				ctx.InternalServerError(err)
   356  				return
   357  			}
   358  			ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
   359  		} else if ctx.Repo.GitRepo.IsTagExist(refName) {
   360  			ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refName)
   361  			if err != nil {
   362  				ctx.InternalServerError(err)
   363  				return
   364  			}
   365  			ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
   366  		} else if len(refName) == git.SHAFullLength {
   367  			ctx.Repo.CommitID = refName
   368  			ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
   369  			if err != nil {
   370  				ctx.NotFound("GetCommit", err)
   371  				return
   372  			}
   373  		} else {
   374  			ctx.NotFound(fmt.Errorf("not exist: '%s'", ctx.Params("*")))
   375  			return
   376  		}
   377  
   378  		next.ServeHTTP(w, req)
   379  	})
   380  }