Files

639 lines
20 KiB
Go
Raw Permalink Normal View History

2021-06-06 01:59:27 +02:00
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
2020-04-05 01:20:50 -05:00
2016-12-26 02:16:37 +01:00
package lfs
import (
stdCtx "context"
2024-02-25 14:32:13 +01:00
"crypto/sha256"
2016-12-26 02:16:37 +01:00
"encoding/base64"
2021-08-31 15:35:08 +02:00
"encoding/hex"
2021-06-06 01:59:27 +02:00
"errors"
2016-12-26 02:16:37 +01:00
"fmt"
"io"
"net/http"
2021-11-16 18:18:25 +00:00
"net/url"
2016-12-26 02:16:37 +01:00
"regexp"
"strconv"
"strings"
2025-10-21 02:43:08 +08:00
"time"
2016-12-26 02:16:37 +01:00
2023-06-04 14:57:16 -04:00
auth_model "code.gitea.io/gitea/models/auth"
2022-06-12 23:51:54 +08:00
git_model "code.gitea.io/gitea/models/git"
perm_model "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
2021-11-10 03:57:58 +08:00
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
2025-07-03 11:02:38 +08:00
"code.gitea.io/gitea/modules/auth/httpauth"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
2021-04-09 00:25:57 +02:00
lfs_module "code.gitea.io/gitea/modules/lfs"
2016-12-26 02:16:37 +01:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"github.com/golang-jwt/jwt/v5"
2016-12-26 02:16:37 +01:00
)
2021-04-09 00:25:57 +02:00
// requestContext contain variables from the HTTP request.
type requestContext struct {
2016-12-26 02:16:37 +01:00
User string
Repo string
Authorization string
RepoGitURL string
2016-12-26 02:16:37 +01:00
}
2020-03-09 19:56:18 +00:00
// Claims is a JWT Token Claims
type Claims struct {
RepoID int64
Op string
UserID int64
jwt.RegisteredClaims
2020-03-09 19:56:18 +00:00
}
2025-10-21 02:43:08 +08:00
type AuthTokenOptions struct {
Op string
UserID int64
RepoID int64
}
func GetLFSAuthTokenWithBearer(opts AuthTokenOptions) (string, error) {
now := time.Now()
claims := Claims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)),
NotBefore: jwt.NewNumericDate(now),
},
RepoID: opts.RepoID,
Op: opts.Op,
UserID: opts.UserID,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
if err != nil {
return "", fmt.Errorf("failed to sign LFS JWT token: %w", err)
}
return "Bearer " + tokenString, nil
}
2021-06-06 01:59:27 +02:00
// DownloadLink builds a URL to download the object.
func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
return rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid)
}
2021-06-06 01:59:27 +02:00
// UploadLink builds a URL to upload the object.
func (rc *requestContext) UploadLink(p lfs_module.Pointer) string {
return rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid) + "/" + strconv.FormatInt(p.Size, 10)
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
// VerifyLink builds a URL for verifying the object.
func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string {
return rc.RepoGitURL + "/info/lfs/verify"
2018-07-19 23:39:19 +08:00
}
2021-06-06 01:59:27 +02:00
// CheckAcceptMediaType checks if the client accepts the LFS media type.
func CheckAcceptMediaType(ctx *context.Context) {
mediaParts := strings.Split(ctx.Req.Header.Get("Accept"), ";")
2021-06-06 01:59:27 +02:00
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)
2016-12-26 02:16:37 +01:00
return
}
}
var rangeHeaderRegexp = regexp.MustCompile(`bytes=(\d+)-(\d*).*`)
2023-06-21 14:57:18 -05:00
2021-06-06 01:59:27 +02:00
// DownloadHandler gets the content from the content store
func DownloadHandler(ctx *context.Context) {
rc := getRequestContext(ctx)
2024-06-19 06:32:45 +08:00
p := lfs_module.Pointer{Oid: ctx.PathParam("oid")}
2021-06-06 01:59:27 +02:00
meta := getAuthenticatedMeta(ctx, rc, p, false)
if meta == nil {
2016-12-26 02:16:37 +01:00
return
}
// Support resume download using Range header
2020-05-11 09:37:59 +01:00
var fromByte, toByte int64
toByte = meta.Size - 1
2021-06-06 01:59:27 +02:00
statusCode := http.StatusOK
2016-12-26 02:16:37 +01:00
if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" {
2023-06-21 14:57:18 -05:00
match := rangeHeaderRegexp.FindStringSubmatch(rangeHdr)
2019-06-12 21:41:28 +02:00
if len(match) > 1 {
2021-06-06 01:59:27 +02:00
statusCode = http.StatusPartialContent
2016-12-26 02:16:37 +01:00
fromByte, _ = strconv.ParseInt(match[1], 10, 32)
2020-05-11 09:37:59 +01:00
if fromByte >= meta.Size {
writeStatus(ctx, http.StatusRequestedRangeNotSatisfiable)
return
}
2020-05-11 09:37:59 +01:00
if match[2] != "" {
_toByte, _ := strconv.ParseInt(match[2], 10, 32)
if _toByte >= fromByte && _toByte < toByte {
toByte = _toByte
}
}
2025-08-16 10:51:45 -06:00
ctx.Resp.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", fromByte, toByte, meta.Size))
2020-08-13 18:18:18 +01:00
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Range")
2016-12-26 02:16:37 +01:00
}
}
2021-04-09 00:25:57 +02:00
contentStore := lfs_module.NewContentStore()
content, err := contentStore.Get(meta.Pointer)
2016-12-26 02:16:37 +01:00
if err != nil {
writeStatus(ctx, http.StatusNotFound)
2016-12-26 02:16:37 +01:00
return
}
2020-03-09 19:56:18 +00:00
defer content.Close()
2016-12-26 02:16:37 +01:00
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
}
}
2020-05-11 09:37:59 +01:00
contentLength := toByte + 1 - fromByte
2025-01-31 19:05:48 +08:00
contentLengthStr := strconv.FormatInt(contentLength, 10)
ctx.Resp.Header().Set("Content-Length", contentLengthStr)
ctx.Resp.Header().Set("X-Gitea-LFS-Content-Length", contentLengthStr) // we need this header to make sure it won't be affected by reverse proxy or compression
2016-12-26 02:16:37 +01:00
ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
2024-06-19 06:32:45 +08:00
filename := ctx.PathParam("filename")
2016-12-26 02:16:37 +01:00
if len(filename) > 0 {
decodedFilename, err := base64.RawURLEncoding.DecodeString(filename)
if err == nil {
ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(string(decodedFilename)))
2020-08-13 18:18:18 +01:00
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
2016-12-26 02:16:37 +01:00
}
}
ctx.Resp.WriteHeader(statusCode)
2020-05-11 09:37:59 +01:00
if written, err := io.CopyN(ctx.Resp, content, contentLength); err != nil {
2020-03-09 19:56:18 +00:00
log.Error("Error whilst copying LFS OID[%s] to the response after %d bytes. Error: %v", meta.Oid, written, err)
}
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
// BatchHandler provides the batch api
func 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)
2018-05-01 04:46:04 +03:00
return
}
2021-06-06 01:59:27 +02:00
var isUpload bool
2025-03-29 22:32:28 +01:00
switch br.Operation {
case "upload":
2021-06-06 01:59:27 +02:00
isUpload = true
2025-03-29 22:32:28 +01:00
case "download":
2021-06-06 01:59:27 +02:00
isUpload = false
2025-03-29 22:32:28 +01:00
default:
2021-06-06 01:59:27 +02:00
log.Trace("Attempt to BATCH with invalid operation: %s", br.Operation)
writeStatus(ctx, http.StatusBadRequest)
2018-05-01 04:46:04 +03:00
return
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
rc := getRequestContext(ctx)
2020-02-28 04:46:57 +00:00
2021-06-06 01:59:27 +02:00
repository := getAuthenticatedRepository(ctx, rc, isUpload)
if repository == nil {
2016-12-26 02:16:37 +01:00
return
}
if setting.LFS.MaxBatchSize != 0 && len(br.Objects) > setting.LFS.MaxBatchSize {
writeStatus(ctx, http.StatusRequestEntityTooLarge)
return
}
2021-04-09 00:25:57 +02:00
contentStore := lfs_module.NewContentStore()
var responseObjects []*lfs_module.ObjectResponse
2016-12-26 02:16:37 +01:00
2021-06-06 01:59:27 +02:00
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",
}))
2018-07-19 23:39:19 +08:00
continue
}
2021-06-06 01:59:27 +02:00
exists, err := contentStore.Exists(p)
2016-12-26 02:16:37 +01:00
if err != nil {
2025-06-24 21:51:04 +08:00
log.Error("Unable to check if LFS object with ID '%s' exists for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
2021-06-06 01:59:27 +02:00
writeStatus(ctx, http.StatusInternalServerError)
2016-12-26 02:16:37 +01:00
return
}
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)
2022-06-12 23:51:54 +08:00
if err != nil && err != git_model.ErrLFSObjectNotExist {
2021-06-06 01:59:27 +02:00
log.Error("Unable to get LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
writeStatus(ctx, http.StatusInternalServerError)
2016-12-26 02:16:37 +01:00
return
}
2021-06-06 01:59:27 +02:00
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
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
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),
}
}
2020-03-03 20:57:27 +00:00
2021-08-31 15:35:08 +02:00
if exists && meta == nil {
accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
2021-08-31 15:35:08 +02:00
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)
2021-06-06 01:59:27 +02:00
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
}
2021-08-31 15:35:08 +02:00
} else {
exists = false
2021-06-06 01:59:27 +02:00
}
2020-09-08 23:45:10 +08:00
}
2021-06-06 01:59:27 +02:00
responseObject = buildObjectResponse(rc, p, false, !exists, err)
2020-03-09 19:56:18 +00:00
} else {
2021-06-06 01:59:27 +02:00
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)
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
responseObjects = append(responseObjects, responseObject)
2016-12-26 02:16:37 +01:00
}
2021-04-09 00:25:57 +02:00
respobj := &lfs_module.BatchResponse{Objects: responseObjects}
2016-12-26 02:16:37 +01:00
2021-06-06 01:59:27 +02:00
ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
enc := json.NewEncoder(ctx.Resp)
2020-03-09 19:56:18 +00:00
if err := enc.Encode(respobj); err != nil {
log.Error("Failed to encode representation as json. Error: %v", err)
}
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
// UploadHandler receives data from the client and puts it into the content store
func UploadHandler(ctx *context.Context) {
rc := getRequestContext(ctx)
2016-12-26 02:16:37 +01:00
2024-06-19 06:32:45 +08:00
p := lfs_module.Pointer{Oid: ctx.PathParam("oid")}
2021-06-06 01:59:27 +02:00
var err error
2024-06-19 06:32:45 +08:00
if p.Size, err = strconv.ParseInt(ctx.PathParam("size"), 10, 64); err != nil {
2021-06-06 01:59:27 +02:00
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
}
2021-04-09 00:25:57 +02:00
contentStore := lfs_module.NewContentStore()
2021-06-06 01:59:27 +02:00
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
}
2021-08-31 15:35:08 +02:00
uploadOrVerify := func() error {
if exists {
accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
2021-08-31 15:35:08 +02:00
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)
2021-08-31 15:35:08 +02:00
return err
2021-06-06 01:59:27 +02:00
}
2021-01-26 23:36:53 +08:00
defer ctx.Req.Body.Close()
2021-08-31 15:35:08 +02:00
if err := uploadOrVerify(); err != nil {
2021-06-06 01:59:27 +02:00
if errors.Is(err, lfs_module.ErrSizeMismatch) || errors.Is(err, lfs_module.ErrHashMismatch) {
2021-08-31 15:35:08 +02:00
log.Error("Upload does not match LFS MetaObject [%s]. Error: %v", p.Oid, err)
2021-06-06 01:59:27 +02:00
writeStatusMessage(ctx, http.StatusUnprocessableEntity, err.Error())
2020-03-09 19:56:18 +00:00
} else {
2023-04-12 23:01:41 +08:00
log.Error("Error whilst uploadOrVerify LFS OID[%s]: %v", p.Oid, err)
2021-06-06 01:59:27 +02:00
writeStatus(ctx, http.StatusInternalServerError)
2020-03-09 19:56:18 +00:00
}
if _, err = git_model.RemoveLFSMetaObjectByOid(ctx, repository.ID, p.Oid); err != nil {
2023-04-12 23:01:41 +08:00
log.Error("Error whilst removing MetaObject for LFS OID[%s]: %v", p.Oid, err)
}
2016-12-26 02:16:37 +01:00
return
}
2021-06-06 01:59:27 +02:00
writeStatus(ctx, http.StatusOK)
2016-12-26 02:16:37 +01:00
}
// VerifyHandler verify oid and its size from the content store
func VerifyHandler(ctx *context.Context) {
2021-06-06 01:59:27 +02:00
var p lfs_module.Pointer
if err := decodeJSON(ctx.Req, &p); err != nil {
writeStatus(ctx, http.StatusUnprocessableEntity)
return
}
2021-06-06 01:59:27 +02:00
rc := getRequestContext(ctx)
2021-06-06 01:59:27 +02:00
meta := getAuthenticatedMeta(ctx, rc, p, true)
if meta == nil {
return
}
2021-04-09 00:25:57 +02:00
contentStore := lfs_module.NewContentStore()
ok, err := contentStore.Verify(meta.Pointer)
2021-06-06 01:59:27 +02:00
status := http.StatusOK
if err != nil {
2023-04-12 23:01:41 +08:00
log.Error("Error whilst verifying LFS OID[%s]: %v", p.Oid, err)
2021-06-06 01:59:27 +02:00
status = http.StatusInternalServerError
} else if !ok {
status = http.StatusNotFound
}
2021-06-06 01:59:27 +02:00
writeStatus(ctx, status)
}
2023-07-04 20:36:08 +02:00
func decodeJSON(req *http.Request, v any) error {
2021-06-06 01:59:27 +02:00
defer req.Body.Close()
dec := json.NewDecoder(req.Body)
2021-06-06 01:59:27 +02:00
return dec.Decode(v)
}
2021-06-06 01:59:27 +02:00
func getRequestContext(ctx *context.Context) *requestContext {
ownerName := ctx.PathParam("username")
repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
2021-06-06 01:59:27 +02:00
return &requestContext{
User: ownerName,
Repo: repoName,
2021-06-06 01:59:27 +02:00
Authorization: ctx.Req.Header.Get("Authorization"),
RepoGitURL: httplib.GuessCurrentAppURL(ctx) + url.PathEscape(ownerName) + "/" + url.PathEscape(repoName+".git"),
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
}
2016-12-26 02:16:37 +01:00
2022-06-12 23:51:54 +08:00
func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module.Pointer, requireWrite bool) *git_model.LFSMetaObject {
2021-06-06 01:59:27 +02:00
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
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
repository := getAuthenticatedRepository(ctx, rc, requireWrite)
if repository == nil {
return nil
2016-12-26 02:16:37 +01:00
}
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)
2021-06-06 01:59:27 +02:00
if err != nil {
log.Error("Unable to get LFS OID[%s] Error: %v", p.Oid, err)
writeStatus(ctx, http.StatusNotFound)
return nil
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
return meta
}
func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requireWrite bool) *repo_model.Repository {
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rc.User, rc.Repo)
2021-06-06 01:59:27 +02:00
if err != nil {
log.Error("Unable to get repository: %s/%s Error: %v", rc.User, rc.Repo, err)
writeStatus(ctx, http.StatusNotFound)
return nil
}
2021-06-06 01:59:27 +02:00
if !authenticate(ctx, repository, rc.Authorization, false, requireWrite) {
requireAuth(ctx)
return nil
}
2023-06-04 14:57:16 -04:00
if requireWrite {
context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
} else {
context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
}
if ctx.Written() {
return nil
}
2021-06-06 01:59:27 +02:00
return repository
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
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)
if download {
var link *lfs_module.Link
2024-05-30 15:33:50 +08:00
if setting.LFS.Storage.ServeDirect() {
2022-01-20 18:46:10 +01:00
// If we have a signed url (S3, object storage), redirect to this directly.
// DO NOT USE the http POST method coming from the lfs batch endpoint
u, err := storage.LFS.ServeDirectURL(pointer.RelativePath(), pointer.Oid, http.MethodGet, nil)
if u != nil && err == nil {
link = lfs_module.NewLink(u.String()) // Presigned url does not need the Authorization header
}
}
if link == nil {
link = lfs_module.NewLink(rc.DownloadLink(pointer)).WithHeader("Authorization", rc.Authorization)
}
rep.Actions["download"] = link
2021-06-06 01:59:27 +02:00
}
if upload {
// Set Transfer-Encoding header to enable chunked uploads. Required by git-lfs client to do chunked transfer.
// See: https://github.com/git-lfs/git-lfs/blob/main/tq/basic_upload.go#L58-59
rep.Actions["upload"] = lfs_module.NewLink(rc.UploadLink(pointer)).
WithHeader("Authorization", rc.Authorization).
WithHeader("Transfer-Encoding", "chunked")
// "Accept" header is the workaround for git-lfs < 2.8.0 (before 2019).
// This workaround could be removed in the future: https://github.com/git-lfs/git-lfs/issues/3662
rep.Actions["verify"] = lfs_module.NewLink(rc.VerifyLink(pointer)).
WithHeader("Authorization", rc.Authorization).
WithHeader("Accept", lfs_module.AcceptHeader)
2021-06-06 01:59:27 +02:00
}
2016-12-26 02:16:37 +01:00
}
2021-06-06 01:59:27 +02:00
return rep
2016-12-26 02:16:37 +01:00
}
func writeStatus(ctx *context.Context, status int) {
2021-06-06 01:59:27 +02:00
writeStatusMessage(ctx, status, http.StatusText(status))
}
2016-12-26 02:16:37 +01:00
2021-06-06 01:59:27 +02:00
func writeStatusMessage(ctx *context.Context, status int, message string) {
ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
2016-12-26 02:16:37 +01:00
ctx.Resp.WriteHeader(status)
2021-06-06 01:59:27 +02:00
er := lfs_module.ErrorResponse{Message: message}
enc := json.NewEncoder(ctx.Resp)
2021-06-06 01:59:27 +02:00
if err := enc.Encode(er); err != nil {
log.Error("Failed to encode error response as json. Error: %v", err)
}
2016-12-26 02:16:37 +01:00
}
// authenticate uses the authorization string to determine whether
// to proceed. This server assumes an HTTP Basic auth format.
func authenticate(ctx *context.Context, repository *repo_model.Repository, authorization string, requireSigned, requireWrite bool) bool {
accessMode := perm_model.AccessModeRead
2016-12-26 02:16:37 +01:00
if requireWrite {
accessMode = perm_model.AccessModeWrite
2016-12-26 02:16:37 +01:00
}
2026-02-01 14:01:08 +08:00
if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
2025-10-22 13:12:31 +02:00
perm, err := access_model.GetActionsUserRepoPermission(ctx, repository, ctx.Doer, taskID)
if err != nil {
2025-10-22 13:12:31 +02:00
log.Error("Unable to GetActionsUserRepoPermission for task[%d] Error: %v", taskID, err)
return false
}
2025-10-22 13:12:31 +02:00
return perm.CanAccess(accessMode, unit.TypeCode)
}
2025-10-22 13:12:31 +02:00
// it works for both anonymous request and signed-in user, then perm.CanAccess will do the permission check
perm, err := access_model.GetDoerRepoPermission(ctx, repository, ctx.Doer)
if err != nil {
log.Error("Unable to GetDoerRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository, err)
return false
2016-12-26 02:16:37 +01:00
}
2025-10-22 13:12:31 +02:00
canAccess := perm.CanAccess(accessMode, unit.TypeCode)
// if it doesn't require sign-in and anonymous user has access, return true
// if the user is already signed in (for example: by session auth method), and the doer can access, return true
if canAccess && (!requireSigned || ctx.IsSigned) {
return true
2016-12-26 02:16:37 +01:00
}
2025-10-22 13:12:31 +02:00
// now, either sign-in is required or the ctx.Doer cannot access, check the LFS token
// however, "ctx.Doer exists but cannot access then check LFS token" should not really happen:
// * why a request can be sent with both valid user session and valid LFS token then use LFS token to access?
user, err := parseToken(ctx, authorization, repository, accessMode)
2016-12-26 02:16:37 +01:00
if err != nil {
2020-03-09 19:56:18 +00:00
// Most of these are Warn level - the true internal server errors are logged in parseToken already
log.Warn("Authentication failure for provided token with Error: %v", err)
2016-12-26 02:16:37 +01:00
return false
}
2022-03-22 08:03:22 +01:00
ctx.Doer = user
return true
}
func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm_model.AccessMode) (*user_model.User, error) {
2023-07-04 20:36:08 +02:00
token, err := jwt.ParseWithClaims(tokenSHA, &Claims{}, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
2018-01-27 17:48:15 +01:00
}
return setting.LFS.JWTSecretBytes, nil
})
if err != nil {
2025-10-21 02:43:08 +08:00
return nil, errors.New("invalid token")
2016-12-26 02:16:37 +01:00
}
claims, claimsOk := token.Claims.(*Claims)
if !token.Valid || !claimsOk {
2025-04-01 12:14:01 +02:00
return nil, errors.New("invalid token claim")
2018-01-27 17:48:15 +01:00
}
if claims.RepoID != target.ID {
2025-04-01 12:14:01 +02:00
return nil, errors.New("invalid token claim")
2016-12-26 02:16:37 +01:00
}
if mode == perm_model.AccessModeWrite && claims.Op != "upload" {
2025-04-01 12:14:01 +02:00
return nil, errors.New("invalid token claim")
2016-12-26 02:16:37 +01:00
}
u, err := user_model.GetUserByID(ctx, claims.UserID)
if err != nil {
log.Error("Unable to GetUserById[%d]: Error: %v", claims.UserID, err)
return nil, err
}
if !u.IsActive || u.ProhibitLogin {
return nil, util.NewPermissionDeniedErrorf("not allowed to access any repository")
}
perm, err := access_model.GetDoerRepoPermission(ctx, target, u)
if err != nil {
log.Error("Unable to GetDoerRepoPermission for user[%d] repo[%d]: %v", claims.UserID, target.ID, err)
return nil, err
}
if !perm.CanAccess(mode, unit.TypeCode) {
return nil, util.NewPermissionDeniedErrorf("no permission to access the repository")
}
return u, nil
}
func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Repository, mode perm_model.AccessMode) (*user_model.User, error) {
if authorization == "" {
2025-04-01 12:14:01 +02:00
return nil, errors.New("no token")
}
2025-07-03 11:02:38 +08:00
parsed, ok := httpauth.ParseAuthorizationHeader(authorization)
if !ok || parsed.BearerToken == nil {
return nil, errors.New("token not found")
}
2025-07-03 11:02:38 +08:00
return handleLFSToken(ctx, parsed.BearerToken.Token, target, mode)
2016-12-26 02:16:37 +01:00
}
func requireAuth(ctx *context.Context) {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
2021-06-06 01:59:27 +02:00
writeStatus(ctx, http.StatusUnauthorized)
2016-12-26 02:16:37 +01:00
}