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 }