Skip to content

Commit

Permalink
Fix LFS URL (#33840) (#33843)
Browse files Browse the repository at this point in the history
Backport #33840 by wxiaoguang

Fix #33839

---------

Co-authored-by: wxiaoguang <[email protected]>
  • Loading branch information
GiteaBot and wxiaoguang authored Mar 10, 2025
1 parent 523a84e commit a2c6ecc
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 21 deletions.
7 changes: 7 additions & 0 deletions modules/httplib/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
Expand Down Expand Up @@ -101,6 +102,9 @@ func (r *Request) Param(key, value string) *Request {

// Body adds request raw body. It supports string, []byte and io.Reader as body.
func (r *Request) Body(data any) *Request {
if r == nil {
return nil
}
switch t := data.(type) {
case nil: // do nothing
case string:
Expand Down Expand Up @@ -193,6 +197,9 @@ func (r *Request) getResponse() (*http.Response, error) {
// Response executes request client gets response manually.
// Caller MUST close the response body if no error occurs
func (r *Request) Response() (*http.Response, error) {
if r == nil {
return nil, errors.New("invalid request")
}
return r.getResponse()
}

Expand Down
12 changes: 4 additions & 8 deletions modules/lfstransfer/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,13 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
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 := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
req := newInternalRequestLFS(g.ctx, g.server.JoinPath("objects/batch").String(), http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
if err != nil {
g.logger.Log("http request error", err)
Expand Down Expand Up @@ -179,13 +178,12 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser,
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 := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil)
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodGet, headers, nil)
resp, err := req.Response()
if err != nil {
return nil, 0, fmt.Errorf("failed to get response: %w", err)
Expand Down Expand Up @@ -225,15 +223,14 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer
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),
}

req := newInternalRequestLFS(g.ctx, url, http.MethodPut, headers, nil)
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPut, headers, nil)
req.Body(r)
resp, err := req.Response()
if err != nil {
Expand Down Expand Up @@ -274,14 +271,13 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans
// 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 := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
if err != nil {
return transfer.NewStatus(transfer.StatusInternalServerError), err
Expand Down
13 changes: 5 additions & 8 deletions modules/lfstransfer/backend/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,13 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) {
g.logger.Log("json marshal error", err)
return nil, err
}
url := g.server.String()
headers := map[string]string{
headerAuthorization: g.authToken,
headerGiteaInternalAuth: g.internalAuth,
headerAccept: mimeGitLFS,
headerContentType: mimeGitLFS,
}
req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
req := newInternalRequestLFS(g.ctx, g.server.String(), http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
if err != nil {
g.logger.Log("http request error", err)
Expand Down Expand Up @@ -95,14 +94,13 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error {
g.logger.Log("json marshal error", err)
return err
}
url := g.server.JoinPath(lock.ID(), "unlock").String()
headers := map[string]string{
headerAuthorization: g.authToken,
headerGiteaInternalAuth: g.internalAuth,
headerAccept: mimeGitLFS,
headerContentType: mimeGitLFS,
}
req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
req := newInternalRequestLFS(g.ctx, g.server.JoinPath(lock.ID(), "unlock").String(), http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
if err != nil {
g.logger.Log("http request error", err)
Expand Down Expand Up @@ -176,16 +174,15 @@ func (g *giteaLockBackend) Range(cursor string, limit int, iter func(transfer.Lo
}

func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, error) {
urlq := g.server.JoinPath() // get a copy
urlq.RawQuery = v.Encode()
url := urlq.String()
serverURLWithQuery := g.server.JoinPath() // get a copy
serverURLWithQuery.RawQuery = v.Encode()
headers := map[string]string{
headerAuthorization: g.authToken,
headerGiteaInternalAuth: g.internalAuth,
headerAccept: mimeGitLFS,
headerContentType: mimeGitLFS,
}
req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil)
req := newInternalRequestLFS(g.ctx, serverURLWithQuery.String(), http.MethodGet, headers, nil)
resp, err := req.Response()
if err != nil {
g.logger.Log("http request error", err)
Expand Down
52 changes: 48 additions & 4 deletions modules/lfstransfer/backend/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"

"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"

"github.com/charmbracelet/git-lfs-transfer/transfer"
)
Expand Down Expand Up @@ -57,8 +61,7 @@ const (

// Operations enum
const (
opNone = iota
opDownload
opDownload = iota + 1
opUpload
)

Expand Down Expand Up @@ -86,8 +89,49 @@ func statusCodeToErr(code int) error {
}
}

func newInternalRequestLFS(ctx context.Context, url, method string, headers map[string]string, body any) *httplib.Request {
req := private.NewInternalRequest(ctx, url, method)
func toInternalLFSURL(s string) string {
pos1 := strings.Index(s, "://")
if pos1 == -1 {
return ""
}
appSubURLWithSlash := setting.AppSubURL + "/"
pos2 := strings.Index(s[pos1+3:], appSubURLWithSlash)
if pos2 == -1 {
return ""
}
routePath := s[pos1+3+pos2+len(appSubURLWithSlash):]
fields := strings.SplitN(routePath, "/", 3)
if len(fields) < 3 || !strings.HasPrefix(fields[2], "info/lfs") {
return ""
}
return setting.LocalURL + "api/internal/repo/" + routePath
}

func isInternalLFSURL(s string) bool {
if !strings.HasPrefix(s, setting.LocalURL) {
return false
}
u, err := url.Parse(s)
if err != nil {
return false
}
routePath := util.PathJoinRelX(u.Path)
subRoutePath, cut := strings.CutPrefix(routePath, "api/internal/repo/")
if !cut {
return false
}
fields := strings.SplitN(subRoutePath, "/", 3)
if len(fields) < 3 || !strings.HasPrefix(fields[2], "info/lfs") {
return false
}
return true
}

func newInternalRequestLFS(ctx context.Context, internalURL, method string, headers map[string]string, body any) *httplib.Request {
if !isInternalLFSURL(internalURL) {
return nil
}
req := private.NewInternalRequest(ctx, internalURL, method)
for k, v := range headers {
req.Header(k, v)
}
Expand Down
54 changes: 54 additions & 0 deletions modules/lfstransfer/backend/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package backend

import (
"context"
"testing"

"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"

"github.com/stretchr/testify/assert"
)

func TestToInternalLFSURL(t *testing.T) {
defer test.MockVariableValue(&setting.LocalURL, "http://localurl/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
cases := []struct {
url string
expected string
}{
{"http://appurl/any", ""},
{"http://appurl/sub/any", ""},
{"http://appurl/sub/owner/repo/any", ""},
{"http://appurl/sub/owner/repo/info/any", ""},
{"http://appurl/sub/owner/repo/info/lfs/any", "http://localurl/api/internal/repo/owner/repo/info/lfs/any"},
}
for _, c := range cases {
assert.Equal(t, c.expected, toInternalLFSURL(c.url), c.url)
}
}

func TestIsInternalLFSURL(t *testing.T) {
defer test.MockVariableValue(&setting.LocalURL, "http://localurl/")()
defer test.MockVariableValue(&setting.InternalToken, "mock-token")()
cases := []struct {
url string
expected bool
}{
{"", false},
{"http://otherurl/api/internal/repo/owner/repo/info/lfs/any", false},
{"http://localurl/api/internal/repo/owner/repo/info/lfs/any", true},
{"http://localurl/api/internal/repo/owner/repo/info", false},
{"http://localurl/api/internal/misc/owner/repo/info/lfs/any", false},
{"http://localurl/api/internal/owner/repo/info/lfs/any", false},
{"http://localurl/api/internal/foo/bar", false},
}
for _, c := range cases {
req := newInternalRequestLFS(context.Background(), c.url, "GET", nil, nil)
assert.Equal(t, c.expected, req != nil, c.url)
assert.Equal(t, c.expected, isInternalLFSURL(c.url), c.url)
}
}
4 changes: 4 additions & 0 deletions modules/private/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ func NewInternalRequest(ctx context.Context, url, method string) *httplib.Reques
Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf)
}

if !strings.HasPrefix(url, setting.LocalURL) {
log.Fatal("Invalid internal request URL: %q", url)
}

req := httplib.NewRequest(url, method).
SetContext(ctx).
Header("X-Real-IP", getClientIP()).
Expand Down
7 changes: 6 additions & 1 deletion tests/integration/git_lfs_ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,14 @@ func TestGitLFSSSH(t *testing.T) {
return strings.Contains(s, "POST /api/internal/repo/user2/repo1.git/info/lfs/objects/batch")
})
countUpload := slices.ContainsFunc(routerCalls, func(s string) bool {
return strings.Contains(s, "PUT /user2/repo1.git/info/lfs/objects/")
return strings.Contains(s, "PUT /api/internal/repo/user2/repo1.git/info/lfs/objects/")
})
nonAPIRequests := slices.ContainsFunc(routerCalls, func(s string) bool {
fields := strings.Fields(s)
return !strings.HasPrefix(fields[1], "/api/")
})
assert.NotZero(t, countBatch)
assert.NotZero(t, countUpload)
assert.Zero(t, nonAPIRequests)
})
}
1 change: 1 addition & 0 deletions tests/mssql.ini.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ SIGNING_KEY = none
SSH_DOMAIN = localhost
HTTP_PORT = 3003
ROOT_URL = http://localhost:3003/
LOCAL_ROOT_URL = http://127.0.0.1:3003/
DISABLE_SSH = false
SSH_LISTEN_HOST = localhost
SSH_PORT = 2201
Expand Down
1 change: 1 addition & 0 deletions tests/mysql.ini.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ SIGNING_KEY = none
SSH_DOMAIN = localhost
HTTP_PORT = 3001
ROOT_URL = http://localhost:3001/
LOCAL_ROOT_URL = http://127.0.0.1:3001/
DISABLE_SSH = false
SSH_LISTEN_HOST = localhost
SSH_PORT = 2201
Expand Down
1 change: 1 addition & 0 deletions tests/pgsql.ini.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ SIGNING_KEY = none
SSH_DOMAIN = localhost
HTTP_PORT = 3002
ROOT_URL = http://localhost:3002/
LOCAL_ROOT_URL = http://127.0.0.1:3002/
DISABLE_SSH = false
SSH_LISTEN_HOST = localhost
SSH_PORT = 2202
Expand Down
1 change: 1 addition & 0 deletions tests/sqlite.ini.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ SIGNING_KEY = none
SSH_DOMAIN = localhost
HTTP_PORT = 3003
ROOT_URL = http://localhost:3003/
LOCAL_ROOT_URL = http://127.0.0.1:3003/
DISABLE_SSH = false
SSH_LISTEN_HOST = localhost
SSH_PORT = 2203
Expand Down

0 comments on commit a2c6ecc

Please sign in to comment.