639 lines · 18.8 KB · Go
RawDownload
// Copyright 2021 The Gitea Authors. All rights reserved.// SPDX-License-Identifier: MIT package lfs import (	stdCtx "context"	"crypto/sha256"	"encoding/base64"	"encoding/hex"	"errors"	"fmt"	"io"	"net/http"	"net/url"	"path"	"regexp"	"strconv"	"strings" 	actions_model "forgejo.org/models/actions"	auth_model "forgejo.org/models/auth"	git_model "forgejo.org/models/git"	"forgejo.org/models/perm"	access_model "forgejo.org/models/perm/access"	quota_model "forgejo.org/models/quota"	repo_model "forgejo.org/models/repo"	"forgejo.org/models/unit"	user_model "forgejo.org/models/user"	"forgejo.org/modules/json"	lfs_module "forgejo.org/modules/lfs"	"forgejo.org/modules/log"	"forgejo.org/modules/setting"	"forgejo.org/modules/storage"	"forgejo.org/services/context" 	"github.com/golang-jwt/jwt/v5") // requestContext contain variables from the HTTP request.type requestContext struct {	User          string	Repo          string	Authorization string} // Claims is a JWT Token Claimstype Claims struct {	RepoID int64	Op     string	UserID int64	jwt.RegisteredClaims} // DownloadLink builds a URL to download the object.func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {	return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid))} // UploadLink builds a URL to upload the object.func (rc *requestContext) UploadLink(p lfs_module.Pointer) string {	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))} // VerifyLink builds a URL for verifying the object.func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string {	return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/verify")} // CheckAcceptMediaType checks if the client accepts the LFS media type.func CheckAcceptMediaType(ctx *context.Context) {	mediaParts := strings.Split(ctx.Req.Header.Get("Accept"), ";") 	if mediaParts[0] != lfs_module.MediaType {		log.Trace("Calling a LFS method without accepting the correct media type: %s", lfs_module.MediaType)		writeStatus(ctx, http.StatusUnsupportedMediaType)		return	}} var rangeHeaderRegexp = regexp.MustCompile(`bytes=(\d+)\-(\d*).*`) // DownloadHandler gets the content from the content storefunc DownloadHandler(ctx *context.Context) {	rc := getRequestContext(ctx)	p := lfs_module.Pointer{Oid: ctx.Params("oid")} 	meta := getAuthenticatedMeta(ctx, rc, p, false)	if meta == nil {		return	} 	// Support resume download using Range header	var fromByte, toByte int64	toByte = meta.Size - 1	statusCode := http.StatusOK	if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" {		match := rangeHeaderRegexp.FindStringSubmatch(rangeHdr)		if len(match) > 1 {			statusCode = http.StatusPartialContent			fromByte, _ = strconv.ParseInt(match[1], 10, 32) 			if fromByte >= meta.Size {				writeStatus(ctx, http.StatusRequestedRangeNotSatisfiable)				return			} 			if match[2] != "" {				_toByte, _ := strconv.ParseInt(match[2], 10, 32)				if _toByte >= fromByte && _toByte < toByte {					toByte = _toByte				}			} 			ctx.Resp.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", fromByte, toByte, meta.Size-fromByte))			ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Range")		}	} 	contentStore := lfs_module.NewContentStore()	content, err := contentStore.Get(meta.Pointer)	if err != nil {		writeStatus(ctx, http.StatusNotFound)		return	}	defer content.Close() 	if fromByte > 0 {		_, err = content.Seek(fromByte, io.SeekStart)		if err != nil {			log.Error("Whilst trying to read LFS OID[%s]: Unable to seek to %d Error: %v", meta.Oid, fromByte, err)			writeStatus(ctx, http.StatusInternalServerError)			return		}	} 	contentLength := toByte + 1 - fromByte	ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10))	ctx.Resp.Header().Set("Content-Type", "application/octet-stream") 	filename := ctx.Params("filename")	if len(filename) > 0 {		decodedFilename, err := base64.RawURLEncoding.DecodeString(filename)		if err == nil {			ctx.Resp.Header().Set("Content-Disposition", "attachment; filename=\""+string(decodedFilename)+"\"")			ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")		}	} 	ctx.Resp.WriteHeader(statusCode)	if written, err := io.CopyN(ctx.Resp, content, contentLength); err != nil {		log.Error("Error whilst copying LFS OID[%s] to the response after %d bytes. Error: %v", meta.Oid, written, err)	}} // BatchHandler provides the batch apifunc BatchHandler(ctx *context.Context) {	var br lfs_module.BatchRequest	if err := decodeJSON(ctx.Req, &br); err != nil {		log.Trace("Unable to decode BATCH request vars: Error: %v", err)		writeStatus(ctx, http.StatusBadRequest)		return	} 	var isUpload bool	switch br.Operation {	case "upload":		isUpload = true	case "download":		isUpload = false	default:		log.Trace("Attempt to BATCH with invalid operation: %s", br.Operation)		writeStatus(ctx, http.StatusBadRequest)		return	} 	rc := getRequestContext(ctx) 	repository := getAuthenticatedRepository(ctx, rc, isUpload)	if repository == nil {		return	} 	if isUpload {		ok, err := quota_model.EvaluateForUser(ctx, ctx.Doer.ID, quota_model.LimitSubjectSizeGitLFS)		if err != nil {			log.Error("quota_model.EvaluateForUser: %v", err)			writeStatus(ctx, http.StatusInternalServerError)			return		}		if !ok {			writeStatusMessage(ctx, http.StatusRequestEntityTooLarge, "quota exceeded")		}	} 	if setting.LFS.MaxBatchSize != 0 && len(br.Objects) > setting.LFS.MaxBatchSize {		writeStatus(ctx, http.StatusRequestEntityTooLarge)		return	} 	contentStore := lfs_module.NewContentStore() 	var responseObjects []*lfs_module.ObjectResponse 	for _, p := range br.Objects {		if !p.IsValid() {			responseObjects = append(responseObjects, buildObjectResponse(rc, p, false, false, &lfs_module.ObjectError{				Code:    http.StatusUnprocessableEntity,				Message: "Oid or size are invalid",			}))			continue		} 		exists, err := contentStore.Exists(p)		if err != nil {			log.Error("Unable to check if LFS OID[%s] exist. Error: %v", p.Oid, rc.User, rc.Repo, err)			writeStatus(ctx, http.StatusInternalServerError)			return		} 		meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)		if err != nil && err != git_model.ErrLFSObjectNotExist {			log.Error("Unable to get LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)			writeStatus(ctx, http.StatusInternalServerError)			return		} 		if meta != nil && p.Size != meta.Size {			responseObjects = append(responseObjects, buildObjectResponse(rc, p, false, false, &lfs_module.ObjectError{				Code:    http.StatusUnprocessableEntity,				Message: fmt.Sprintf("Object %s is not %d bytes", p.Oid, p.Size),			}))			continue		} 		var responseObject *lfs_module.ObjectResponse		if isUpload {			var err *lfs_module.ObjectError			if !exists && setting.LFS.MaxFileSize > 0 && p.Size > setting.LFS.MaxFileSize {				err = &lfs_module.ObjectError{					Code:    http.StatusUnprocessableEntity,					Message: fmt.Sprintf("Size must be less than or equal to %d", setting.LFS.MaxFileSize),				}			} 			if exists && meta == nil {				accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)				if err != nil {					log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err)					writeStatus(ctx, http.StatusInternalServerError)					return				}				if accessible {					_, err := git_model.NewLFSMetaObject(ctx, repository.ID, p)					if err != nil {						log.Error("Unable to create LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)						writeStatus(ctx, http.StatusInternalServerError)						return					}				} else {					exists = false				}			} 			responseObject = buildObjectResponse(rc, p, false, !exists, err)		} else {			var err *lfs_module.ObjectError			if !exists || meta == nil {				err = &lfs_module.ObjectError{					Code:    http.StatusNotFound,					Message: http.StatusText(http.StatusNotFound),				}			} 			responseObject = buildObjectResponse(rc, p, true, false, err)		}		responseObjects = append(responseObjects, responseObject)	} 	respobj := &lfs_module.BatchResponse{Objects: responseObjects} 	ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType) 	enc := json.NewEncoder(ctx.Resp)	if err := enc.Encode(respobj); err != nil {		log.Error("Failed to encode representation as json. Error: %v", err)	}} // UploadHandler receives data from the client and puts it into the content storefunc UploadHandler(ctx *context.Context) {	rc := getRequestContext(ctx) 	p := lfs_module.Pointer{Oid: ctx.Params("oid")}	var err error	if p.Size, err = strconv.ParseInt(ctx.Params("size"), 10, 64); err != nil {		writeStatusMessage(ctx, http.StatusUnprocessableEntity, err.Error())	} 	if !p.IsValid() {		log.Trace("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)		writeStatus(ctx, http.StatusUnprocessableEntity)		return	} 	repository := getAuthenticatedRepository(ctx, rc, true)	if repository == nil {		return	} 	contentStore := lfs_module.NewContentStore()	exists, err := contentStore.Exists(p)	if err != nil {		log.Error("Unable to check if LFS OID[%s] exist. Error: %v", p.Oid, err)		writeStatus(ctx, http.StatusInternalServerError)		return	} 	if exists {		ok, err := quota_model.EvaluateForUser(ctx, ctx.Doer.ID, quota_model.LimitSubjectSizeGitLFS)		if err != nil {			log.Error("quota_model.EvaluateForUser: %v", err)			writeStatus(ctx, http.StatusInternalServerError)			return		}		if !ok {			writeStatusMessage(ctx, http.StatusRequestEntityTooLarge, "quota exceeded")		}	} 	uploadOrVerify := func() error {		if exists {			accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)			if err != nil {				log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err)				return err			}			if !accessible {				// The file exists but the user has no access to it.				// The upload gets verified by hashing and size comparison to prove access to it.				hash := sha256.New()				written, err := io.Copy(hash, ctx.Req.Body)				if err != nil {					log.Error("Error creating hash. Error: %v", err)					return err				} 				if written != p.Size {					return lfs_module.ErrSizeMismatch				}				if hex.EncodeToString(hash.Sum(nil)) != p.Oid {					return lfs_module.ErrHashMismatch				}			}		} else if err := contentStore.Put(p, ctx.Req.Body); err != nil {			log.Error("Error putting LFS MetaObject [%s] into content store. Error: %v", p.Oid, err)			return err		}		_, err := git_model.NewLFSMetaObject(ctx, repository.ID, p)		return err	} 	defer ctx.Req.Body.Close()	if err := uploadOrVerify(); err != nil {		if errors.Is(err, lfs_module.ErrSizeMismatch) || errors.Is(err, lfs_module.ErrHashMismatch) {			log.Error("Upload does not match LFS MetaObject [%s]. Error: %v", p.Oid, err)			writeStatusMessage(ctx, http.StatusUnprocessableEntity, err.Error())		} else {			log.Error("Error whilst uploadOrVerify LFS OID[%s]: %v", p.Oid, err)			writeStatus(ctx, http.StatusInternalServerError)		}		if _, err = git_model.RemoveLFSMetaObjectByOid(ctx, repository.ID, p.Oid); err != nil {			log.Error("Error whilst removing MetaObject for LFS OID[%s]: %v", p.Oid, err)		}		return	} 	writeStatus(ctx, http.StatusOK)} // VerifyHandler verify oid and its size from the content storefunc VerifyHandler(ctx *context.Context) {	var p lfs_module.Pointer	if err := decodeJSON(ctx.Req, &p); err != nil {		writeStatus(ctx, http.StatusUnprocessableEntity)		return	} 	rc := getRequestContext(ctx) 	meta := getAuthenticatedMeta(ctx, rc, p, true)	if meta == nil {		return	} 	contentStore := lfs_module.NewContentStore()	ok, err := contentStore.Verify(meta.Pointer) 	status := http.StatusOK	if err != nil {		log.Error("Error whilst verifying LFS OID[%s]: %v", p.Oid, err)		status = http.StatusInternalServerError	} else if !ok {		status = http.StatusNotFound	}	writeStatus(ctx, status)} func decodeJSON(req *http.Request, v any) error {	defer req.Body.Close() 	dec := json.NewDecoder(req.Body)	return dec.Decode(v)} func getRequestContext(ctx *context.Context) *requestContext {	return &requestContext{		User:          ctx.Params("username"),		Repo:          strings.TrimSuffix(ctx.Params("reponame"), ".git"),		Authorization: ctx.Req.Header.Get("Authorization"),	}} func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module.Pointer, requireWrite bool) *git_model.LFSMetaObject {	if !p.IsValid() {		log.Info("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)		writeStatusMessage(ctx, http.StatusUnprocessableEntity, "Oid or size are invalid")		return nil	} 	repository := getAuthenticatedRepository(ctx, rc, requireWrite)	if repository == nil {		return nil	} 	meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)	if err != nil {		log.Error("Unable to get LFS OID[%s] Error: %v", p.Oid, err)		writeStatus(ctx, http.StatusNotFound)		return nil	} 	return meta} func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requireWrite bool) *repo_model.Repository {	repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rc.User, rc.Repo)	if err != nil {		log.Error("Unable to get repository: %s/%s Error: %v", rc.User, rc.Repo, err)		writeStatus(ctx, http.StatusNotFound)		return nil	} 	if !authenticate(ctx, repository, rc.Authorization, false, requireWrite) {		requireAuth(ctx)		return nil	} 	if requireWrite {		context.CheckRepoScopedToken(ctx, repository, auth_model.Write)	} else {		context.CheckRepoScopedToken(ctx, repository, auth_model.Read)	} 	if ctx.Written() {		return nil	} 	return repository} func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, download, upload bool, err *lfs_module.ObjectError) *lfs_module.ObjectResponse {	rep := &lfs_module.ObjectResponse{Pointer: pointer}	if err != nil {		rep.Error = err	} else {		rep.Actions = make(map[string]*lfs_module.Link) 		header := make(map[string]string) 		if len(rc.Authorization) > 0 {			header["Authorization"] = rc.Authorization		} 		if download {			var link *lfs_module.Link			if setting.LFS.Storage.MinioConfig.ServeDirect {				// If we have a signed url (S3, object storage), redirect to this directly.				u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid, nil)				if u != nil && err == nil {					// Presigned url does not need the Authorization header					// https://github.com/go-gitea/gitea/issues/21525					delete(header, "Authorization")					link = &lfs_module.Link{Href: u.String(), Header: header}				}			}			if link == nil {				link = &lfs_module.Link{Href: rc.DownloadLink(pointer), Header: header}			}			rep.Actions["download"] = link

Lines 1–500 of 639.