From a0b9ce05c4d324db85a49e255bb1457c36d84fee Mon Sep 17 00:00:00 2001 From: Roland Singer <10167163+r0l1@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:52:24 +0200 Subject: [PATCH] fix: go get fails with GO_GET_CLONE_URL_PROTOCOL=ssh on the default SSH port (#39674) Go rejects scp-style addresses like `git@host:owner/repo.git` in the go-import meta tag because they have no URL scheme, so GO_GET_CLONE_URL_PROTOCOL=ssh did not work with the default SSH port unless USE_COMPAT_SSH_URI was set. Always use the ssh:// form for go-get. When DISABLE_HTTP_GIT is enabled, the https clone URL can never work, so GO_GET_CLONE_URL_PROTOCOL now defaults to ssh in that case. --------- Co-authored-by: wxiaoguang --- custom/conf/app.example.ini | 5 +++-- models/repo/repo.go | 11 ++++++++++- models/repo/repo_test.go | 2 ++ modules/setting/repository.go | 2 +- routers/web/goget.go | 8 +------- services/context/repo.go | 17 ++++++++++------- tests/integration/goget_test.go | 5 +++++ 7 files changed, 32 insertions(+), 18 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 716edbd4d6d..7afd67e5117 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1095,8 +1095,9 @@ LEVEL = Info ;; Force ssh:// clone url instead of scp-style uri when default SSH port is used ;USE_COMPAT_SSH_URI = false ;; -;; Value for the "go get" request returns the repository url as https or ssh, default is https -;GO_GET_CLONE_URL_PROTOCOL = https +;; Scheme of the returned URL for the "go get" response. +;; Default is "https" if DISABLE_HTTP_GIT=false or SSH is disabled, otherwise "ssh". +;GO_GET_CLONE_URL_PROTOCOL = ;; ;; Close issues as long as a commit on any branch marks it as fixed ;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false diff --git a/models/repo/repo.go b/models/repo/repo.go index 8387e429edf..55d7d2ed814 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -616,6 +616,15 @@ func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string { // ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name. func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string { + return composeSSHCloneURL(doer, ownerName, repoName, setting.Repository.UseCompatSSHURI) +} + +// ComposeSSHCloneURI is like ComposeSSHCloneURL but always returns the "ssh://" form, because "go get" rejects scp-style addresses +func ComposeSSHCloneURI(doer *user_model.User, ownerName, repoName string) string { + return composeSSHCloneURL(doer, ownerName, repoName, true) +} + +func composeSSHCloneURL(doer *user_model.User, ownerName, repoName string, useURI bool) string { sshUser := setting.SSH.User sshDomain := setting.SSH.Domain @@ -642,7 +651,7 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) strin if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil { sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets } - if setting.Repository.UseCompatSSHURI { + if useURI { return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) } return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index afef5f9cd9c..e36cfa2b517 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -185,6 +185,8 @@ func TestComposeSSHCloneURL(t *testing.T) { assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) setting.Repository.UseCompatSSHURI = true assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) + setting.Repository.UseCompatSSHURI = false + assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURI(nil, "user", "repo")) // test SSH_DOMAIN while use non-standard SSH port setting.SSH.Port = 123 setting.Repository.UseCompatSSHURI = false diff --git a/modules/setting/repository.go b/modules/setting/repository.go index a4f4c4c773c..05bf4a7274d 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -303,7 +303,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("repository") Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool() Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool() - Repository.GoGetCloneURLProtocol = sec.Key("GO_GET_CLONE_URL_PROTOCOL").MustString("https") + Repository.GoGetCloneURLProtocol = sec.Key("GO_GET_CLONE_URL_PROTOCOL").String() // MAX_CREATION_LIMIT is a shortcut that sets the default for the two per-type limits below. // USER_/ORG_MAX_CREATION_LIMIT take precedence when explicitly set. Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1) // FIXME: INI-MUST-SIDE-EFFECT diff --git a/routers/web/goget.go b/routers/web/goget.go index 922e9cc13e4..d4af7b3518f 100644 --- a/routers/web/goget.go +++ b/routers/web/goget.go @@ -69,13 +69,7 @@ func goGet(ctx *context.Context) { goGetImport := context.ComposeGoGetImport(ctx, ownerName, trimmedRepoName) - var cloneURL string - if setting.Repository.GoGetCloneURLProtocol == "ssh" { - cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName) - } else { - cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, ownerName, repoName) - } - goImportContent := fmt.Sprintf("%s git %s", goGetImport, cloneURL /*CloneLink*/) + goImportContent := fmt.Sprintf("%s git %s", goGetImport, context.ComposeGoGetCloneURL(ctx, ownerName, trimmedRepoName)) goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/) goGetCli := fmt.Sprintf("go get %s%s", insecure, goGetImport) diff --git a/services/context/repo.go b/services/context/repo.go index 8389a499142..aac5b0617a3 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -389,6 +389,15 @@ func ComposeGoGetImport(ctx context.Context, owner, repo string) string { return path.Join(curAppURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo)) } +// ComposeGoGetCloneURL returns the clone URL for the go-import meta content. +func ComposeGoGetCloneURL(ctx *Context, owner, repo string) string { + useSSH := setting.Repository.GoGetCloneURLProtocol == "ssh" || (setting.Repository.DisableHTTPGit && !setting.SSH.Disabled) + if useSSH { + return repo_model.ComposeSSHCloneURI(ctx.Doer, owner, repo) + } + return repo_model.ComposeHTTPSCloneURL(ctx, owner, repo) +} + // EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200 // if user does not have actual access to the requested repository, // or the owner or repository does not exist at all. @@ -402,13 +411,7 @@ func EarlyResponseForGoGetMeta(ctx *Context) { return } - var cloneURL string - if setting.Repository.GoGetCloneURLProtocol == "ssh" { - cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame) - } else { - cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame) - } - goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL) + goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), ComposeGoGetCloneURL(ctx, username, reponame)) htmlMeta := fmt.Sprintf(``, html.EscapeString(goImportContent)) ctx.PlainText(http.StatusOK, htmlMeta) } diff --git a/tests/integration/goget_test.go b/tests/integration/goget_test.go index b254c5e9b15..de78e40e13c 100644 --- a/tests/integration/goget_test.go +++ b/tests/integration/goget_test.go @@ -58,6 +58,11 @@ func TestGoGetForSSH(t *testing.T) { `, setting.AppDomain, setting.HTTPPort, setting.AppURL, setting.SSH.Domain, setting.SSH.Port) assert.Equal(t, expected, resp.Body.String()) + + // go rejects scp-style addresses, so the standard port must still produce an ssh:// URL + defer test.MockVariableValue(&setting.SSH.Port, 22)() + resp = MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), fmt.Sprintf(`git ssh://git@%s/blah/glah.git">`, setting.SSH.Domain)) } // TestGoGetPrivateRepoBranchNotLeaked ensures the go-get meta endpoint does not disclose a