Skip to content

Commit 34dfc25

Browse files
authored
Make git clone URL could use current signed-in user (#33091)
close #33086 * Add a special value for "SSH_USER" setting: `(DOER_USERNAME)` * Improve parseRepositoryURL and add tests (now it doesn't have hard dependency on some setting values) Many changes are just adding "ctx" and "doer" argument to functions. By the way, improve app.example.ini, remove all `%(key)s` syntax, it only makes messy and no user really cares about it. Document: https://gitea.com/gitea/docs/pulls/138
1 parent 98637fe commit 34dfc25

File tree

21 files changed

+272
-142
lines changed

21 files changed

+272
-142
lines changed

custom/conf/app.example.ini

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ RUN_USER = ; git
7878
;; Set the domain for the server
7979
;DOMAIN = localhost
8080
;;
81-
;; Overwrite the automatically generated public URL. Necessary for proxies and docker.
82-
;ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/
81+
;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/".
82+
;; Most users should set it to the real website URL of their Gitea instance.
83+
;ROOT_URL =
8384
;;
8485
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
8586
;; DO NOT USE IT IN PRODUCTION!!!
@@ -103,8 +104,8 @@ RUN_USER = ; git
103104
;REDIRECT_OTHER_PORT = false
104105
;PORT_TO_REDIRECT = 80
105106
;;
106-
;; expect PROXY protocol header on connections to https redirector.
107-
;REDIRECTOR_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s
107+
;; expect PROXY protocol header on connections to https redirector, defaults to USE_PROXY_PROTOCOL
108+
;REDIRECTOR_USE_PROXY_PROTOCOL =
108109
;; Minimum and maximum supported TLS versions
109110
;SSL_MIN_VERSION=TLSv1.2
110111
;SSL_MAX_VERSION=
@@ -128,13 +129,14 @@ RUN_USER = ; git
128129
;; most cases you do not need to change the default value. Alter it only if
129130
;; your SSH server node is not the same as HTTP node. For different protocol, the default
130131
;; values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`.
131-
;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`.
132-
;; If listen on `0.0.0.0`, the default value is `%(PROTOCOL)s://localhost:%(HTTP_PORT)s/`, Otherwise the default
133-
;; value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`.
134-
;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/
132+
;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`.
133+
;; If listen on `0.0.0.0`, the default value is `{PROTOCOL}://localhost:{HTTP_PORT}/`.
134+
;; Otherwise the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`.
135+
;; Most users don't need (and shouldn't) set this value.
136+
;LOCAL_ROOT_URL =
135137
;;
136-
;; When making local connections pass the PROXY protocol header.
137-
;LOCAL_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s
138+
;; When making local connections pass the PROXY protocol header, defaults to USE_PROXY_PROTOCOL
139+
;LOCAL_USE_PROXY_PROTOCOL =
138140
;;
139141
;; Disable SSH feature when not available
140142
;DISABLE_SSH = false
@@ -146,22 +148,26 @@ RUN_USER = ; git
146148
;SSH_SERVER_USE_PROXY_PROTOCOL = false
147149
;;
148150
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
149-
;BUILTIN_SSH_SERVER_USER = %(RUN_USER)s
151+
;BUILTIN_SSH_SERVER_USER =
150152
;;
151-
;; Domain name to be exposed in clone URL
152-
;SSH_DOMAIN = %(DOMAIN)s
153+
;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL
154+
;SSH_DOMAIN =
153155
;;
154-
;; SSH username displayed in clone URLs.
155-
;SSH_USER = %(BUILTIN_SSH_SERVER_USER)s
156+
;; SSH username displayed in clone URLs. It defaults to BUILTIN_SSH_SERVER_USER or RUN_USER.
157+
;; If it is set to "(DOER_USERNAME)", it will use current signed-in user's username.
158+
;; This option is only for some advanced users who have configured their SSH reverse-proxy
159+
;; and need to use different usernames for git SSH clone.
160+
;; Most users should just leave it blank.
161+
;SSH_USER =
156162
;;
157163
;; The network interface the builtin SSH server should listen on
158164
;SSH_LISTEN_HOST =
159165
;;
160166
;; Port number to be exposed in clone URL
161167
;SSH_PORT = 22
162168
;;
163-
;; The port number the builtin SSH server should listen on
164-
;SSH_LISTEN_PORT = %(SSH_PORT)s
169+
;; The port number the builtin SSH server should listen on, defaults to SSH_PORT
170+
;SSH_LISTEN_PORT =
165171
;;
166172
;; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'.
167173
;SSH_ROOT_PATH =
@@ -188,7 +194,7 @@ RUN_USER = ; git
188194
;;
189195
;; For the built-in SSH server, choose the keypair to offer as the host key
190196
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
191-
;; relative paths are made absolute relative to the %(APP_DATA_PATH)s
197+
;; relative paths are made absolute relative to the APP_DATA_PATH
192198
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
193199
;;
194200
;; Directory to create temporary files in when testing public keys using ssh-keygen,
@@ -582,7 +588,7 @@ ENABLED = true
582588
[log]
583589
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
584590
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
585-
;; Root path for the log files - defaults to %(GITEA_WORK_DIR)/log
591+
;; Root path for the log files - defaults to "{AppWorkPath}/log"
586592
;ROOT_PATH =
587593
;;
588594
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -682,8 +688,8 @@ LEVEL = Info
682688
;; The path of git executable. If empty, Gitea searches through the PATH environment.
683689
;PATH =
684690
;;
685-
;; The HOME directory for Git
686-
;HOME_PATH = %(APP_DATA_PATH)s/home
691+
;; The HOME directory for Git, defaults to "{APP_DATA_PATH}/home"
692+
;HOME_PATH =
687693
;;
688694
;; Disables highlight of added and removed changes
689695
;DISABLE_DIFF_HIGHLIGHT = false
@@ -946,8 +952,8 @@ LEVEL = Info
946952
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
947953
;[repository]
948954
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
949-
;; Root path for storing all repository data. By default, it is set to %(APP_DATA_PATH)s/gitea-repositories.
950-
;; A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s
955+
;; Root path for storing all repository data. By default, it is set to "{APP_DATA_PATH}/gitea-repositories".
956+
;; A relative path is interpreted as "{AppWorkPath}/{ROOT}" (use AppWorkPath as base path).
951957
;ROOT =
952958
;;
953959
;; The script type this server supports. Usually this is `bash`, but some users report that only `sh` is available.
@@ -1506,7 +1512,8 @@ LEVEL = Info
15061512
;TYPE = persistable-channel
15071513
;;
15081514
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
1509-
;DATADIR = queues/ ; Relative paths will be made absolute against `%(APP_DATA_PATH)s`.
1515+
;; Relative paths will be made absolute against "APP_DATA_PATH"
1516+
;DATADIR = queues/
15101517
;;
15111518
;; Default queue length before a channel queue will block
15121519
;LENGTH = 100000

models/migrations/v1_21/v276.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
172172
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
173173
}
174174

175-
u, err := giturl.Parse(remoteURL)
175+
u, err := giturl.ParseGitURL(remoteURL)
176176
if err != nil {
177177
return "", err
178178
}

models/repo/repo.go

Lines changed: 82 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
user_model "code.gitea.io/gitea/models/user"
2121
"code.gitea.io/gitea/modules/base"
2222
"code.gitea.io/gitea/modules/git"
23+
giturl "code.gitea.io/gitea/modules/git/url"
2324
"code.gitea.io/gitea/modules/httplib"
2425
"code.gitea.io/gitea/modules/log"
2526
"code.gitea.io/gitea/modules/markup"
@@ -637,14 +638,26 @@ type CloneLink struct {
637638
}
638639

639640
// ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name.
640-
func ComposeHTTPSCloneURL(owner, repo string) string {
641-
return fmt.Sprintf("%s%s/%s.git", setting.AppURL, url.PathEscape(owner), url.PathEscape(repo))
641+
func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string {
642+
return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo))
642643
}
643644

644-
func ComposeSSHCloneURL(ownerName, repoName string) string {
645+
func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
645646
sshUser := setting.SSH.User
646647
sshDomain := setting.SSH.Domain
647648

649+
if sshUser == "(DOER_USERNAME)" {
650+
// Some users use SSH reverse-proxy and need to use the current signed-in username as the SSH user
651+
// to make the SSH reverse-proxy could prepare the user's public keys ahead.
652+
// For most cases we have the correct "doer", then use it as the SSH user.
653+
// If we can't get the doer, then use the built-in SSH user.
654+
if doer != nil {
655+
sshUser = doer.Name
656+
} else {
657+
sshUser = setting.SSH.BuiltinServerUser
658+
}
659+
}
660+
648661
// non-standard port, it must use full URI
649662
if setting.SSH.Port != 22 {
650663
sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port))
@@ -662,21 +675,20 @@ func ComposeSSHCloneURL(ownerName, repoName string) string {
662675
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
663676
}
664677

665-
func (repo *Repository) cloneLink(isUncyclo bool) *CloneLink {
666-
repoName := repo.Name
667-
if isUncyclo {
668-
repoName += ".wiki"
669-
}
670-
678+
func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink {
671679
cl := new(CloneLink)
672-
cl.SSH = ComposeSSHCloneURL(repo.OwnerName, repoName)
673-
cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName)
680+
cl.SSH = ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName)
681+
cl.HTTPS = ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName)
674682
return cl
675683
}
676684

677685
// CloneLink returns clone URLs of repository.
678-
func (repo *Repository) CloneLink() (cl *CloneLink) {
679-
return repo.cloneLink(false)
686+
func (repo *Repository) CloneLink(ctx context.Context, doer *user_model.User) (cl *CloneLink) {
687+
return repo.cloneLink(ctx, doer, repo.Name)
688+
}
689+
690+
func (repo *Repository) CloneLinkGeneral(ctx context.Context) (cl *CloneLink) {
691+
return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name)
680692
}
681693

682694
// GetOriginalURLHostname returns the hostname of a URL or the URL
@@ -772,47 +784,75 @@ func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repo
772784
return &repo, err
773785
}
774786

775-
// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url
776-
func getRepositoryURLPathSegments(repoURL string) []string {
777-
if strings.HasPrefix(repoURL, setting.AppURL) {
778-
return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/")
787+
func parseRepositoryURL(ctx context.Context, repoURL string) (ret struct {
788+
OwnerName, RepoName, RemainingPath string
789+
},
790+
) {
791+
// possible urls for git:
792+
// https://my.domain/sub-path/<owner>/<repo>[.git]
793+
// git+ssh://[email protected]/<owner>/<repo>[.git]
794+
// ssh://[email protected]/<owner>/<repo>[.git]
795+
// [email protected]:<owner>/<repo>[.git]
796+
797+
fillPathParts := func(s string) {
798+
s = strings.TrimPrefix(s, "/")
799+
fields := strings.SplitN(s, "/", 3)
800+
if len(fields) >= 2 {
801+
ret.OwnerName = fields[0]
802+
ret.RepoName = strings.TrimSuffix(fields[1], ".git")
803+
if len(fields) == 3 {
804+
ret.RemainingPath = "/" + fields[2]
805+
}
806+
}
779807
}
780808

781-
sshURLVariants := [4]string{
782-
setting.SSH.Domain + ":",
783-
setting.SSH.User + "@" + setting.SSH.Domain + ":",
784-
"git+ssh://" + setting.SSH.Domain + "/",
785-
"git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/",
809+
parsed, err := giturl.ParseGitURL(repoURL)
810+
if err != nil {
811+
return ret
786812
}
787-
788-
for _, sshURL := range sshURLVariants {
789-
if strings.HasPrefix(repoURL, sshURL) {
790-
return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/")
813+
if parsed.URL.Scheme == "http" || parsed.URL.Scheme == "https" {
814+
if !httplib.IsCurrentGiteaSiteURL(ctx, repoURL) {
815+
return ret
816+
}
817+
fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL))
818+
} else if parsed.URL.Scheme == "ssh" || parsed.URL.Scheme == "git+ssh" {
819+
domainSSH := setting.SSH.Domain
820+
domainCur := httplib.GuessCurrentHostDomain(ctx)
821+
urlDomain, _, _ := net.SplitHostPort(parsed.URL.Host)
822+
urlDomain = util.IfZero(urlDomain, parsed.URL.Host)
823+
if urlDomain == "" {
824+
return ret
825+
}
826+
// check whether URL domain is the App domain
827+
domainMatches := domainSSH == urlDomain
828+
// check whether URL domain is current domain from context
829+
domainMatches = domainMatches || (domainCur != "" && domainCur == urlDomain)
830+
if domainMatches {
831+
fillPathParts(parsed.URL.Path)
791832
}
792833
}
793-
794-
return nil
834+
return ret
795835
}
796836

797837
// GetRepositoryByURL returns the repository by given url
798838
func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) {
799-
// possible urls for git:
800-
// https://my.domain/sub-path/<owner>/<repo>.git
801-
// https://my.domain/sub-path/<owner>/<repo>
802-
// git+ssh://[email protected]/<owner>/<repo>.git
803-
// git+ssh://[email protected]/<owner>/<repo>
804-
// [email protected]:<owner>/<repo>.git
805-
// [email protected]:<owner>/<repo>
806-
807-
pathSegments := getRepositoryURLPathSegments(repoURL)
808-
809-
if len(pathSegments) != 2 {
839+
ret := parseRepositoryURL(ctx, repoURL)
840+
if ret.OwnerName == "" {
810841
return nil, fmt.Errorf("unknown or malformed repository URL")
811842
}
843+
return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName)
844+
}
812845

813-
ownerName := pathSegments[0]
814-
repoName := strings.TrimSuffix(pathSegments[1], ".git")
815-
return GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
846+
// GetRepositoryByURLRelax also accepts an SSH clone URL without user part
847+
func GetRepositoryByURLRelax(ctx context.Context, repoURL string) (*Repository, error) {
848+
if !strings.Contains(repoURL, "://") && !strings.Contains(repoURL, "@") {
849+
// convert "example.com:owner/repo" to "@example.com:owner/repo"
850+
p1, p2, p3 := strings.Index(repoURL, "."), strings.Index(repoURL, ":"), strings.Index(repoURL, "/")
851+
if 0 < p1 && p1 < p2 && p2 < p3 {
852+
repoURL = "@" + repoURL
853+
}
854+
}
855+
return GetRepositoryByURL(ctx, repoURL)
816856
}
817857

818858
// GetRepositoryByID returns the repository by given id if exists.

0 commit comments

Comments
 (0)