Skip to content

Commit 580e21d

Browse files
authored
Refactor LFS SSH and internal routers (#32473)
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.
1 parent f35e2b0 commit 580e21d

File tree

17 files changed

+376
-264
lines changed

17 files changed

+376
-264
lines changed

cmd/serv.go

+6-8
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,10 @@ func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error
111111
if !setting.IsProd {
112112
_, _ = fmt.Fprintln(os.Stderr, "Gitea:", logMsg)
113113
}
114-
if userMessage != "" {
115-
if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) {
116-
logMsg = userMessage + " " + logMsg
117-
} else {
118-
logMsg = userMessage + ". " + logMsg
119-
}
114+
if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) {
115+
logMsg = userMessage + " " + logMsg
116+
} else {
117+
logMsg = userMessage + ". " + logMsg
120118
}
121119
_ = private.SSHLog(ctx, true, logMsg)
122120
}
@@ -288,10 +286,10 @@ func runServ(c *cli.Context) error {
288286
if allowedCommands.Contains(verb) {
289287
if allowedCommandsLfs.Contains(verb) {
290288
if !setting.LFS.StartServer {
291-
return fail(ctx, "Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled")
289+
return fail(ctx, "LFS Server is not enabled", "")
292290
}
293291
if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH {
294-
return fail(ctx, "Unknown git command", "LFS SSH transfer connection denied, pure SSH protocol is disabled")
292+
return fail(ctx, "LFS SSH transfer is not enabled", "")
295293
}
296294
if len(words) > 2 {
297295
lfsVerb = words[2]

models/fixtures/lfs_meta_object.yml

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
# These are the LFS objects in user2/lfs.git
2+
# user2/lfs is an INVALID repository
3+
#
4+
# commit e9c32647bab825977942598c0efa415de300304b (HEAD -> master)
5+
# Author: Rowan Bohde <[email protected]>
6+
# Date: Thu Aug 1 14:38:23 2024 -0500
7+
#
8+
# add invalid lfs file
29
-
310

411
id: 1
@@ -11,7 +18,7 @@
1118

1219
id: 2
1320
oid: 2eccdb43825d2a49d99d542daa20075cff1d97d9d2349a8977efe9c03661737c
14-
size: 107
21+
size: 107 # real size is 2048
1522
repository_id: 54
1623
created_unix: 1671607299
1724

@@ -30,3 +37,12 @@
3037
size: 25
3138
repository_id: 54
3239
created_unix: 1671607299
40+
41+
# this file is missing
42+
# -
43+
#
44+
# id: 5
45+
# oid: 9d178b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351
46+
# size: 25
47+
# repository_id: 54
48+
# created_unix: 1671607299

models/migrations/v1_21/v276.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"code.gitea.io/gitea/modules/git"
1313
giturl "code.gitea.io/gitea/modules/git/url"
1414
"code.gitea.io/gitea/modules/setting"
15+
"code.gitea.io/gitea/modules/util"
1516

1617
"xorm.io/xorm"
1718
)
@@ -163,7 +164,9 @@ func migratePushMirrors(x *xorm.Engine) error {
163164

164165
func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
165166
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git")
166-
167+
if exist, _ := util.IsExist(repoPath); !exist {
168+
return "", nil
169+
}
167170
remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName)
168171
if err != nil {
169172
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)

modules/git/batch_reader.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,8 @@ func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi
146146
}
147147

148148
// ReadBatchLine reads the header line from cat-file --batch
149-
// We expect:
150-
// <sha> SP <type> SP <size> LF
151-
// sha is a hex encoded here
149+
// We expect: <oid> SP <type> SP <size> LF
150+
// then leaving the rest of the stream "<contents> LF" to be read
152151
func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
153152
typ, err = rd.ReadString('\n')
154153
if err != nil {

modules/lfstransfer/backend/backend.go

+24-24
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ var _ transfer.Backend = &GiteaBackend{}
3333

3434
// GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API
3535
type GiteaBackend struct {
36-
ctx context.Context
37-
server *url.URL
38-
op string
39-
token string
40-
itoken string
41-
logger transfer.Logger
36+
ctx context.Context
37+
server *url.URL
38+
op string
39+
authToken string
40+
internalAuth string
41+
logger transfer.Logger
4242
}
4343

4444
func New(ctx context.Context, repo, op, token string, logger transfer.Logger) (transfer.Backend, error) {
@@ -48,7 +48,7 @@ func New(ctx context.Context, repo, op, token string, logger transfer.Logger) (t
4848
return nil, err
4949
}
5050
server = server.JoinPath("api/internal/repo", repo, "info/lfs")
51-
return &GiteaBackend{ctx: ctx, server: server, op: op, token: token, itoken: fmt.Sprintf("Bearer %s", setting.InternalToken), logger: logger}, nil
51+
return &GiteaBackend{ctx: ctx, server: server, op: op, authToken: token, internalAuth: fmt.Sprintf("Bearer %s", setting.InternalToken), logger: logger}, nil
5252
}
5353

5454
// Batch implements transfer.Backend
@@ -73,10 +73,10 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
7373
}
7474
url := g.server.JoinPath("objects/batch").String()
7575
headers := map[string]string{
76-
headerAuthorisation: g.itoken,
77-
headerAuthX: g.token,
78-
headerAccept: mimeGitLFS,
79-
headerContentType: mimeGitLFS,
76+
headerAuthorization: g.authToken,
77+
headerGiteaInternalAuth: g.internalAuth,
78+
headerAccept: mimeGitLFS,
79+
headerContentType: mimeGitLFS,
8080
}
8181
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
8282
resp, err := req.Response()
@@ -119,7 +119,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
119119
}
120120
idMapStr := base64.StdEncoding.EncodeToString(idMapBytes)
121121
item.Args[argID] = idMapStr
122-
if authHeader, ok := action.Header[headerAuthorisation]; ok {
122+
if authHeader, ok := action.Header[headerAuthorization]; ok {
123123
authHeaderB64 := base64.StdEncoding.EncodeToString([]byte(authHeader))
124124
item.Args[argToken] = authHeaderB64
125125
}
@@ -142,7 +142,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
142142
}
143143
idMapStr := base64.StdEncoding.EncodeToString(idMapBytes)
144144
item.Args[argID] = idMapStr
145-
if authHeader, ok := action.Header[headerAuthorisation]; ok {
145+
if authHeader, ok := action.Header[headerAuthorization]; ok {
146146
authHeaderB64 := base64.StdEncoding.EncodeToString([]byte(authHeader))
147147
item.Args[argToken] = authHeaderB64
148148
}
@@ -183,9 +183,9 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser,
183183
}
184184
url := action.Href
185185
headers := map[string]string{
186-
headerAuthorisation: g.itoken,
187-
headerAuthX: g.token,
188-
headerAccept: mimeOctetStream,
186+
headerAuthorization: g.authToken,
187+
headerGiteaInternalAuth: g.internalAuth,
188+
headerAccept: mimeOctetStream,
189189
}
190190
req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
191191
resp, err := req.Response()
@@ -229,10 +229,10 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer
229229
}
230230
url := action.Href
231231
headers := map[string]string{
232-
headerAuthorisation: g.itoken,
233-
headerAuthX: g.token,
234-
headerContentType: mimeOctetStream,
235-
headerContentLength: strconv.FormatInt(size, 10),
232+
headerAuthorization: g.authToken,
233+
headerGiteaInternalAuth: g.internalAuth,
234+
headerContentType: mimeOctetStream,
235+
headerContentLength: strconv.FormatInt(size, 10),
236236
}
237237
reqBytes, err := io.ReadAll(r)
238238
if err != nil {
@@ -279,10 +279,10 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans
279279
}
280280
url := action.Href
281281
headers := map[string]string{
282-
headerAuthorisation: g.itoken,
283-
headerAuthX: g.token,
284-
headerAccept: mimeGitLFS,
285-
headerContentType: mimeGitLFS,
282+
headerAuthorization: g.authToken,
283+
headerGiteaInternalAuth: g.internalAuth,
284+
headerAccept: mimeGitLFS,
285+
headerContentType: mimeGitLFS,
286286
}
287287
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
288288
resp, err := req.Response()

modules/lfstransfer/backend/lock.go

+19-19
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@ import (
2121
var _ transfer.LockBackend = &giteaLockBackend{}
2222

2323
type giteaLockBackend struct {
24-
ctx context.Context
25-
g *GiteaBackend
26-
server *url.URL
27-
token string
28-
itoken string
29-
logger transfer.Logger
24+
ctx context.Context
25+
g *GiteaBackend
26+
server *url.URL
27+
authToken string
28+
internalAuth string
29+
logger transfer.Logger
3030
}
3131

3232
func newGiteaLockBackend(g *GiteaBackend) transfer.LockBackend {
3333
server := g.server.JoinPath("locks")
34-
return &giteaLockBackend{ctx: g.ctx, g: g, server: server, token: g.token, itoken: g.itoken, logger: g.logger}
34+
return &giteaLockBackend{ctx: g.ctx, g: g, server: server, authToken: g.authToken, internalAuth: g.internalAuth, logger: g.logger}
3535
}
3636

3737
// Create implements transfer.LockBackend
@@ -45,10 +45,10 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) {
4545
}
4646
url := g.server.String()
4747
headers := map[string]string{
48-
headerAuthorisation: g.itoken,
49-
headerAuthX: g.token,
50-
headerAccept: mimeGitLFS,
51-
headerContentType: mimeGitLFS,
48+
headerAuthorization: g.authToken,
49+
headerGiteaInternalAuth: g.internalAuth,
50+
headerAccept: mimeGitLFS,
51+
headerContentType: mimeGitLFS,
5252
}
5353
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
5454
resp, err := req.Response()
@@ -97,10 +97,10 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error {
9797
}
9898
url := g.server.JoinPath(lock.ID(), "unlock").String()
9999
headers := map[string]string{
100-
headerAuthorisation: g.itoken,
101-
headerAuthX: g.token,
102-
headerAccept: mimeGitLFS,
103-
headerContentType: mimeGitLFS,
100+
headerAuthorization: g.authToken,
101+
headerGiteaInternalAuth: g.internalAuth,
102+
headerAccept: mimeGitLFS,
103+
headerContentType: mimeGitLFS,
104104
}
105105
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
106106
resp, err := req.Response()
@@ -180,10 +180,10 @@ func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, er
180180
urlq.RawQuery = v.Encode()
181181
url := urlq.String()
182182
headers := map[string]string{
183-
headerAuthorisation: g.itoken,
184-
headerAuthX: g.token,
185-
headerAccept: mimeGitLFS,
186-
headerContentType: mimeGitLFS,
183+
headerAuthorization: g.authToken,
184+
headerGiteaInternalAuth: g.internalAuth,
185+
headerAccept: mimeGitLFS,
186+
headerContentType: mimeGitLFS,
187187
}
188188
req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
189189
resp, err := req.Response()

modules/lfstransfer/backend/util.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ import (
2020

2121
// HTTP headers
2222
const (
23-
headerAccept = "Accept"
24-
headerAuthorisation = "Authorization"
25-
headerAuthX = "X-Auth"
26-
headerContentType = "Content-Type"
27-
headerContentLength = "Content-Length"
23+
headerAccept = "Accept"
24+
headerAuthorization = "Authorization"
25+
headerGiteaInternalAuth = "X-Gitea-Internal-Auth"
26+
headerContentType = "Content-Type"
27+
headerContentLength = "Content-Length"
2828
)
2929

3030
// MIME types

modules/private/internal.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Ensure you are running in the correct environment or set the correct configurati
4343
req := httplib.NewRequest(url, method).
4444
SetContext(ctx).
4545
Header("X-Real-IP", getClientIP()).
46-
Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)).
46+
Header("X-Gitea-Internal-Auth", fmt.Sprintf("Bearer %s", setting.InternalToken)).
4747
SetTLSClientConfig(&tls.Config{
4848
InsecureSkipVerify: true,
4949
ServerName: setting.Domain,

modules/web/route.go

+11-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package web
66
import (
77
"net/http"
88
"net/url"
9+
"reflect"
910
"strings"
1011

1112
"code.gitea.io/gitea/modules/setting"
@@ -82,15 +83,23 @@ func (r *Router) getPattern(pattern string) string {
8283
return strings.TrimSuffix(newPattern, "/")
8384
}
8485

86+
func isNilOrFuncNil(v any) bool {
87+
if v == nil {
88+
return true
89+
}
90+
r := reflect.ValueOf(v)
91+
return r.Kind() == reflect.Func && r.IsNil()
92+
}
93+
8594
func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) {
8695
handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1)
8796
for _, m := range r.curMiddlewares {
88-
if m != nil {
97+
if !isNilOrFuncNil(m) {
8998
handlerProviders = append(handlerProviders, toHandlerProvider(m))
9099
}
91100
}
92101
for _, m := range h {
93-
if h != nil {
102+
if !isNilOrFuncNil(m) {
94103
handlerProviders = append(handlerProviders, toHandlerProvider(m))
95104
}
96105
}

routers/common/lfs.go

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package common
5+
6+
import (
7+
"net/http"
8+
9+
"code.gitea.io/gitea/modules/web"
10+
"code.gitea.io/gitea/services/lfs"
11+
)
12+
13+
func AddOwnerRepoGitLFSRoutes(m *web.Router, middlewares ...any) {
14+
// shared by web and internal routers
15+
m.Group("/{username}/{reponame}/info/lfs", func() {
16+
m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler)
17+
m.Put("/objects/{oid}/{size}", lfs.UploadHandler)
18+
m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler)
19+
m.Get("/objects/{oid}", lfs.DownloadHandler)
20+
m.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler)
21+
m.Group("/locks", func() {
22+
m.Get("/", lfs.GetListLockHandler)
23+
m.Post("/", lfs.PostLockHandler)
24+
m.Post("/verify", lfs.VerifyLockHandler)
25+
m.Post("/{lid}/unlock", lfs.UnLockHandler)
26+
}, lfs.CheckAcceptMediaType)
27+
m.Any("/*", http.NotFound)
28+
}, middlewares...)
29+
}

0 commit comments

Comments
 (0)