aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--modules/httplib/request.go7
-rw-r--r--modules/lfstransfer/backend/backend.go12
-rw-r--r--modules/lfstransfer/backend/lock.go13
-rw-r--r--modules/lfstransfer/backend/util.go52
-rw-r--r--modules/lfstransfer/backend/util_test.go54
-rw-r--r--modules/private/internal.go4
-rw-r--r--tests/integration/git_lfs_ssh_test.go7
-rw-r--r--tests/mssql.ini.tmpl1
-rw-r--r--tests/mysql.ini.tmpl1
-rw-r--r--tests/pgsql.ini.tmpl1
-rw-r--r--tests/sqlite.ini.tmpl1
11 files changed, 132 insertions, 21 deletions
diff --git a/modules/httplib/request.go b/modules/httplib/request.go
index 267e276df3..5e40922896 100644
--- a/modules/httplib/request.go
+++ b/modules/httplib/request.go
@@ -8,6 +8,7 @@ import (
"bytes"
"context"
"crypto/tls"
+ "errors"
"fmt"
"io"
"net"
@@ -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:
@@ -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()
}
diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go
index 540932b930..1328d93a48 100644
--- a/modules/lfstransfer/backend/backend.go
+++ b/modules/lfstransfer/backend/backend.go
@@ -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)
@@ -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)
@@ -225,7 +223,6 @@ 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,
@@ -233,7 +230,7 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer
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 {
@@ -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
diff --git a/modules/lfstransfer/backend/lock.go b/modules/lfstransfer/backend/lock.go
index 4b45658611..639f8b184e 100644
--- a/modules/lfstransfer/backend/lock.go
+++ b/modules/lfstransfer/backend/lock.go
@@ -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)
@@ -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)
@@ -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)
diff --git a/modules/lfstransfer/backend/util.go b/modules/lfstransfer/backend/util.go
index f322d54257..98ce0b1e62 100644
--- a/modules/lfstransfer/backend/util.go
+++ b/modules/lfstransfer/backend/util.go
@@ -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"
)
@@ -57,8 +61,7 @@ const (
// Operations enum
const (
- opNone = iota
- opDownload
+ opDownload = iota + 1
opUpload
)
@@ -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)
}
diff --git a/modules/lfstransfer/backend/util_test.go b/modules/lfstransfer/backend/util_test.go
new file mode 100644
index 0000000000..0f6d7af803
--- /dev/null
+++ b/modules/lfstransfer/backend/util_test.go
@@ -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)
+ }
+}
diff --git a/modules/private/internal.go b/modules/private/internal.go
index 3bd4eb06b1..35eed1d608 100644
--- a/modules/private/internal.go
+++ b/modules/private/internal.go
@@ -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()).
diff --git a/tests/integration/git_lfs_ssh_test.go b/tests/integration/git_lfs_ssh_test.go
index 9cb7fd089b..c6011523a8 100644
--- a/tests/integration/git_lfs_ssh_test.go
+++ b/tests/integration/git_lfs_ssh_test.go
@@ -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)
})
}
diff --git a/tests/mssql.ini.tmpl b/tests/mssql.ini.tmpl
index 8b1593a396..0c5bcd4977 100644
--- a/tests/mssql.ini.tmpl
+++ b/tests/mssql.ini.tmpl
@@ -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
diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl
index 8cafba9591..5778cc5867 100644
--- a/tests/mysql.ini.tmpl
+++ b/tests/mysql.ini.tmpl
@@ -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
diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl
index fb881d411c..1febdf930e 100644
--- a/tests/pgsql.ini.tmpl
+++ b/tests/pgsql.ini.tmpl
@@ -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
diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl
index c0b3e1c859..ce1b65d02c 100644
--- a/tests/sqlite.ini.tmpl
+++ b/tests/sqlite.ini.tmpl
@@ -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