From 4b808133414e39348f56d691883426947bf2d322 Mon Sep 17 00:00:00 2001
From: rune <runner.mei@gmail.com>
Date: Fri, 12 May 2023 17:44:37 +0800
Subject: [PATCH] Support SSH for go get (#24664)

fix #12192 Support SSH for go get

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: mfk <mfk@hengwei.com.cn>
Co-authored-by: silverwind <me@silverwind.io>
---
 custom/conf/app.example.ini                   |  3 ++
 .../config-cheat-sheet.en-us.md               |  2 ++
 models/repo/repo.go                           | 32 +++++++++++--------
 modules/context/repo.go                       |  9 +++++-
 modules/setting/repository.go                 |  2 ++
 routers/web/goget.go                          |  9 +++++-
 tests/integration/goget_test.go               | 26 +++++++++++++++
 7 files changed, 68 insertions(+), 15 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 3ceb53dcd0..e279b67ded 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -940,6 +940,9 @@ ROUTER = console
 ;; 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
+;;
 ;; Close issues as long as a commit on any branch marks it as fixed
 ;; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki, repo.projects, repo.packages, repo.actions.
 ;DISABLED_REPO_UNITS =
diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md
index c1befed489..470e299a18 100644
--- a/docs/content/doc/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md
@@ -95,6 +95,8 @@ In addition there is _`StaticRootPath`_ which can be set as a built-in at build
    HTTP protocol.
 - `USE_COMPAT_SSH_URI`: **false**: Force ssh:// clone url instead of scp-style uri when
    default SSH port is used.
+- `GO_GET_CLONE_URL_PROTOCOL`: **https**: Value for the "go get" request returns the repository url as https or ssh
+   default is https.
 - `ACCESS_CONTROL_ALLOW_ORIGIN`: **\<empty\>**: Value for Access-Control-Allow-Origin header,
    default is not to present. **WARNING**: This maybe harmful to you website if you do not
    give it a right value.
diff --git a/models/repo/repo.go b/models/repo/repo.go
index 2e8c28cbb3..7cbd5867b7 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -547,16 +547,9 @@ func ComposeHTTPSCloneURL(owner, repo string) string {
 	return fmt.Sprintf("%s%s/%s.git", setting.AppURL, url.PathEscape(owner), url.PathEscape(repo))
 }
 
-func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
-	repoName := repo.Name
-	if isWiki {
-		repoName += ".wiki"
-	}
-
+func ComposeSSHCloneURL(ownerName, repoName string) string {
 	sshUser := setting.SSH.User
 
-	cl := new(CloneLink)
-
 	// if we have a ipv6 literal we need to put brackets around it
 	// for the git cloning to work.
 	sshDomain := setting.SSH.Domain
@@ -566,12 +559,25 @@ func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
 	}
 
 	if setting.SSH.Port != 22 {
-		cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)), url.PathEscape(repo.OwnerName), url.PathEscape(repoName))
-	} else if setting.Repository.UseCompatSSHURI {
-		cl.SSH = fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, url.PathEscape(repo.OwnerName), url.PathEscape(repoName))
-	} else {
-		cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, url.PathEscape(repo.OwnerName), url.PathEscape(repoName))
+		return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser,
+			net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)),
+			url.PathEscape(ownerName),
+			url.PathEscape(repoName))
 	}
+	if setting.Repository.UseCompatSSHURI {
+		return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, url.PathEscape(ownerName), url.PathEscape(repoName))
+	}
+	return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, url.PathEscape(ownerName), url.PathEscape(repoName))
+}
+
+func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
+	repoName := repo.Name
+	if isWiki {
+		repoName += ".wiki"
+	}
+
+	cl := new(CloneLink)
+	cl.SSH = ComposeSSHCloneURL(repo.OwnerName, repoName)
 	cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName)
 	return cl
 }
diff --git a/modules/context/repo.go b/modules/context/repo.go
index 84e07ab422..b20ea26e4e 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -319,7 +319,14 @@ func EarlyResponseForGoGetMeta(ctx *Context) {
 		ctx.PlainText(http.StatusBadRequest, "invalid repository path")
 		return
 	}
-	goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(username, reponame), repo_model.ComposeHTTPSCloneURL(username, reponame))
+
+	var cloneURL string
+	if setting.Repository.GoGetCloneURLProtocol == "ssh" {
+		cloneURL = repo_model.ComposeSSHCloneURL(username, reponame)
+	} else {
+		cloneURL = repo_model.ComposeHTTPSCloneURL(username, reponame)
+	}
+	goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(username, reponame), cloneURL)
 	htmlMeta := fmt.Sprintf(`<meta name="go-import" content="%s">`, html.EscapeString(goImportContent))
 	ctx.PlainText(http.StatusOK, htmlMeta)
 }
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index 56e7e6f4ac..153307a0b6 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -36,6 +36,7 @@ var (
 		DisableHTTPGit                          bool
 		AccessControlAllowOrigin                string
 		UseCompatSSHURI                         bool
+		GoGetCloneURLProtocol                   string
 		DefaultCloseIssuesViaCommitsInAnyBranch bool
 		EnablePushCreateUser                    bool
 		EnablePushCreateOrg                     bool
@@ -273,6 +274,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.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1)
 	Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString(Repository.DefaultBranch)
 	RepoRootPath = sec.Key("ROOT").MustString(path.Join(AppDataPath, "gitea-repositories"))
diff --git a/routers/web/goget.go b/routers/web/goget.go
index fb8afae999..c5b8b6cbc0 100644
--- a/routers/web/goget.go
+++ b/routers/web/goget.go
@@ -66,7 +66,14 @@ func goGet(ctx *context.Context) {
 	}
 
 	goGetImport := context.ComposeGoGetImport(ownerName, trimmedRepoName)
-	goImportContent := fmt.Sprintf("%s git %s", goGetImport, repo_model.ComposeHTTPSCloneURL(ownerName, repoName) /*CloneLink*/)
+
+	var cloneURL string
+	if setting.Repository.GoGetCloneURLProtocol == "ssh" {
+		cloneURL = repo_model.ComposeSSHCloneURL(ownerName, repoName)
+	} else {
+		cloneURL = repo_model.ComposeHTTPSCloneURL(ownerName, repoName)
+	}
+	goImportContent := fmt.Sprintf("%s git %s", goGetImport, cloneURL /*CloneLink*/)
 	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/tests/integration/goget_test.go b/tests/integration/goget_test.go
index fab3911464..854f8d7a2d 100644
--- a/tests/integration/goget_test.go
+++ b/tests/integration/goget_test.go
@@ -33,3 +33,29 @@ func TestGoGet(t *testing.T) {
 
 	assert.Equal(t, expected, resp.Body.String())
 }
+
+func TestGoGetForSSH(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	old := setting.Repository.GoGetCloneURLProtocol
+	defer func() {
+		setting.Repository.GoGetCloneURLProtocol = old
+	}()
+	setting.Repository.GoGetCloneURLProtocol = "ssh"
+
+	req := NewRequest(t, "GET", "/blah/glah/plah?go-get=1")
+	resp := MakeRequest(t, req, http.StatusOK)
+
+	expected := fmt.Sprintf(`<!doctype html>
+<html>
+	<head>
+		<meta name="go-import" content="%[1]s:%[2]s/blah/glah git ssh://git@%[4]s:%[5]d/blah/glah.git">
+		<meta name="go-source" content="%[1]s:%[2]s/blah/glah _ %[3]sblah/glah/src/branch/master{/dir} %[3]sblah/glah/src/branch/master{/dir}/{file}#L{line}">
+	</head>
+	<body>
+		go get --insecure %[1]s:%[2]s/blah/glah
+	</body>
+</html>`, setting.Domain, setting.HTTPPort, setting.AppURL, setting.SSH.Domain, setting.SSH.Port)
+
+	assert.Equal(t, expected, resp.Body.String())
+}