mirror of https://github.com/go-gitea/gitea
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.pull/32430/head^2
parent
f35e2b0cd1
commit
580e21dd2e
@ -0,0 +1,29 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package common |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/web" |
||||||
|
"code.gitea.io/gitea/services/lfs" |
||||||
|
) |
||||||
|
|
||||||
|
func AddOwnerRepoGitLFSRoutes(m *web.Router, middlewares ...any) { |
||||||
|
// shared by web and internal routers
|
||||||
|
m.Group("/{username}/{reponame}/info/lfs", func() { |
||||||
|
m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler) |
||||||
|
m.Put("/objects/{oid}/{size}", lfs.UploadHandler) |
||||||
|
m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler) |
||||||
|
m.Get("/objects/{oid}", lfs.DownloadHandler) |
||||||
|
m.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler) |
||||||
|
m.Group("/locks", func() { |
||||||
|
m.Get("/", lfs.GetListLockHandler) |
||||||
|
m.Post("/", lfs.PostLockHandler) |
||||||
|
m.Post("/verify", lfs.VerifyLockHandler) |
||||||
|
m.Post("/{lid}/unlock", lfs.UnLockHandler) |
||||||
|
}, lfs.CheckAcceptMediaType) |
||||||
|
m.Any("/*", http.NotFound) |
||||||
|
}, middlewares...) |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/url" |
||||||
|
"sync" |
||||||
|
"testing" |
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/web" |
||||||
|
"code.gitea.io/gitea/routers/private" |
||||||
|
"code.gitea.io/gitea/services/context" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
) |
||||||
|
|
||||||
|
func TestGitLFSSSH(t *testing.T) { |
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) { |
||||||
|
dstPath := t.TempDir() |
||||||
|
apiTestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) |
||||||
|
|
||||||
|
var mu sync.Mutex |
||||||
|
var routerCalls []string |
||||||
|
web.RouteMock(private.RouterMockPointInternalLFS, func(ctx *context.PrivateContext) { |
||||||
|
mu.Lock() |
||||||
|
routerCalls = append(routerCalls, ctx.Req.Method+" "+ctx.Req.URL.Path) |
||||||
|
mu.Unlock() |
||||||
|
}) |
||||||
|
|
||||||
|
withKeyFile(t, "my-testing-key", func(keyFile string) { |
||||||
|
t.Run("CreateUserKey", doAPICreateUserKey(apiTestContext, "test-key", keyFile)) |
||||||
|
cloneURL := createSSHUrl(apiTestContext.GitPath(), u) |
||||||
|
t.Run("Clone", doGitClone(dstPath, cloneURL)) |
||||||
|
|
||||||
|
cfg, err := setting.CfgProvider.PrepareSaving() |
||||||
|
require.NoError(t, err) |
||||||
|
cfg.Section("server").Key("LFS_ALLOW_PURE_SSH").SetValue("true") |
||||||
|
setting.LFS.AllowPureSSH = true |
||||||
|
require.NoError(t, cfg.Save()) |
||||||
|
|
||||||
|
// do LFS SSH transfer?
|
||||||
|
lfsCommitAndPushTest(t, dstPath, 10) |
||||||
|
}) |
||||||
|
|
||||||
|
// FIXME: Here we only see the following calls, but actually there should be calls to "PUT"?
|
||||||
|
// 0 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks"
|
||||||
|
// 1 = {string} "POST /api/internal/repo/user2/repo1.git/info/lfs/objects/batch"
|
||||||
|
// 2 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks"
|
||||||
|
// 3 = {string} "POST /api/internal/repo/user2/repo1.git/info/lfs/locks"
|
||||||
|
// 4 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks"
|
||||||
|
// 5 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks"
|
||||||
|
// 6 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks"
|
||||||
|
// 7 = {string} "POST /api/internal/repo/user2/repo1.git/info/lfs/locks/24/unlock"
|
||||||
|
assert.NotEmpty(t, routerCalls) |
||||||
|
// assert.Contains(t, routerCalls, "PUT /api/internal/repo/user2/repo1.git/info/lfs/objects/....")
|
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,138 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"context" |
||||||
|
"io" |
||||||
|
"net/url" |
||||||
|
"sync" |
||||||
|
"testing" |
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth" |
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
issues_model "code.gitea.io/gitea/models/issues" |
||||||
|
repo_model "code.gitea.io/gitea/models/repo" |
||||||
|
"code.gitea.io/gitea/models/unittest" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/gitrepo" |
||||||
|
files_service "code.gitea.io/gitea/services/repository/files" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestDataAsyncDoubleRead_Issue29101(t *testing.T) { |
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) { |
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) |
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) |
||||||
|
|
||||||
|
testContent := bytes.Repeat([]byte{'a'}, 10000) |
||||||
|
resp, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{ |
||||||
|
Files: []*files_service.ChangeRepoFile{ |
||||||
|
{ |
||||||
|
Operation: "create", |
||||||
|
TreePath: "test.txt", |
||||||
|
ContentReader: bytes.NewReader(testContent), |
||||||
|
}, |
||||||
|
}, |
||||||
|
OldBranch: repo.DefaultBranch, |
||||||
|
NewBranch: repo.DefaultBranch, |
||||||
|
}) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
sha := resp.Commit.SHA |
||||||
|
|
||||||
|
gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
commit, err := gitRepo.GetCommit(sha) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
entry, err := commit.GetTreeEntryByPath("test.txt") |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
b := entry.Blob() |
||||||
|
r1, err := b.DataAsync() |
||||||
|
assert.NoError(t, err) |
||||||
|
defer r1.Close() |
||||||
|
r2, err := b.DataAsync() |
||||||
|
assert.NoError(t, err) |
||||||
|
defer r2.Close() |
||||||
|
|
||||||
|
var data1, data2 []byte |
||||||
|
wg := sync.WaitGroup{} |
||||||
|
wg.Add(2) |
||||||
|
go func() { |
||||||
|
data1, _ = io.ReadAll(r1) |
||||||
|
assert.NoError(t, err) |
||||||
|
wg.Done() |
||||||
|
}() |
||||||
|
go func() { |
||||||
|
data2, _ = io.ReadAll(r2) |
||||||
|
assert.NoError(t, err) |
||||||
|
wg.Done() |
||||||
|
}() |
||||||
|
wg.Wait() |
||||||
|
assert.Equal(t, testContent, data1) |
||||||
|
assert.Equal(t, testContent, data2) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestAgitPullPush(t *testing.T) { |
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) { |
||||||
|
baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) |
||||||
|
|
||||||
|
u.Path = baseAPITestContext.GitPath() |
||||||
|
u.User = url.UserPassword("user2", userPassword) |
||||||
|
|
||||||
|
dstPath := t.TempDir() |
||||||
|
doGitClone(dstPath, u)(t) |
||||||
|
|
||||||
|
gitRepo, err := git.OpenRepository(context.Background(), dstPath) |
||||||
|
assert.NoError(t, err) |
||||||
|
defer gitRepo.Close() |
||||||
|
|
||||||
|
doGitCreateBranch(dstPath, "test-agit-push") |
||||||
|
|
||||||
|
// commit 1
|
||||||
|
_, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
// push to create an agit pull request
|
||||||
|
err = git.NewCommand(git.DefaultContext, "push", "origin", |
||||||
|
"-o", "title=test-title", "-o", "description=test-description", |
||||||
|
"HEAD:refs/for/master/test-agit-push", |
||||||
|
).Run(&git.RunOpts{Dir: dstPath}) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
// check pull request exist
|
||||||
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Flow: issues_model.PullRequestFlowAGit, HeadBranch: "user2/test-agit-push"}) |
||||||
|
assert.NoError(t, pr.LoadIssue(db.DefaultContext)) |
||||||
|
assert.Equal(t, "test-title", pr.Issue.Title) |
||||||
|
assert.Equal(t, "test-description", pr.Issue.Content) |
||||||
|
|
||||||
|
// commit 2
|
||||||
|
_, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-2-") |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
// push 2
|
||||||
|
err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").Run(&git.RunOpts{Dir: dstPath}) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
// reset to first commit
|
||||||
|
err = git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath}) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
// test force push without confirm
|
||||||
|
_, stderr, err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").RunStdString(&git.RunOpts{Dir: dstPath}) |
||||||
|
assert.Error(t, err) |
||||||
|
assert.Contains(t, stderr, "[remote rejected] HEAD -> refs/for/master/test-agit-push (request `force-push` push option)") |
||||||
|
|
||||||
|
// test force push with confirm
|
||||||
|
err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push", "-o", "force-push").Run(&git.RunOpts{Dir: dstPath}) |
||||||
|
assert.NoError(t, err) |
||||||
|
}) |
||||||
|
} |
Loading…
Reference in new issue