diff options
author | ConcurrentCrab <102517200+ConcurrentCrab@users.noreply.github.com> | 2024-09-27 19:57:37 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-27 10:27:37 -0400 |
commit | 8a9fd7f771f4f694594a0652e95ddda2b7479b3e (patch) | |
tree | eaad5bf33ca2a745645fc2368b4b01d598e35486 /cmd | |
parent | fdb1df9eca2f7adabf08883042325af26c3fd509 (diff) | |
download | gitea-8a9fd7f771f4f694594a0652e95ddda2b7479b3e.tar.gz gitea-8a9fd7f771f4f694594a0652e95ddda2b7479b3e.zip |
Add pure SSH LFS support (#31516)
Fixes #17554
/claim #17554
Docs PR https://gitea.com/gitea/docs/pulls/49
To test, run pushes like: `GIT_TRACE=1` git push. The trace output
should mention "pure SSH connection".
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/serv.go | 129 |
1 files changed, 85 insertions, 44 deletions
diff --git a/cmd/serv.go b/cmd/serv.go index f74a8fd3d0..2d2df8aa23 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -20,8 +20,10 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/lfstransfer" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/pprof" "code.gitea.io/gitea/modules/private" @@ -36,7 +38,11 @@ import ( ) const ( - lfsAuthenticateVerb = "git-lfs-authenticate" + verbUploadPack = "git-upload-pack" + verbUploadArchive = "git-upload-archive" + verbReceivePack = "git-receive-pack" + verbLfsAuthenticate = "git-lfs-authenticate" + verbLfsTransfer = "git-lfs-transfer" ) // CmdServ represents the available serv sub-command. @@ -73,12 +79,18 @@ func setup(ctx context.Context, debug bool) { } var ( - allowedCommands = map[string]perm.AccessMode{ - "git-upload-pack": perm.AccessModeRead, - "git-upload-archive": perm.AccessModeRead, - "git-receive-pack": perm.AccessModeWrite, - lfsAuthenticateVerb: perm.AccessModeNone, - } + // keep getAccessMode() in sync + allowedCommands = container.SetOf( + verbUploadPack, + verbUploadArchive, + verbReceivePack, + verbLfsAuthenticate, + verbLfsTransfer, + ) + allowedCommandsLfs = container.SetOf( + verbLfsAuthenticate, + verbLfsTransfer, + ) alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) ) @@ -124,6 +136,45 @@ func handleCliResponseExtra(extra private.ResponseExtra) error { return nil } +func getAccessMode(verb, lfsVerb string) perm.AccessMode { + switch verb { + case verbUploadPack, verbUploadArchive: + return perm.AccessModeRead + case verbReceivePack: + return perm.AccessModeWrite + case verbLfsAuthenticate, verbLfsTransfer: + switch lfsVerb { + case "upload": + return perm.AccessModeWrite + case "download": + return perm.AccessModeRead + } + } + // should be unreachable + return perm.AccessModeNone +} + +func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServCommandResults) (string, error) { + now := time.Now() + claims := lfs.Claims{ + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)), + NotBefore: jwt.NewNumericDate(now), + }, + RepoID: results.RepoID, + Op: lfsVerb, + UserID: results.UserID, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Sign and get the complete encoded token as a string using the secret + tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes) + if err != nil { + return "", fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err) + } + return fmt.Sprintf("Bearer %s", tokenString), nil +} + func runServ(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() @@ -198,15 +249,6 @@ func runServ(c *cli.Context) error { repoPath := strings.TrimPrefix(words[1], "/") var lfsVerb string - if verb == lfsAuthenticateVerb { - if !setting.LFS.StartServer { - return fail(ctx, "Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled") - } - - if len(words) > 2 { - lfsVerb = words[2] - } - } rr := strings.SplitN(repoPath, "/", 2) if len(rr) != 2 { @@ -243,53 +285,52 @@ func runServ(c *cli.Context) error { }() } - requestedMode, has := allowedCommands[verb] - if !has { + if allowedCommands.Contains(verb) { + if allowedCommandsLfs.Contains(verb) { + if !setting.LFS.StartServer { + return fail(ctx, "Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled") + } + if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH { + return fail(ctx, "Unknown git command", "LFS SSH transfer connection denied, pure SSH protocol is disabled") + } + if len(words) > 2 { + lfsVerb = words[2] + } + } + } else { return fail(ctx, "Unknown git command", "Unknown git command %s", verb) } - if verb == lfsAuthenticateVerb { - if lfsVerb == "upload" { - requestedMode = perm.AccessModeWrite - } else if lfsVerb == "download" { - requestedMode = perm.AccessModeRead - } else { - return fail(ctx, "Unknown LFS verb", "Unknown lfs verb %s", lfsVerb) - } - } + requestedMode := getAccessMode(verb, lfsVerb) results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) if extra.HasError() { return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error) } + // LFS SSH protocol + if verb == verbLfsTransfer { + token, err := getLFSAuthToken(ctx, lfsVerb, results) + if err != nil { + return err + } + return lfstransfer.Main(ctx, repoPath, lfsVerb, token) + } + // LFS token authentication - if verb == lfsAuthenticateVerb { + if verb == verbLfsAuthenticate { url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName)) - now := time.Now() - claims := lfs.Claims{ - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)), - NotBefore: jwt.NewNumericDate(now), - }, - RepoID: results.RepoID, - Op: lfsVerb, - UserID: results.UserID, - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - // Sign and get the complete encoded token as a string using the secret - tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes) + token, err := getLFSAuthToken(ctx, lfsVerb, results) if err != nil { - return fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err) + return err } tokenAuthentication := &git_model.LFSTokenResponse{ Header: make(map[string]string), Href: url, } - tokenAuthentication.Header["Authorization"] = fmt.Sprintf("Bearer %s", tokenString) + tokenAuthentication.Header["Authorization"] = token enc := json.NewEncoder(os.Stdout) err = enc.Encode(tokenAuthentication) |