mirror of
https://github.com/go-gitea/gitea
synced 2024-12-22 16:44:26 +00:00
580e21dd2e
Gitea instance keeps reporting a lot of errors like "LFS SSH transfer connection denied, pure SSH protocol is disabled". When starting debugging the problem, there are more problems found. Try to address most of them: * avoid unnecessary server side error logs (change `fail()` to not log them) * figure out the broken tests/user2/lfs.git (added comments) * avoid `migratePushMirrors` failure when a repository doesn't exist (ignore them) * avoid "Authorization" (internal&lfs) header conflicts, remove the tricky "swapAuth" and use "X-Gitea-Internal-Auth" * make internal token comparing constant time (it wasn't a serous problem because in a real world it's nearly impossible to timing-attack the token, but good to fix and backport) * avoid duplicate routers (introduce AddOwnerRepoGitLFSRoutes) * avoid "internal (private)" routes using session/web context (they should use private context) * fix incorrect "path" usages (use "filepath") * fix incorrect mocked route point handling (need to check func nil correctly) * split some tests from "git general tests" to "git misc tests" (to keep "git_general_test.go" simple) Still no correct result for Git LFS SSH tests. So the code is kept there (`tests/integration/git_lfs_ssh_test.go`) and a FIXME explains the details.
302 lines
9.1 KiB
Go
302 lines
9.1 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package backend
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
"code.gitea.io/gitea/modules/json"
|
|
"code.gitea.io/gitea/modules/lfs"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
|
|
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
|
)
|
|
|
|
// Version is the git-lfs-transfer protocol version number.
|
|
const Version = "1"
|
|
|
|
// Capabilities is a list of Git LFS capabilities supported by this package.
|
|
var Capabilities = []string{
|
|
"version=" + Version,
|
|
"locking",
|
|
}
|
|
|
|
var _ transfer.Backend = &GiteaBackend{}
|
|
|
|
// GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API
|
|
type GiteaBackend struct {
|
|
ctx context.Context
|
|
server *url.URL
|
|
op string
|
|
authToken string
|
|
internalAuth string
|
|
logger transfer.Logger
|
|
}
|
|
|
|
func New(ctx context.Context, repo, op, token string, logger transfer.Logger) (transfer.Backend, error) {
|
|
// runServ guarantees repo will be in form [owner]/[name].git
|
|
server, err := url.Parse(setting.LocalURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
server = server.JoinPath("api/internal/repo", repo, "info/lfs")
|
|
return &GiteaBackend{ctx: ctx, server: server, op: op, authToken: token, internalAuth: fmt.Sprintf("Bearer %s", setting.InternalToken), logger: logger}, nil
|
|
}
|
|
|
|
// Batch implements transfer.Backend
|
|
func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args transfer.Args) ([]transfer.BatchItem, error) {
|
|
reqBody := lfs.BatchRequest{Operation: g.op}
|
|
if transfer, ok := args[argTransfer]; ok {
|
|
reqBody.Transfers = []string{transfer}
|
|
}
|
|
if ref, ok := args[argRefname]; ok {
|
|
reqBody.Ref = &lfs.Reference{Name: ref}
|
|
}
|
|
reqBody.Objects = make([]lfs.Pointer, len(pointers))
|
|
for i := range pointers {
|
|
reqBody.Objects[i].Oid = pointers[i].Oid
|
|
reqBody.Objects[i].Size = pointers[i].Size
|
|
}
|
|
|
|
bodyBytes, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
g.logger.Log("json marshal error", err)
|
|
return nil, err
|
|
}
|
|
url := g.server.JoinPath("objects/batch").String()
|
|
headers := map[string]string{
|
|
headerAuthorization: g.authToken,
|
|
headerGiteaInternalAuth: g.internalAuth,
|
|
headerAccept: mimeGitLFS,
|
|
headerContentType: mimeGitLFS,
|
|
}
|
|
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
|
|
resp, err := req.Response()
|
|
if err != nil {
|
|
g.logger.Log("http request error", err)
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
|
|
return nil, statusCodeToErr(resp.StatusCode)
|
|
}
|
|
defer resp.Body.Close()
|
|
respBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
g.logger.Log("http read error", err)
|
|
return nil, err
|
|
}
|
|
var respBody lfs.BatchResponse
|
|
err = json.Unmarshal(respBytes, &respBody)
|
|
if err != nil {
|
|
g.logger.Log("json umarshal error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// rebuild slice, we can't rely on order in resp being the same as req
|
|
pointers = pointers[:0]
|
|
opNum := opMap[g.op]
|
|
for _, obj := range respBody.Objects {
|
|
pointer := transfer.Pointer{Oid: obj.Pointer.Oid, Size: obj.Pointer.Size}
|
|
item := transfer.BatchItem{Pointer: pointer, Args: map[string]string{}}
|
|
switch opNum {
|
|
case opDownload:
|
|
if action, ok := obj.Actions[actionDownload]; ok {
|
|
item.Present = true
|
|
idMap := obj.Actions
|
|
idMapBytes, err := json.Marshal(idMap)
|
|
if err != nil {
|
|
g.logger.Log("json marshal error", err)
|
|
return nil, err
|
|
}
|
|
idMapStr := base64.StdEncoding.EncodeToString(idMapBytes)
|
|
item.Args[argID] = idMapStr
|
|
if authHeader, ok := action.Header[headerAuthorization]; ok {
|
|
authHeaderB64 := base64.StdEncoding.EncodeToString([]byte(authHeader))
|
|
item.Args[argToken] = authHeaderB64
|
|
}
|
|
if action.ExpiresAt != nil {
|
|
item.Args[argExpiresAt] = action.ExpiresAt.String()
|
|
}
|
|
} else {
|
|
// must be an error, but the SSH protocol can't propagate individual errors
|
|
g.logger.Log("object not found", obj.Pointer.Oid, obj.Pointer.Size)
|
|
item.Present = false
|
|
}
|
|
case opUpload:
|
|
if action, ok := obj.Actions[actionUpload]; ok {
|
|
item.Present = false
|
|
idMap := obj.Actions
|
|
idMapBytes, err := json.Marshal(idMap)
|
|
if err != nil {
|
|
g.logger.Log("json marshal error", err)
|
|
return nil, err
|
|
}
|
|
idMapStr := base64.StdEncoding.EncodeToString(idMapBytes)
|
|
item.Args[argID] = idMapStr
|
|
if authHeader, ok := action.Header[headerAuthorization]; ok {
|
|
authHeaderB64 := base64.StdEncoding.EncodeToString([]byte(authHeader))
|
|
item.Args[argToken] = authHeaderB64
|
|
}
|
|
if action.ExpiresAt != nil {
|
|
item.Args[argExpiresAt] = action.ExpiresAt.String()
|
|
}
|
|
} else {
|
|
item.Present = true
|
|
}
|
|
}
|
|
pointers = append(pointers, item)
|
|
}
|
|
return pointers, nil
|
|
}
|
|
|
|
// Download implements transfer.Backend. The returned reader must be closed by the
|
|
// caller.
|
|
func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, int64, error) {
|
|
idMapStr, exists := args[argID]
|
|
if !exists {
|
|
return nil, 0, ErrMissingID
|
|
}
|
|
idMapBytes, err := base64.StdEncoding.DecodeString(idMapStr)
|
|
if err != nil {
|
|
g.logger.Log("base64 decode error", err)
|
|
return nil, 0, transfer.ErrCorruptData
|
|
}
|
|
idMap := map[string]*lfs.Link{}
|
|
err = json.Unmarshal(idMapBytes, &idMap)
|
|
if err != nil {
|
|
g.logger.Log("json unmarshal error", err)
|
|
return nil, 0, transfer.ErrCorruptData
|
|
}
|
|
action, exists := idMap[actionDownload]
|
|
if !exists {
|
|
g.logger.Log("argument id incorrect")
|
|
return nil, 0, transfer.ErrCorruptData
|
|
}
|
|
url := action.Href
|
|
headers := map[string]string{
|
|
headerAuthorization: g.authToken,
|
|
headerGiteaInternalAuth: g.internalAuth,
|
|
headerAccept: mimeOctetStream,
|
|
}
|
|
req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
|
|
resp, err := req.Response()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, 0, statusCodeToErr(resp.StatusCode)
|
|
}
|
|
defer resp.Body.Close()
|
|
respBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
respSize := int64(len(respBytes))
|
|
respBuf := io.NopCloser(bytes.NewBuffer(respBytes))
|
|
return respBuf, respSize, nil
|
|
}
|
|
|
|
// StartUpload implements transfer.Backend.
|
|
func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer.Args) error {
|
|
idMapStr, exists := args[argID]
|
|
if !exists {
|
|
return ErrMissingID
|
|
}
|
|
idMapBytes, err := base64.StdEncoding.DecodeString(idMapStr)
|
|
if err != nil {
|
|
g.logger.Log("base64 decode error", err)
|
|
return transfer.ErrCorruptData
|
|
}
|
|
idMap := map[string]*lfs.Link{}
|
|
err = json.Unmarshal(idMapBytes, &idMap)
|
|
if err != nil {
|
|
g.logger.Log("json unmarshal error", err)
|
|
return transfer.ErrCorruptData
|
|
}
|
|
action, exists := idMap[actionUpload]
|
|
if !exists {
|
|
g.logger.Log("argument id incorrect")
|
|
return transfer.ErrCorruptData
|
|
}
|
|
url := action.Href
|
|
headers := map[string]string{
|
|
headerAuthorization: g.authToken,
|
|
headerGiteaInternalAuth: g.internalAuth,
|
|
headerContentType: mimeOctetStream,
|
|
headerContentLength: strconv.FormatInt(size, 10),
|
|
}
|
|
reqBytes, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req := newInternalRequest(g.ctx, url, http.MethodPut, headers, reqBytes)
|
|
resp, err := req.Response()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return statusCodeToErr(resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Verify implements transfer.Backend.
|
|
func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (transfer.Status, error) {
|
|
reqBody := lfs.Pointer{Oid: oid, Size: size}
|
|
|
|
bodyBytes, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return transfer.NewStatus(transfer.StatusInternalServerError), err
|
|
}
|
|
idMapStr, exists := args[argID]
|
|
if !exists {
|
|
return transfer.NewStatus(transfer.StatusBadRequest, "missing argument: id"), ErrMissingID
|
|
}
|
|
idMapBytes, err := base64.StdEncoding.DecodeString(idMapStr)
|
|
if err != nil {
|
|
g.logger.Log("base64 decode error", err)
|
|
return transfer.NewStatus(transfer.StatusBadRequest, "corrupt argument: id"), transfer.ErrCorruptData
|
|
}
|
|
idMap := map[string]*lfs.Link{}
|
|
err = json.Unmarshal(idMapBytes, &idMap)
|
|
if err != nil {
|
|
g.logger.Log("json unmarshal error", err)
|
|
return transfer.NewStatus(transfer.StatusBadRequest, "corrupt argument: id"), transfer.ErrCorruptData
|
|
}
|
|
action, exists := idMap[actionVerify]
|
|
if !exists {
|
|
// the server sent no verify action
|
|
return transfer.SuccessStatus(), nil
|
|
}
|
|
url := action.Href
|
|
headers := map[string]string{
|
|
headerAuthorization: g.authToken,
|
|
headerGiteaInternalAuth: g.internalAuth,
|
|
headerAccept: mimeGitLFS,
|
|
headerContentType: mimeGitLFS,
|
|
}
|
|
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
|
|
resp, err := req.Response()
|
|
if err != nil {
|
|
return transfer.NewStatus(transfer.StatusInternalServerError), err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return transfer.NewStatus(uint32(resp.StatusCode), http.StatusText(resp.StatusCode)), statusCodeToErr(resp.StatusCode)
|
|
}
|
|
return transfer.SuccessStatus(), nil
|
|
}
|
|
|
|
// LockBackend implements transfer.Backend.
|
|
func (g *GiteaBackend) LockBackend(_ transfer.Args) transfer.LockBackend {
|
|
return newGiteaLockBackend(g)
|
|
}
|