diff options
-rw-r--r-- | modules/httplib/request.go | 7 | ||||
-rw-r--r-- | modules/lfstransfer/backend/backend.go | 12 | ||||
-rw-r--r-- | modules/lfstransfer/backend/lock.go | 13 | ||||
-rw-r--r-- | modules/lfstransfer/backend/util.go | 52 | ||||
-rw-r--r-- | modules/lfstransfer/backend/util_test.go | 54 | ||||
-rw-r--r-- | modules/private/internal.go | 4 | ||||
-rw-r--r-- | tests/integration/git_lfs_ssh_test.go | 7 | ||||
-rw-r--r-- | tests/mssql.ini.tmpl | 1 | ||||
-rw-r--r-- | tests/mysql.ini.tmpl | 1 | ||||
-rw-r--r-- | tests/pgsql.ini.tmpl | 1 | ||||
-rw-r--r-- | tests/sqlite.ini.tmpl | 1 |
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 |