code.gitea.io/gitea@v1.21.7/services/lfs/server.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package lfs
     5  
     6  import (
     7  	stdCtx "context"
     8  	"encoding/base64"
     9  	"encoding/hex"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"net/http"
    14  	"net/url"
    15  	"path"
    16  	"regexp"
    17  	"strconv"
    18  	"strings"
    19  
    20  	actions_model "code.gitea.io/gitea/models/actions"
    21  	auth_model "code.gitea.io/gitea/models/auth"
    22  	git_model "code.gitea.io/gitea/models/git"
    23  	"code.gitea.io/gitea/models/perm"
    24  	access_model "code.gitea.io/gitea/models/perm/access"
    25  	repo_model "code.gitea.io/gitea/models/repo"
    26  	"code.gitea.io/gitea/models/unit"
    27  	user_model "code.gitea.io/gitea/models/user"
    28  	"code.gitea.io/gitea/modules/context"
    29  	"code.gitea.io/gitea/modules/json"
    30  	lfs_module "code.gitea.io/gitea/modules/lfs"
    31  	"code.gitea.io/gitea/modules/log"
    32  	"code.gitea.io/gitea/modules/setting"
    33  	"code.gitea.io/gitea/modules/storage"
    34  
    35  	"github.com/golang-jwt/jwt/v5"
    36  	"github.com/minio/sha256-simd"
    37  )
    38  
    39  // requestContext contain variables from the HTTP request.
    40  type requestContext struct {
    41  	User          string
    42  	Repo          string
    43  	Authorization string
    44  }
    45  
    46  // Claims is a JWT Token Claims
    47  type Claims struct {
    48  	RepoID int64
    49  	Op     string
    50  	UserID int64
    51  	jwt.RegisteredClaims
    52  }
    53  
    54  // DownloadLink builds a URL to download the object.
    55  func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
    56  	return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid))
    57  }
    58  
    59  // UploadLink builds a URL to upload the object.
    60  func (rc *requestContext) UploadLink(p lfs_module.Pointer) string {
    61  	return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid), strconv.FormatInt(p.Size, 10))
    62  }
    63  
    64  // VerifyLink builds a URL for verifying the object.
    65  func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string {
    66  	return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/verify")
    67  }
    68  
    69  // CheckAcceptMediaType checks if the client accepts the LFS media type.
    70  func CheckAcceptMediaType(ctx *context.Context) {
    71  	mediaParts := strings.Split(ctx.Req.Header.Get("Accept"), ";")
    72  
    73  	if mediaParts[0] != lfs_module.MediaType {
    74  		log.Trace("Calling a LFS method without accepting the correct media type: %s", lfs_module.MediaType)
    75  		writeStatus(ctx, http.StatusUnsupportedMediaType)
    76  		return
    77  	}
    78  }
    79  
    80  var rangeHeaderRegexp = regexp.MustCompile(`bytes=(\d+)\-(\d*).*`)
    81  
    82  // DownloadHandler gets the content from the content store
    83  func DownloadHandler(ctx *context.Context) {
    84  	rc := getRequestContext(ctx)
    85  	p := lfs_module.Pointer{Oid: ctx.Params("oid")}
    86  
    87  	meta := getAuthenticatedMeta(ctx, rc, p, false)
    88  	if meta == nil {
    89  		return
    90  	}
    91  
    92  	// Support resume download using Range header
    93  	var fromByte, toByte int64
    94  	toByte = meta.Size - 1
    95  	statusCode := http.StatusOK
    96  	if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" {
    97  		match := rangeHeaderRegexp.FindStringSubmatch(rangeHdr)
    98  		if len(match) > 1 {
    99  			statusCode = http.StatusPartialContent
   100  			fromByte, _ = strconv.ParseInt(match[1], 10, 32)
   101  
   102  			if fromByte >= meta.Size {
   103  				writeStatus(ctx, http.StatusRequestedRangeNotSatisfiable)
   104  				return
   105  			}
   106  
   107  			if match[2] != "" {
   108  				_toByte, _ := strconv.ParseInt(match[2], 10, 32)
   109  				if _toByte >= fromByte && _toByte < toByte {
   110  					toByte = _toByte
   111  				}
   112  			}
   113  
   114  			ctx.Resp.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", fromByte, toByte, meta.Size-fromByte))
   115  			ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Range")
   116  		}
   117  	}
   118  
   119  	contentStore := lfs_module.NewContentStore()
   120  	content, err := contentStore.Get(meta.Pointer)
   121  	if err != nil {
   122  		writeStatus(ctx, http.StatusNotFound)
   123  		return
   124  	}
   125  	defer content.Close()
   126  
   127  	if fromByte > 0 {
   128  		_, err = content.Seek(fromByte, io.SeekStart)
   129  		if err != nil {
   130  			log.Error("Whilst trying to read LFS OID[%s]: Unable to seek to %d Error: %v", meta.Oid, fromByte, err)
   131  			writeStatus(ctx, http.StatusInternalServerError)
   132  			return
   133  		}
   134  	}
   135  
   136  	contentLength := toByte + 1 - fromByte
   137  	ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10))
   138  	ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
   139  
   140  	filename := ctx.Params("filename")
   141  	if len(filename) > 0 {
   142  		decodedFilename, err := base64.RawURLEncoding.DecodeString(filename)
   143  		if err == nil {
   144  			ctx.Resp.Header().Set("Content-Disposition", "attachment; filename=\""+string(decodedFilename)+"\"")
   145  			ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
   146  		}
   147  	}
   148  
   149  	ctx.Resp.WriteHeader(statusCode)
   150  	if written, err := io.CopyN(ctx.Resp, content, contentLength); err != nil {
   151  		log.Error("Error whilst copying LFS OID[%s] to the response after %d bytes. Error: %v", meta.Oid, written, err)
   152  	}
   153  }
   154  
   155  // BatchHandler provides the batch api
   156  func BatchHandler(ctx *context.Context) {
   157  	var br lfs_module.BatchRequest
   158  	if err := decodeJSON(ctx.Req, &br); err != nil {
   159  		log.Trace("Unable to decode BATCH request vars: Error: %v", err)
   160  		writeStatus(ctx, http.StatusBadRequest)
   161  		return
   162  	}
   163  
   164  	var isUpload bool
   165  	if br.Operation == "upload" {
   166  		isUpload = true
   167  	} else if br.Operation == "download" {
   168  		isUpload = false
   169  	} else {
   170  		log.Trace("Attempt to BATCH with invalid operation: %s", br.Operation)
   171  		writeStatus(ctx, http.StatusBadRequest)
   172  		return
   173  	}
   174  
   175  	rc := getRequestContext(ctx)
   176  
   177  	repository := getAuthenticatedRepository(ctx, rc, isUpload)
   178  	if repository == nil {
   179  		return
   180  	}
   181  
   182  	contentStore := lfs_module.NewContentStore()
   183  
   184  	var responseObjects []*lfs_module.ObjectResponse
   185  
   186  	for _, p := range br.Objects {
   187  		if !p.IsValid() {
   188  			responseObjects = append(responseObjects, buildObjectResponse(rc, p, false, false, &lfs_module.ObjectError{
   189  				Code:    http.StatusUnprocessableEntity,
   190  				Message: "Oid or size are invalid",
   191  			}))
   192  			continue
   193  		}
   194  
   195  		exists, err := contentStore.Exists(p)
   196  		if err != nil {
   197  			log.Error("Unable to check if LFS OID[%s] exist. Error: %v", p.Oid, rc.User, rc.Repo, err)
   198  			writeStatus(ctx, http.StatusInternalServerError)
   199  			return
   200  		}
   201  
   202  		meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)
   203  		if err != nil && err != git_model.ErrLFSObjectNotExist {
   204  			log.Error("Unable to get LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
   205  			writeStatus(ctx, http.StatusInternalServerError)
   206  			return
   207  		}
   208  
   209  		if meta != nil && p.Size != meta.Size {
   210  			responseObjects = append(responseObjects, buildObjectResponse(rc, p, false, false, &lfs_module.ObjectError{
   211  				Code:    http.StatusUnprocessableEntity,
   212  				Message: fmt.Sprintf("Object %s is not %d bytes", p.Oid, p.Size),
   213  			}))
   214  			continue
   215  		}
   216  
   217  		var responseObject *lfs_module.ObjectResponse
   218  		if isUpload {
   219  			var err *lfs_module.ObjectError
   220  			if !exists && setting.LFS.MaxFileSize > 0 && p.Size > setting.LFS.MaxFileSize {
   221  				err = &lfs_module.ObjectError{
   222  					Code:    http.StatusUnprocessableEntity,
   223  					Message: fmt.Sprintf("Size must be less than or equal to %d", setting.LFS.MaxFileSize),
   224  				}
   225  			}
   226  
   227  			if exists && meta == nil {
   228  				accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
   229  				if err != nil {
   230  					log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err)
   231  					writeStatus(ctx, http.StatusInternalServerError)
   232  					return
   233  				}
   234  				if accessible {
   235  					_, err := git_model.NewLFSMetaObject(ctx, &git_model.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
   236  					if err != nil {
   237  						log.Error("Unable to create LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
   238  						writeStatus(ctx, http.StatusInternalServerError)
   239  						return
   240  					}
   241  				} else {
   242  					exists = false
   243  				}
   244  			}
   245  
   246  			responseObject = buildObjectResponse(rc, p, false, !exists, err)
   247  		} else {
   248  			var err *lfs_module.ObjectError
   249  			if !exists || meta == nil {
   250  				err = &lfs_module.ObjectError{
   251  					Code:    http.StatusNotFound,
   252  					Message: http.StatusText(http.StatusNotFound),
   253  				}
   254  			}
   255  
   256  			responseObject = buildObjectResponse(rc, p, true, false, err)
   257  		}
   258  		responseObjects = append(responseObjects, responseObject)
   259  	}
   260  
   261  	respobj := &lfs_module.BatchResponse{Objects: responseObjects}
   262  
   263  	ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
   264  
   265  	enc := json.NewEncoder(ctx.Resp)
   266  	if err := enc.Encode(respobj); err != nil {
   267  		log.Error("Failed to encode representation as json. Error: %v", err)
   268  	}
   269  }
   270  
   271  // UploadHandler receives data from the client and puts it into the content store
   272  func UploadHandler(ctx *context.Context) {
   273  	rc := getRequestContext(ctx)
   274  
   275  	p := lfs_module.Pointer{Oid: ctx.Params("oid")}
   276  	var err error
   277  	if p.Size, err = strconv.ParseInt(ctx.Params("size"), 10, 64); err != nil {
   278  		writeStatusMessage(ctx, http.StatusUnprocessableEntity, err.Error())
   279  	}
   280  
   281  	if !p.IsValid() {
   282  		log.Trace("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)
   283  		writeStatus(ctx, http.StatusUnprocessableEntity)
   284  		return
   285  	}
   286  
   287  	repository := getAuthenticatedRepository(ctx, rc, true)
   288  	if repository == nil {
   289  		return
   290  	}
   291  
   292  	contentStore := lfs_module.NewContentStore()
   293  	exists, err := contentStore.Exists(p)
   294  	if err != nil {
   295  		log.Error("Unable to check if LFS OID[%s] exist. Error: %v", p.Oid, err)
   296  		writeStatus(ctx, http.StatusInternalServerError)
   297  		return
   298  	}
   299  
   300  	uploadOrVerify := func() error {
   301  		if exists {
   302  			accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
   303  			if err != nil {
   304  				log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err)
   305  				return err
   306  			}
   307  			if !accessible {
   308  				// The file exists but the user has no access to it.
   309  				// The upload gets verified by hashing and size comparison to prove access to it.
   310  				hash := sha256.New()
   311  				written, err := io.Copy(hash, ctx.Req.Body)
   312  				if err != nil {
   313  					log.Error("Error creating hash. Error: %v", err)
   314  					return err
   315  				}
   316  
   317  				if written != p.Size {
   318  					return lfs_module.ErrSizeMismatch
   319  				}
   320  				if hex.EncodeToString(hash.Sum(nil)) != p.Oid {
   321  					return lfs_module.ErrHashMismatch
   322  				}
   323  			}
   324  		} else if err := contentStore.Put(p, ctx.Req.Body); err != nil {
   325  			log.Error("Error putting LFS MetaObject [%s] into content store. Error: %v", p.Oid, err)
   326  			return err
   327  		}
   328  		_, err := git_model.NewLFSMetaObject(ctx, &git_model.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
   329  		return err
   330  	}
   331  
   332  	defer ctx.Req.Body.Close()
   333  	if err := uploadOrVerify(); err != nil {
   334  		if errors.Is(err, lfs_module.ErrSizeMismatch) || errors.Is(err, lfs_module.ErrHashMismatch) {
   335  			log.Error("Upload does not match LFS MetaObject [%s]. Error: %v", p.Oid, err)
   336  			writeStatusMessage(ctx, http.StatusUnprocessableEntity, err.Error())
   337  		} else {
   338  			log.Error("Error whilst uploadOrVerify LFS OID[%s]: %v", p.Oid, err)
   339  			writeStatus(ctx, http.StatusInternalServerError)
   340  		}
   341  		if _, err = git_model.RemoveLFSMetaObjectByOid(ctx, repository.ID, p.Oid); err != nil {
   342  			log.Error("Error whilst removing MetaObject for LFS OID[%s]: %v", p.Oid, err)
   343  		}
   344  		return
   345  	}
   346  
   347  	writeStatus(ctx, http.StatusOK)
   348  }
   349  
   350  // VerifyHandler verify oid and its size from the content store
   351  func VerifyHandler(ctx *context.Context) {
   352  	var p lfs_module.Pointer
   353  	if err := decodeJSON(ctx.Req, &p); err != nil {
   354  		writeStatus(ctx, http.StatusUnprocessableEntity)
   355  		return
   356  	}
   357  
   358  	rc := getRequestContext(ctx)
   359  
   360  	meta := getAuthenticatedMeta(ctx, rc, p, true)
   361  	if meta == nil {
   362  		return
   363  	}
   364  
   365  	contentStore := lfs_module.NewContentStore()
   366  	ok, err := contentStore.Verify(meta.Pointer)
   367  
   368  	status := http.StatusOK
   369  	if err != nil {
   370  		log.Error("Error whilst verifying LFS OID[%s]: %v", p.Oid, err)
   371  		status = http.StatusInternalServerError
   372  	} else if !ok {
   373  		status = http.StatusNotFound
   374  	}
   375  	writeStatus(ctx, status)
   376  }
   377  
   378  func decodeJSON(req *http.Request, v any) error {
   379  	defer req.Body.Close()
   380  
   381  	dec := json.NewDecoder(req.Body)
   382  	return dec.Decode(v)
   383  }
   384  
   385  func getRequestContext(ctx *context.Context) *requestContext {
   386  	return &requestContext{
   387  		User:          ctx.Params("username"),
   388  		Repo:          strings.TrimSuffix(ctx.Params("reponame"), ".git"),
   389  		Authorization: ctx.Req.Header.Get("Authorization"),
   390  	}
   391  }
   392  
   393  func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module.Pointer, requireWrite bool) *git_model.LFSMetaObject {
   394  	if !p.IsValid() {
   395  		log.Info("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)
   396  		writeStatusMessage(ctx, http.StatusUnprocessableEntity, "Oid or size are invalid")
   397  		return nil
   398  	}
   399  
   400  	repository := getAuthenticatedRepository(ctx, rc, requireWrite)
   401  	if repository == nil {
   402  		return nil
   403  	}
   404  
   405  	meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)
   406  	if err != nil {
   407  		log.Error("Unable to get LFS OID[%s] Error: %v", p.Oid, err)
   408  		writeStatus(ctx, http.StatusNotFound)
   409  		return nil
   410  	}
   411  
   412  	return meta
   413  }
   414  
   415  func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requireWrite bool) *repo_model.Repository {
   416  	repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rc.User, rc.Repo)
   417  	if err != nil {
   418  		log.Error("Unable to get repository: %s/%s Error: %v", rc.User, rc.Repo, err)
   419  		writeStatus(ctx, http.StatusNotFound)
   420  		return nil
   421  	}
   422  
   423  	if !authenticate(ctx, repository, rc.Authorization, false, requireWrite) {
   424  		requireAuth(ctx)
   425  		return nil
   426  	}
   427  
   428  	if requireWrite {
   429  		context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
   430  	} else {
   431  		context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
   432  	}
   433  
   434  	if ctx.Written() {
   435  		return nil
   436  	}
   437  
   438  	return repository
   439  }
   440  
   441  func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, download, upload bool, err *lfs_module.ObjectError) *lfs_module.ObjectResponse {
   442  	rep := &lfs_module.ObjectResponse{Pointer: pointer}
   443  	if err != nil {
   444  		rep.Error = err
   445  	} else {
   446  		rep.Actions = make(map[string]*lfs_module.Link)
   447  
   448  		header := make(map[string]string)
   449  
   450  		if len(rc.Authorization) > 0 {
   451  			header["Authorization"] = rc.Authorization
   452  		}
   453  
   454  		if download {
   455  			var link *lfs_module.Link
   456  			if setting.LFS.Storage.MinioConfig.ServeDirect {
   457  				// If we have a signed url (S3, object storage), redirect to this directly.
   458  				u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid)
   459  				if u != nil && err == nil {
   460  					// Presigned url does not need the Authorization header
   461  					// https://github.com/go-gitea/gitea/issues/21525
   462  					delete(header, "Authorization")
   463  					link = &lfs_module.Link{Href: u.String(), Header: header}
   464  				}
   465  			}
   466  			if link == nil {
   467  				link = &lfs_module.Link{Href: rc.DownloadLink(pointer), Header: header}
   468  			}
   469  			rep.Actions["download"] = link
   470  		}
   471  		if upload {
   472  			rep.Actions["upload"] = &lfs_module.Link{Href: rc.UploadLink(pointer), Header: header}
   473  
   474  			verifyHeader := make(map[string]string)
   475  			for key, value := range header {
   476  				verifyHeader[key] = value
   477  			}
   478  
   479  			// This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662
   480  			verifyHeader["Accept"] = lfs_module.MediaType
   481  
   482  			rep.Actions["verify"] = &lfs_module.Link{Href: rc.VerifyLink(pointer), Header: verifyHeader}
   483  		}
   484  	}
   485  	return rep
   486  }
   487  
   488  func writeStatus(ctx *context.Context, status int) {
   489  	writeStatusMessage(ctx, status, http.StatusText(status))
   490  }
   491  
   492  func writeStatusMessage(ctx *context.Context, status int, message string) {
   493  	ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
   494  	ctx.Resp.WriteHeader(status)
   495  
   496  	er := lfs_module.ErrorResponse{Message: message}
   497  
   498  	enc := json.NewEncoder(ctx.Resp)
   499  	if err := enc.Encode(er); err != nil {
   500  		log.Error("Failed to encode error response as json. Error: %v", err)
   501  	}
   502  }
   503  
   504  // authenticate uses the authorization string to determine whether
   505  // or not to proceed. This server assumes an HTTP Basic auth format.
   506  func authenticate(ctx *context.Context, repository *repo_model.Repository, authorization string, requireSigned, requireWrite bool) bool {
   507  	accessMode := perm.AccessModeRead
   508  	if requireWrite {
   509  		accessMode = perm.AccessModeWrite
   510  	}
   511  
   512  	if ctx.Data["IsActionsToken"] == true {
   513  		taskID := ctx.Data["ActionsTaskID"].(int64)
   514  		task, err := actions_model.GetTaskByID(ctx, taskID)
   515  		if err != nil {
   516  			log.Error("Unable to GetTaskByID for task[%d] Error: %v", taskID, err)
   517  			return false
   518  		}
   519  		if task.RepoID != repository.ID {
   520  			return false
   521  		}
   522  
   523  		if task.IsForkPullRequest {
   524  			return accessMode <= perm.AccessModeRead
   525  		}
   526  		return accessMode <= perm.AccessModeWrite
   527  	}
   528  
   529  	// ctx.IsSigned is unnecessary here, this will be checked in perm.CanAccess
   530  	perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
   531  	if err != nil {
   532  		log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository, err)
   533  		return false
   534  	}
   535  
   536  	canRead := perm.CanAccess(accessMode, unit.TypeCode)
   537  	if canRead && (!requireSigned || ctx.IsSigned) {
   538  		return true
   539  	}
   540  
   541  	user, err := parseToken(ctx, authorization, repository, accessMode)
   542  	if err != nil {
   543  		// Most of these are Warn level - the true internal server errors are logged in parseToken already
   544  		log.Warn("Authentication failure for provided token with Error: %v", err)
   545  		return false
   546  	}
   547  	ctx.Doer = user
   548  	return true
   549  }
   550  
   551  func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
   552  	if !strings.Contains(tokenSHA, ".") {
   553  		return nil, nil
   554  	}
   555  	token, err := jwt.ParseWithClaims(tokenSHA, &Claims{}, func(t *jwt.Token) (any, error) {
   556  		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
   557  			return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
   558  		}
   559  		return setting.LFS.JWTSecretBytes, nil
   560  	})
   561  	if err != nil {
   562  		return nil, nil
   563  	}
   564  
   565  	claims, claimsOk := token.Claims.(*Claims)
   566  	if !token.Valid || !claimsOk {
   567  		return nil, fmt.Errorf("invalid token claim")
   568  	}
   569  
   570  	if claims.RepoID != target.ID {
   571  		return nil, fmt.Errorf("invalid token claim")
   572  	}
   573  
   574  	if mode == perm.AccessModeWrite && claims.Op != "upload" {
   575  		return nil, fmt.Errorf("invalid token claim")
   576  	}
   577  
   578  	u, err := user_model.GetUserByID(ctx, claims.UserID)
   579  	if err != nil {
   580  		log.Error("Unable to GetUserById[%d]: Error: %v", claims.UserID, err)
   581  		return nil, err
   582  	}
   583  	return u, nil
   584  }
   585  
   586  func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
   587  	if authorization == "" {
   588  		return nil, fmt.Errorf("no token")
   589  	}
   590  
   591  	parts := strings.SplitN(authorization, " ", 2)
   592  	if len(parts) != 2 {
   593  		return nil, fmt.Errorf("no token")
   594  	}
   595  	tokenSHA := parts[1]
   596  	switch strings.ToLower(parts[0]) {
   597  	case "bearer":
   598  		fallthrough
   599  	case "token":
   600  		return handleLFSToken(ctx, tokenSHA, target, mode)
   601  	}
   602  	return nil, fmt.Errorf("token not found")
   603  }
   604  
   605  func requireAuth(ctx *context.Context) {
   606  	ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
   607  	writeStatus(ctx, http.StatusUnauthorized)
   608  }