Skip to content

Commit

Permalink
Make git clone URL could use current signed-in user (#33091)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
wxiaoguang authored Jan 7, 2025
1 parent 98637fe commit 34dfc25
Show file tree
Hide file tree
Showing 21 changed files with 272 additions and 142 deletions.
55 changes: 31 additions & 24 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ RUN_USER = ; git
;; Set the domain for the server
;DOMAIN = localhost
;;
;; Overwrite the automatically generated public URL. Necessary for proxies and docker.
;ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/
;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/".
;; Most users should set it to the real website URL of their Gitea instance.
;ROOT_URL =
;;
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
;; DO NOT USE IT IN PRODUCTION!!!
Expand All @@ -103,8 +104,8 @@ RUN_USER = ; git
;REDIRECT_OTHER_PORT = false
;PORT_TO_REDIRECT = 80
;;
;; expect PROXY protocol header on connections to https redirector.
;REDIRECTOR_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s
;; expect PROXY protocol header on connections to https redirector, defaults to USE_PROXY_PROTOCOL
;REDIRECTOR_USE_PROXY_PROTOCOL =
;; Minimum and maximum supported TLS versions
;SSL_MIN_VERSION=TLSv1.2
;SSL_MAX_VERSION=
Expand All @@ -128,13 +129,14 @@ RUN_USER = ; git
;; most cases you do not need to change the default value. Alter it only if
;; your SSH server node is not the same as HTTP node. For different protocol, the default
;; values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`.
;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`.
;; If listen on `0.0.0.0`, the default value is `%(PROTOCOL)s://localhost:%(HTTP_PORT)s/`, Otherwise the default
;; value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`.
;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/
;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`.
;; If listen on `0.0.0.0`, the default value is `{PROTOCOL}://localhost:{HTTP_PORT}/`.
;; Otherwise the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`.
;; Most users don't need (and shouldn't) set this value.
;LOCAL_ROOT_URL =
;;
;; When making local connections pass the PROXY protocol header.
;LOCAL_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s
;; When making local connections pass the PROXY protocol header, defaults to USE_PROXY_PROTOCOL
;LOCAL_USE_PROXY_PROTOCOL =
;;
;; Disable SSH feature when not available
;DISABLE_SSH = false
Expand All @@ -146,22 +148,26 @@ RUN_USER = ; git
;SSH_SERVER_USE_PROXY_PROTOCOL = false
;;
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
;BUILTIN_SSH_SERVER_USER = %(RUN_USER)s
;BUILTIN_SSH_SERVER_USER =
;;
;; Domain name to be exposed in clone URL
;SSH_DOMAIN = %(DOMAIN)s
;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL
;SSH_DOMAIN =
;;
;; SSH username displayed in clone URLs.
;SSH_USER = %(BUILTIN_SSH_SERVER_USER)s
;; SSH username displayed in clone URLs. It defaults to BUILTIN_SSH_SERVER_USER or RUN_USER.
;; If it is set to "(DOER_USERNAME)", it will use current signed-in user's username.
;; This option is only for some advanced users who have configured their SSH reverse-proxy
;; and need to use different usernames for git SSH clone.
;; Most users should just leave it blank.
;SSH_USER =
;;
;; The network interface the builtin SSH server should listen on
;SSH_LISTEN_HOST =
;;
;; Port number to be exposed in clone URL
;SSH_PORT = 22
;;
;; The port number the builtin SSH server should listen on
;SSH_LISTEN_PORT = %(SSH_PORT)s
;; The port number the builtin SSH server should listen on, defaults to SSH_PORT
;SSH_LISTEN_PORT =
;;
;; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'.
;SSH_ROOT_PATH =
Expand All @@ -188,7 +194,7 @@ RUN_USER = ; git
;;
;; For the built-in SSH server, choose the keypair to offer as the host key
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
;; relative paths are made absolute relative to the %(APP_DATA_PATH)s
;; relative paths are made absolute relative to the APP_DATA_PATH
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
;;
;; Directory to create temporary files in when testing public keys using ssh-keygen,
Expand Down Expand Up @@ -582,7 +588,7 @@ ENABLED = true
[log]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Root path for the log files - defaults to %(GITEA_WORK_DIR)/log
;; Root path for the log files - defaults to "{AppWorkPath}/log"
;ROOT_PATH =
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down Expand Up @@ -682,8 +688,8 @@ LEVEL = Info
;; The path of git executable. If empty, Gitea searches through the PATH environment.
;PATH =
;;
;; The HOME directory for Git
;HOME_PATH = %(APP_DATA_PATH)s/home
;; The HOME directory for Git, defaults to "{APP_DATA_PATH}/home"
;HOME_PATH =
;;
;; Disables highlight of added and removed changes
;DISABLE_DIFF_HIGHLIGHT = false
Expand Down Expand Up @@ -946,8 +952,8 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[repository]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Root path for storing all repository data. By default, it is set to %(APP_DATA_PATH)s/gitea-repositories.
;; A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s
;; Root path for storing all repository data. By default, it is set to "{APP_DATA_PATH}/gitea-repositories".
;; A relative path is interpreted as "{AppWorkPath}/{ROOT}" (use AppWorkPath as base path).
;ROOT =
;;
;; The script type this server supports. Usually this is `bash`, but some users report that only `sh` is available.
Expand Down Expand Up @@ -1506,7 +1512,8 @@ LEVEL = Info
;TYPE = persistable-channel
;;
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
;DATADIR = queues/ ; Relative paths will be made absolute against `%(APP_DATA_PATH)s`.
;; Relative paths will be made absolute against "APP_DATA_PATH"
;DATADIR = queues/
;;
;; Default queue length before a channel queue will block
;LENGTH = 100000
Expand Down
2 changes: 1 addition & 1 deletion models/migrations/v1_21/v276.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
}

u, err := giturl.Parse(remoteURL)
u, err := giturl.ParseGitURL(remoteURL)
if err != nil {
return "", err
}
Expand Down
124 changes: 82 additions & 42 deletions models/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
Expand Down Expand Up @@ -637,14 +638,26 @@ type CloneLink struct {
}

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

func ComposeSSHCloneURL(ownerName, repoName string) string {
func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
sshUser := setting.SSH.User
sshDomain := setting.SSH.Domain

if sshUser == "(DOER_USERNAME)" {
// Some users use SSH reverse-proxy and need to use the current signed-in username as the SSH user
// to make the SSH reverse-proxy could prepare the user's public keys ahead.
// For most cases we have the correct "doer", then use it as the SSH user.
// If we can't get the doer, then use the built-in SSH user.
if doer != nil {
sshUser = doer.Name
} else {
sshUser = setting.SSH.BuiltinServerUser
}
}

// non-standard port, it must use full URI
if setting.SSH.Port != 22 {
sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port))
Expand All @@ -662,21 +675,20 @@ func ComposeSSHCloneURL(ownerName, repoName string) string {
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
}

func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
repoName := repo.Name
if isWiki {
repoName += ".wiki"
}

func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink {
cl := new(CloneLink)
cl.SSH = ComposeSSHCloneURL(repo.OwnerName, repoName)
cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName)
cl.SSH = ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName)
cl.HTTPS = ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName)
return cl
}

// CloneLink returns clone URLs of repository.
func (repo *Repository) CloneLink() (cl *CloneLink) {
return repo.cloneLink(false)
func (repo *Repository) CloneLink(ctx context.Context, doer *user_model.User) (cl *CloneLink) {
return repo.cloneLink(ctx, doer, repo.Name)
}

func (repo *Repository) CloneLinkGeneral(ctx context.Context) (cl *CloneLink) {
return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name)
}

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

// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url
func getRepositoryURLPathSegments(repoURL string) []string {
if strings.HasPrefix(repoURL, setting.AppURL) {
return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/")
func parseRepositoryURL(ctx context.Context, repoURL string) (ret struct {
OwnerName, RepoName, RemainingPath string
},
) {
// possible urls for git:
// https://my.domain/sub-path/<owner>/<repo>[.git]
// git+ssh://[email protected]/<owner>/<repo>[.git]
// ssh://[email protected]/<owner>/<repo>[.git]
// [email protected]:<owner>/<repo>[.git]

fillPathParts := func(s string) {
s = strings.TrimPrefix(s, "/")
fields := strings.SplitN(s, "/", 3)
if len(fields) >= 2 {
ret.OwnerName = fields[0]
ret.RepoName = strings.TrimSuffix(fields[1], ".git")
if len(fields) == 3 {
ret.RemainingPath = "/" + fields[2]
}
}
}

sshURLVariants := [4]string{
setting.SSH.Domain + ":",
setting.SSH.User + "@" + setting.SSH.Domain + ":",
"git+ssh://" + setting.SSH.Domain + "/",
"git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/",
parsed, err := giturl.ParseGitURL(repoURL)
if err != nil {
return ret
}

for _, sshURL := range sshURLVariants {
if strings.HasPrefix(repoURL, sshURL) {
return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/")
if parsed.URL.Scheme == "http" || parsed.URL.Scheme == "https" {
if !httplib.IsCurrentGiteaSiteURL(ctx, repoURL) {
return ret
}
fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL))
} else if parsed.URL.Scheme == "ssh" || parsed.URL.Scheme == "git+ssh" {
domainSSH := setting.SSH.Domain
domainCur := httplib.GuessCurrentHostDomain(ctx)
urlDomain, _, _ := net.SplitHostPort(parsed.URL.Host)
urlDomain = util.IfZero(urlDomain, parsed.URL.Host)
if urlDomain == "" {
return ret
}
// check whether URL domain is the App domain
domainMatches := domainSSH == urlDomain
// check whether URL domain is current domain from context
domainMatches = domainMatches || (domainCur != "" && domainCur == urlDomain)
if domainMatches {
fillPathParts(parsed.URL.Path)
}
}

return nil
return ret
}

// GetRepositoryByURL returns the repository by given url
func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) {
// possible urls for git:
// https://my.domain/sub-path/<owner>/<repo>.git
// https://my.domain/sub-path/<owner>/<repo>
// git+ssh://[email protected]/<owner>/<repo>.git
// git+ssh://[email protected]/<owner>/<repo>
// [email protected]:<owner>/<repo>.git
// [email protected]:<owner>/<repo>

pathSegments := getRepositoryURLPathSegments(repoURL)

if len(pathSegments) != 2 {
ret := parseRepositoryURL(ctx, repoURL)
if ret.OwnerName == "" {
return nil, fmt.Errorf("unknown or malformed repository URL")
}
return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName)
}

ownerName := pathSegments[0]
repoName := strings.TrimSuffix(pathSegments[1], ".git")
return GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
// GetRepositoryByURLRelax also accepts an SSH clone URL without user part
func GetRepositoryByURLRelax(ctx context.Context, repoURL string) (*Repository, error) {
if !strings.Contains(repoURL, "://") && !strings.Contains(repoURL, "@") {
// convert "example.com:owner/repo" to "@example.com:owner/repo"
p1, p2, p3 := strings.Index(repoURL, "."), strings.Index(repoURL, ":"), strings.Index(repoURL, "/")
if 0 < p1 && p1 < p2 && p2 < p3 {
repoURL = "@" + repoURL
}
}
return GetRepositoryByURL(ctx, repoURL)
}

// GetRepositoryByID returns the repository by given id if exists.
Expand Down
Loading

0 comments on commit 34dfc25

Please sign in to comment.