diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2023-03-29 14:32:26 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-29 14:32:26 +0800 |
commit | f4538791f5fc82b173608fcf9c30e36ec01dc9d3 (patch) | |
tree | eaeacc558e9b773eae9268a99146f351dd21a980 /cmd/serv.go | |
parent | 79e7a6ec1e14c820c8475173a37aee84f2f4f1e8 (diff) | |
download | gitea-f4538791f5fc82b173608fcf9c30e36ec01dc9d3.tar.gz gitea-f4538791f5fc82b173608fcf9c30e36ec01dc9d3.zip |
Refactor internal API for git commands, use meaningful messages instead of "Internal Server Error" (#23687)
# Why this PR comes
At first, I'd like to help users like #23636 (there are a lot)
The unclear "Internal Server Error" is quite anonying, scare users,
frustrate contributors, nobody knows what happens.
So, it's always good to provide meaningful messages to end users (of
course, do not leak sensitive information).
When I started working on the "response message to end users", I found
that the related code has a lot of technical debt. A lot of copy&paste
code, unclear fields and usages.
So I think it's good to make everything clear.
# Tech Backgrounds
Gitea has many sub-commands, some are used by admins, some are used by
SSH servers or Git Hooks. Many sub-commands use "internal API" to
communicate with Gitea web server.
Before, Gitea server always use `StatusCode + Json "err" field` to
return messages.
* The CLI sub-commands: they expect to show all error related messages
to site admin
* The Serv/Hook sub-commands (for git clients): they could only show
safe messages to end users, the error log could only be recorded by
"SSHLog" to Gitea web server.
In the old design, it assumes that:
* If the StatusCode is 500 (in some functions), then the "err" field is
error log, shouldn't be exposed to git client.
* If the StatusCode is 40x, then the "err" field could be exposed. And
some functions always read the "err" no matter what the StatusCode is.
The old code is not strict, and it's difficult to distinguish the
messages clearly and then output them correctly.
# This PR
To help to remove duplicate code and make everything clear, this PR
introduces `ResponseExtra` and `requestJSONResp`.
* `ResponseExtra` is a struct which contains "extra" information of a
internal API response, including StatusCode, UserMsg, Error
* `requestJSONResp` is a generic function which can be used for all
cases to help to simplify the calls.
* Remove all `map["err"]`, always use `private.Response{Err}` to
construct error messages.
* User messages and error messages are separated clearly, the `fail` and
`handleCliResponseExtra` will output correct messages.
* Replace all `Internal Server Error` messages with meaningful (still
safe) messages.
This PR saves more than 300 lines, while makes the git client messages
more clear.
Many gitea-serv/git-hook related essential functions are covered by
tests.
---------
Co-authored-by: delvh <dev.lh@web.de>
Diffstat (limited to 'cmd/serv.go')
-rw-r--r-- | cmd/serv.go | 97 |
1 files changed, 56 insertions, 41 deletions
diff --git a/cmd/serv.go b/cmd/serv.go index d7510845a5..72eb637071 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -7,7 +7,6 @@ package cmd import ( "context" "fmt" - "net/http" "net/url" "os" "os/exec" @@ -16,6 +15,7 @@ import ( "strconv" "strings" "time" + "unicode" asymkey_model "code.gitea.io/gitea/models/asymkey" git_model "code.gitea.io/gitea/models/git" @@ -55,7 +55,7 @@ var CmdServ = cli.Command{ }, } -func setup(logPath string, debug bool) { +func setup(ctx context.Context, debug bool) { _ = log.DelLogger("console") if debug { _ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`) @@ -72,15 +72,15 @@ func setup(logPath string, debug bool) { // `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection. if _, err := os.Stat(setting.RepoRootPath); err != nil { if os.IsNotExist(err) { - _ = fail("Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath) + _ = fail(ctx, "Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath) } else { - _ = fail("Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err) + _ = fail(ctx, "Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err) } return } if err := git.InitSimple(context.Background()); err != nil { - _ = fail("Failed to init git", "Failed to init git, err: %v", err) + _ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err) } } @@ -94,24 +94,46 @@ var ( alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) ) -func fail(userMessage, logMessage string, args ...interface{}) error { +// fail prints message to stdout, it's mainly used for git serv and git hook commands. +// The output will be passed to git client and shown to user. +func fail(ctx context.Context, userMessage, logMsgFmt string, args ...interface{}) error { + if userMessage == "" { + userMessage = "Internal Server Error (no specific error)" + } + // There appears to be a chance to cause a zombie process and failure to read the Exit status // if nothing is outputted on stdout. _, _ = fmt.Fprintln(os.Stdout, "") _, _ = fmt.Fprintln(os.Stderr, "Gitea:", userMessage) - if len(logMessage) > 0 { + if logMsgFmt != "" { + logMsg := fmt.Sprintf(logMsgFmt, args...) if !setting.IsProd { - _, _ = fmt.Fprintf(os.Stderr, logMessage+"\n", args...) + _, _ = fmt.Fprintln(os.Stderr, "Gitea:", logMsg) + } + if userMessage != "" { + if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) { + logMsg = userMessage + " " + logMsg + } else { + logMsg = userMessage + ". " + logMsg + } } + _ = private.SSHLog(ctx, true, logMsg) } - ctx, cancel := installSignals() - defer cancel() + return cli.NewExitError("", 1) +} - if len(logMessage) > 0 { - _ = private.SSHLog(ctx, true, fmt.Sprintf(logMessage+": ", args...)) +// handleCliResponseExtra handles the extra response from the cli sub-commands +// If there is a user message it will be printed to stdout +// If the command failed it will return an error (the error will be printed by cli framework) +func handleCliResponseExtra(extra private.ResponseExtra) error { + if extra.UserMsg != "" { + _, _ = fmt.Fprintln(os.Stdout, extra.UserMsg) } - return cli.NewExitError("", 1) + if extra.HasError() { + return cli.NewExitError(extra.Error, 1) + } + return nil } func runServ(c *cli.Context) error { @@ -119,7 +141,7 @@ func runServ(c *cli.Context) error { defer cancel() // FIXME: This needs to internationalised - setup("serv.log", c.Bool("debug")) + setup(ctx, c.Bool("debug")) if setting.SSH.Disabled { println("Gitea: SSH has been disabled") @@ -135,18 +157,18 @@ func runServ(c *cli.Context) error { keys := strings.Split(c.Args()[0], "-") if len(keys) != 2 || keys[0] != "key" { - return fail("Key ID format error", "Invalid key argument: %s", c.Args()[0]) + return fail(ctx, "Key ID format error", "Invalid key argument: %s", c.Args()[0]) } keyID, err := strconv.ParseInt(keys[1], 10, 64) if err != nil { - return fail("Key ID format error", "Invalid key argument: %s", c.Args()[1]) + return fail(ctx, "Key ID parsing error", "Invalid key argument: %s", c.Args()[1]) } cmd := os.Getenv("SSH_ORIGINAL_COMMAND") if len(cmd) == 0 { key, user, err := private.ServNoCommand(ctx, keyID) if err != nil { - return fail("Internal error", "Failed to check provided key: %v", err) + return fail(ctx, "Key check failed", "Failed to check provided key: %v", err) } switch key.Type { case asymkey_model.KeyTypeDeploy: @@ -164,7 +186,7 @@ func runServ(c *cli.Context) error { words, err := shellquote.Split(cmd) if err != nil { - return fail("Error parsing arguments", "Failed to parse arguments: %v", err) + return fail(ctx, "Error parsing arguments", "Failed to parse arguments: %v", err) } if len(words) < 2 { @@ -175,7 +197,7 @@ func runServ(c *cli.Context) error { return nil } } - return fail("Too few arguments", "Too few arguments in cmd: %s", cmd) + return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd) } verb := words[0] @@ -187,7 +209,7 @@ func runServ(c *cli.Context) error { var lfsVerb string if verb == lfsAuthenticateVerb { if !setting.LFS.StartServer { - return fail("Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled") + return fail(ctx, "Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled") } if len(words) > 2 { @@ -200,37 +222,37 @@ func runServ(c *cli.Context) error { rr := strings.SplitN(repoPath, "/", 2) if len(rr) != 2 { - return fail("Invalid repository path", "Invalid repository path: %v", repoPath) + return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath) } username := strings.ToLower(rr[0]) reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git")) if alphaDashDotPattern.MatchString(reponame) { - return fail("Invalid repo name", "Invalid repo name: %s", reponame) + return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) } if c.Bool("enable-pprof") { if err := os.MkdirAll(setting.PprofDataPath, os.ModePerm); err != nil { - return fail("Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err) + return fail(ctx, "Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err) } stopCPUProfiler, err := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username) if err != nil { - return fail("Internal Server Error", "Unable to start CPU profile: %v", err) + return fail(ctx, "Unable to start CPU profiler", "Unable to start CPU profile: %v", err) } defer func() { stopCPUProfiler() err := pprof.DumpMemProfileForUsername(setting.PprofDataPath, username) if err != nil { - _ = fail("Internal Server Error", "Unable to dump Mem Profile: %v", err) + _ = fail(ctx, "Unable to dump Mem profile", "Unable to dump Mem Profile: %v", err) } }() } requestedMode, has := allowedCommands[verb] if !has { - return fail("Unknown git command", "Unknown git command %s", verb) + return fail(ctx, "Unknown git command", "Unknown git command %s", verb) } if verb == lfsAuthenticateVerb { @@ -239,20 +261,13 @@ func runServ(c *cli.Context) error { } else if lfsVerb == "download" { requestedMode = perm.AccessModeRead } else { - return fail("Unknown LFS verb", "Unknown lfs verb %s", lfsVerb) + return fail(ctx, "Unknown LFS verb", "Unknown lfs verb %s", lfsVerb) } } - results, err := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) - if err != nil { - if private.IsErrServCommand(err) { - errServCommand := err.(private.ErrServCommand) - if errServCommand.StatusCode != http.StatusInternalServerError { - return fail("Unauthorized", "%s", errServCommand.Error()) - } - return fail("Internal Server Error", "%s", errServCommand.Error()) - } - return fail("Internal Server Error", "%s", err.Error()) + 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 token authentication @@ -274,7 +289,7 @@ func runServ(c *cli.Context) error { // 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("Internal error", "Failed to sign JWT token: %v", err) + return fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err) } tokenAuthentication := &git_model.LFSTokenResponse{ @@ -286,7 +301,7 @@ func runServ(c *cli.Context) error { enc := json.NewEncoder(os.Stdout) err = enc.Encode(tokenAuthentication) if err != nil { - return fail("Internal error", "Failed to encode LFS json response: %v", err) + return fail(ctx, "Failed to encode LFS json response", "Failed to encode LFS json response: %v", err) } return nil } @@ -332,13 +347,13 @@ func runServ(c *cli.Context) error { gitcmd.Env = append(gitcmd.Env, git.CommonCmdServEnvs()...) if err = gitcmd.Run(); err != nil { - return fail("Internal error", "Failed to execute git command: %v", err) + return fail(ctx, "Failed to execute git command", "Failed to execute git command: %v", err) } // Update user key activity. if results.KeyID > 0 { if err = private.UpdatePublicKeyInRepo(ctx, results.KeyID, results.RepoID); err != nil { - return fail("Internal error", "UpdatePublicKeyInRepo: %v", err) + return fail(ctx, "Failed to update public key", "UpdatePublicKeyInRepo: %v", err) } } |