diff options
Diffstat (limited to 'modules/private')
-rw-r--r-- | modules/private/hook.go | 125 | ||||
-rw-r--r-- | modules/private/internal.go | 56 | ||||
-rw-r--r-- | modules/private/key.go | 35 | ||||
-rw-r--r-- | modules/private/mail.go | 34 | ||||
-rw-r--r-- | modules/private/manager.go | 171 | ||||
-rw-r--r-- | modules/private/request.go | 135 | ||||
-rw-r--r-- | modules/private/restore_repo.go | 34 | ||||
-rw-r--r-- | modules/private/serv.go | 65 |
8 files changed, 236 insertions, 419 deletions
diff --git a/modules/private/hook.go b/modules/private/hook.go index 9533eaae59..0563e4d80a 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -5,14 +5,11 @@ package private import ( "context" - "errors" "fmt" - "net/http" "net/url" "strconv" "time" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" ) @@ -99,126 +96,46 @@ type HookProcReceiveRefResult struct { } // HookPreReceive check whether the provided commits are allowed -func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (int, string) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", - url.PathEscape(ownerName), - url.PathEscape(repoName), - ) - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - jsonBytes, _ := json.Marshal(opts) - req.Body(jsonBytes) - req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "" +func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) + req := newInternalRequest(ctx, reqURL, "POST", opts) + req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + _, extra := requestJSONResp(req, &responseText{}) + return extra } // HookPostReceive updates services and users -func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, string) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", - url.PathEscape(ownerName), - url.PathEscape(repoName), - ) - - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) - jsonBytes, _ := json.Marshal(opts) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return nil, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, decodeJSONError(resp).Err - } - res := &HookPostReceiveResult{} - _ = json.NewDecoder(resp.Body).Decode(res) - - return res, "" +func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) + req := newInternalRequest(ctx, reqURL, "POST", opts) + req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + return requestJSONResp(req, &HookPostReceiveResult{}) } // HookProcReceive proc-receive hook -func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", - url.PathEscape(ownerName), - url.PathEscape(repoName), - ) - - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) - jsonBytes, _ := json.Marshal(opts) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return nil, fmt.Errorf("Unable to contact gitea: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, errors.New(decodeJSONError(resp).Err) - } - res := &HookProcReceiveResult{} - _ = json.NewDecoder(resp.Body).Decode(res) +func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - return res, nil + req := newInternalRequest(ctx, reqURL, "POST", opts) + req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + return requestJSONResp(req, &HookProcReceiveResult{}) } // SetDefaultBranch will set the default branch to the provided branch for the provided repository -func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) error { +func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/set-default-branch/%s/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName), url.PathEscape(branch), ) req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - - req.SetTimeout(60*time.Second, 60*time.Second) - resp, err := req.Response() - if err != nil { - return fmt.Errorf("Unable to contact gitea: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("Error returned from gitea: %v", decodeJSONError(resp).Err) - } - return nil + return requestJSONUserMsg(req, "") } // SSHLog sends ssh error log response func SSHLog(ctx context.Context, isErr bool, msg string) error { reqURL := setting.LocalURL + "api/internal/ssh/log" - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - - jsonBytes, _ := json.Marshal(&SSHLogOption{ - IsError: isErr, - Message: msg, - }) - req.Body(jsonBytes) - - req.SetTimeout(60*time.Second, 60*time.Second) - resp, err := req.Response() - if err != nil { - return fmt.Errorf("unable to contact gitea: %w", err) - } - - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("Error returned from gitea: %v", decodeJSONError(resp).Err) - } - return nil + req := newInternalRequest(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg}) + _, extra := requestJSONResp(req, &responseText{}) + return extra.Error } diff --git a/modules/private/internal.go b/modules/private/internal.go index a8b62fdde7..9c330a24a8 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "strings" + "time" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" @@ -19,29 +20,10 @@ import ( "code.gitea.io/gitea/modules/setting" ) -func newRequest(ctx context.Context, url, method, sourceIP string) *httplib.Request { - if setting.InternalToken == "" { - log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. -Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf) - } - return httplib.NewRequest(url, method). - SetContext(ctx). - Header("X-Real-IP", sourceIP). - Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)) -} - -// Response internal request response +// Response is used for internal request response (for user message and error message) type Response struct { - Err string `json:"err"` -} - -func decodeJSONError(resp *http.Response) *Response { - var res Response - err := json.NewDecoder(resp.Body).Decode(&res) - if err != nil { - res.Err = err.Error() - } - return &res + Err string `json:"err,omitempty"` // server-side error log message, it won't be exposed to end users + UserMsg string `json:"user_msg,omitempty"` // meaningful error message for end users, it will be shown in git client's output. } func getClientIP() string { @@ -52,11 +34,21 @@ func getClientIP() string { return strings.Fields(sshConnEnv)[0] } -func newInternalRequest(ctx context.Context, url, method string) *httplib.Request { - req := newRequest(ctx, url, method, getClientIP()).SetTLSClientConfig(&tls.Config{ - InsecureSkipVerify: true, - ServerName: setting.Domain, - }) +func newInternalRequest(ctx context.Context, url, method string, body ...any) *httplib.Request { + if setting.InternalToken == "" { + log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. +Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf) + } + + req := httplib.NewRequest(url, method). + SetContext(ctx). + Header("X-Real-IP", getClientIP()). + Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)). + SetTLSClientConfig(&tls.Config{ + InsecureSkipVerify: true, + ServerName: setting.Domain, + }) + if setting.Protocol == setting.HTTPUnix { req.SetTransport(&http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { @@ -90,5 +82,15 @@ func newInternalRequest(ctx context.Context, url, method string) *httplib.Reques }, }) } + + if len(body) == 1 { + req.Header("Content-Type", "application/json") + jsonBytes, _ := json.Marshal(body[0]) + req.Body(jsonBytes) + } else if len(body) > 1 { + log.Fatal("Too many arguments for newInternalRequest") + } + + req.SetTimeout(10*time.Second, 60*time.Second) return req } diff --git a/modules/private/key.go b/modules/private/key.go index f09d6de2bf..6f7cd87796 100644 --- a/modules/private/key.go +++ b/modules/private/key.go @@ -6,8 +6,6 @@ package private import ( "context" "fmt" - "io" - "net/http" "code.gitea.io/gitea/modules/setting" ) @@ -16,39 +14,18 @@ import ( func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error { // Ask for running deliver hook and test pull request tasks. reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID) - resp, err := newInternalRequest(ctx, reqURL, "POST").Response() - if err != nil { - return err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return fmt.Errorf("Failed to update public key: %s", decodeJSONError(resp).Err) - } - return nil + req := newInternalRequest(ctx, reqURL, "POST") + _, extra := requestJSONResp(req, &responseText{}) + return extra.Error } // AuthorizedPublicKeyByContent searches content as prefix (leak e-mail part) // and returns public key found. -func AuthorizedPublicKeyByContent(ctx context.Context, content string) (string, error) { +func AuthorizedPublicKeyByContent(ctx context.Context, content string) (string, ResponseExtra) { // Ask for running deliver hook and test pull request tasks. reqURL := setting.LocalURL + "api/internal/ssh/authorized_keys" req := newInternalRequest(ctx, reqURL, "POST") req.Param("content", content) - resp, err := req.Response() - if err != nil { - return "", err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("Failed to update public key: %s", decodeJSONError(resp).Err) - } - bs, err := io.ReadAll(resp.Body) - - return string(bs), err + resp, extra := requestJSONResp(req, &responseText{}) + return resp.Text, extra } diff --git a/modules/private/mail.go b/modules/private/mail.go index 6eb7c2acd0..82216b346b 100644 --- a/modules/private/mail.go +++ b/modules/private/mail.go @@ -5,11 +5,7 @@ package private import ( "context" - "fmt" - "io" - "net/http" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" ) @@ -21,38 +17,18 @@ type Email struct { } // SendEmail calls the internal SendEmail function -// // It accepts a list of usernames. // If DB contains these users it will send the email to them. -// -// If to list == nil its supposed to send an email to every -// user present in DB -func SendEmail(ctx context.Context, subject, message string, to []string) (int, string) { +// If to list == nil, it's supposed to send emails to every user present in DB +func SendEmail(ctx context.Context, subject, message string, to []string) (string, ResponseExtra) { reqURL := setting.LocalURL + "api/internal/mail/send" - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - jsonBytes, _ := json.Marshal(Email{ + req := newInternalRequest(ctx, reqURL, "POST", Email{ Subject: subject, Message: message, To: to, }) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Response body error: %v", err.Error()) - } - - users := fmt.Sprintf("%d", len(to)) - if len(to) == 0 { - users = "all" - } - - return http.StatusOK, fmt.Sprintf("Sent %s email(s) to %s users", body, users) + resp, extra := requestJSONResp(req, &responseText{}) + return resp.Text, extra } diff --git a/modules/private/manager.go b/modules/private/manager.go index bbf470cd7a..5853db34e4 100644 --- a/modules/private/manager.go +++ b/modules/private/manager.go @@ -12,44 +12,21 @@ import ( "strconv" "time" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" ) // Shutdown calls the internal shutdown function -func Shutdown(ctx context.Context) (int, string) { +func Shutdown(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/shutdown" - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Shutting down" + return requestJSONUserMsg(req, "Shutting down") } // Restart calls the internal restart function -func Restart(ctx context.Context) (int, string) { +func Restart(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/restart" - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Restarting" + return requestJSONUserMsg(req, "Restarting") } // FlushOptions represents the options for the flush call @@ -59,102 +36,41 @@ type FlushOptions struct { } // FlushQueues calls the internal flush-queues function -func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) (int, string) { +func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/flush-queues" - - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequest(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking}) if timeout > 0 { - req.SetTimeout(timeout+10*time.Second, timeout+10*time.Second) - } - req = req.Header("Content-Type", "application/json") - jsonBytes, _ := json.Marshal(FlushOptions{ - Timeout: timeout, - NonBlocking: nonBlocking, - }) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err + req.SetReadWriteTimeout(timeout + 10*time.Second) } - - return http.StatusOK, "Flushed" + return requestJSONUserMsg(req, "Flushed") } // PauseLogging pauses logging -func PauseLogging(ctx context.Context) (int, string) { +func PauseLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/pause-logging" - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Logging Paused" + return requestJSONUserMsg(req, "Logging Paused") } // ResumeLogging resumes logging -func ResumeLogging(ctx context.Context) (int, string) { +func ResumeLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/resume-logging" - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Logging Restarted" + return requestJSONUserMsg(req, "Logging Restarted") } // ReleaseReopenLogging releases and reopens logging files -func ReleaseReopenLogging(ctx context.Context) (int, string) { +func ReleaseReopenLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging" - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Logging Restarted" + return requestJSONUserMsg(req, "Logging Restarted") } // SetLogSQL sets database logging -func SetLogSQL(ctx context.Context, on bool) (int, string) { +func SetLogSQL(ctx context.Context, on bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on) - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Log SQL setting set" + return requestJSONUserMsg(req, "Log SQL setting set") } // LoggerOptions represents the options for the add logger call @@ -166,67 +82,32 @@ type LoggerOptions struct { } // AddLogger adds a logger -func AddLogger(ctx context.Context, group, name, mode string, config map[string]interface{}) (int, string) { +func AddLogger(ctx context.Context, group, name, mode string, config map[string]interface{}) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/add-logger" - - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - jsonBytes, _ := json.Marshal(LoggerOptions{ + req := newInternalRequest(ctx, reqURL, "POST", LoggerOptions{ Group: group, Name: name, Mode: mode, Config: config, }) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Added" + return requestJSONUserMsg(req, "Added") } // RemoveLogger removes a logger -func RemoveLogger(ctx context.Context, group, name string) (int, string) { +func RemoveLogger(ctx context.Context, group, name string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(group), url.PathEscape(name)) - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Removed" + return requestJSONUserMsg(req, "Removed") } // Processes return the current processes from this gitea instance -func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) (int, string) { +func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/processes?flat=%t&no-system=%t&stacktraces=%t&json=%t&cancel-pid=%s", flat, noSystem, stacktraces, json, url.QueryEscape(cancel)) req := newInternalRequest(ctx, reqURL, "GET") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - _, err = io.Copy(out, resp.Body) - if err != nil { - return http.StatusInternalServerError, err.Error() + callback := func(resp *http.Response, extra *ResponseExtra) { + _, extra.Error = io.Copy(out, resp.Body) } - return http.StatusOK, "" + _, extra := requestJSONResp(req, &callback) + return extra } diff --git a/modules/private/request.go b/modules/private/request.go new file mode 100644 index 0000000000..3eb8c92c1a --- /dev/null +++ b/modules/private/request.go @@ -0,0 +1,135 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "fmt" + "io" + "net/http" + "unicode" + + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/json" +) + +// responseText is used to get the response as text, instead of parsing it as JSON. +type responseText struct { + Text string +} + +// ResponseExtra contains extra information about the response, especially for error responses. +type ResponseExtra struct { + StatusCode int + UserMsg string + Error error +} + +type responseCallback func(resp *http.Response, extra *ResponseExtra) + +func (re *ResponseExtra) HasError() bool { + return re.Error != nil +} + +type responseError struct { + statusCode int + errorString string +} + +func (re responseError) Error() string { + if re.errorString == "" { + return fmt.Sprintf("internal API error response, status=%d", re.statusCode) + } + return fmt.Sprintf("internal API error response, status=%d, err=%s", re.statusCode, re.errorString) +} + +// requestJSONUserMsg sends a request to the gitea server and then parses the response. +// If the status code is not 2xx, or any error occurs, the ResponseExtra.Error field is guaranteed to be non-nil, +// and the ResponseExtra.UserMsg field will be set to a message for the end user. +// +// * If the "res" is a struct pointer, the response will be parsed as JSON +// * If the "res" is responseText pointer, the response will be stored as text in it +// * If the "res" is responseCallback pointer, the callback function should set the ResponseExtra fields accordingly +func requestJSONResp[T any](req *httplib.Request, res *T) (ret *T, extra ResponseExtra) { + resp, err := req.Response() + if err != nil { + extra.UserMsg = "Internal Server Connection Error" + extra.Error = fmt.Errorf("unable to contact gitea %q: %w", req.GoString(), err) + return nil, extra + } + defer resp.Body.Close() + + extra.StatusCode = resp.StatusCode + + // if the status code is not 2xx, try to parse the error response + if resp.StatusCode/100 != 2 { + var respErr Response + if err := json.NewDecoder(resp.Body).Decode(&respErr); err != nil { + extra.UserMsg = "Internal Server Error Decoding Failed" + extra.Error = fmt.Errorf("unable to decode error response %q: %w", req.GoString(), err) + return nil, extra + } + extra.UserMsg = respErr.UserMsg + if extra.UserMsg == "" { + extra.UserMsg = "Internal Server Error (no message for end users)" + } + extra.Error = responseError{statusCode: resp.StatusCode, errorString: respErr.Err} + return res, extra + } + + // now, the StatusCode must be 2xx + var v any = res + if respText, ok := v.(*responseText); ok { + // get the whole response as a text string + bs, err := io.ReadAll(resp.Body) + if err != nil { + extra.UserMsg = "Internal Server Response Reading Failed" + extra.Error = fmt.Errorf("unable to read response %q: %w", req.GoString(), err) + return nil, extra + } + respText.Text = string(bs) + return res, extra + } else if callback, ok := v.(*responseCallback); ok { + // pass the response to callback, and let the callback update the ResponseExtra + extra.StatusCode = resp.StatusCode + (*callback)(resp, &extra) + return nil, extra + } else if err := json.NewDecoder(resp.Body).Decode(res); err != nil { + // decode the response into the given struct + extra.UserMsg = "Internal Server Response Decoding Failed" + extra.Error = fmt.Errorf("unable to decode response %q: %w", req.GoString(), err) + return nil, extra + } + + if respMsg, ok := v.(*Response); ok { + // if the "res" is Response structure, try to get the UserMsg from it and update the ResponseExtra + extra.UserMsg = respMsg.UserMsg + if respMsg.Err != "" { + // usually this shouldn't happen, because the StatusCode is 2xx, there should be no error. + // but we still handle the "err" response, in case some people return error messages by status code 200. + extra.Error = responseError{statusCode: resp.StatusCode, errorString: respMsg.Err} + } + } + + return res, extra +} + +// requestJSONUserMsg sends a request to the gitea server and then parses the response as private.Response +// If the request succeeds, the successMsg will be used as part of ResponseExtra.UserMsg. +func requestJSONUserMsg(req *httplib.Request, successMsg string) ResponseExtra { + resp, extra := requestJSONResp(req, &Response{}) + if extra.HasError() { + return extra + } + if resp.UserMsg == "" { + extra.UserMsg = successMsg // if UserMsg is empty, then use successMsg as userMsg + } else if successMsg != "" { + // else, now UserMsg is not empty, if successMsg is not empty, then append successMsg to UserMsg + if unicode.IsPunct(rune(extra.UserMsg[len(extra.UserMsg)-1])) { + extra.UserMsg = extra.UserMsg + " " + successMsg + } else { + extra.UserMsg = extra.UserMsg + ". " + successMsg + } + } + return extra +} diff --git a/modules/private/restore_repo.go b/modules/private/restore_repo.go index f40d914a7b..34d0f5d482 100644 --- a/modules/private/restore_repo.go +++ b/modules/private/restore_repo.go @@ -6,11 +6,8 @@ package private import ( "context" "fmt" - "io" - "net/http" "time" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" ) @@ -24,39 +21,16 @@ type RestoreParams struct { } // RestoreRepo calls the internal RestoreRepo function -func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) (int, string) { +func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/restore_repo" - req := newInternalRequest(ctx, reqURL, "POST") - req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout - req = req.Header("Content-Type", "application/json") - jsonBytes, _ := json.Marshal(RestoreParams{ + req := newInternalRequest(ctx, reqURL, "POST", RestoreParams{ RepoDir: repoDir, OwnerName: ownerName, RepoName: repoName, Units: units, Validation: validation, }) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v, could you confirm it's running?", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - ret := struct { - Err string `json:"err"` - }{} - body, err := io.ReadAll(resp.Body) - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Response body error: %v", err.Error()) - } - if err := json.Unmarshal(body, &ret); err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Response body Unmarshal error: %v", err.Error()) - } - return http.StatusInternalServerError, ret.Err - } - - return http.StatusOK, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName) + req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout + return requestJSONUserMsg(req, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName)) } diff --git a/modules/private/serv.go b/modules/private/serv.go index c176e1ddfc..480a446954 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -6,13 +6,11 @@ package private import ( "context" "fmt" - "net/http" "net/url" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/perm" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" ) @@ -24,20 +22,11 @@ type KeyAndOwner struct { // ServNoCommand returns information about the provided key func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey, *user_model.User, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", - keyID) - resp, err := newInternalRequest(ctx, reqURL, "GET").Response() - if err != nil { - return nil, nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, nil, fmt.Errorf("%s", decodeJSONError(resp).Err) - } - - var keyAndOwner KeyAndOwner - if err := json.NewDecoder(resp.Body).Decode(&keyAndOwner); err != nil { - return nil, nil, err + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", keyID) + req := newInternalRequest(ctx, reqURL, "GET") + keyAndOwner, extra := requestJSONResp(req, &KeyAndOwner{}) + if extra.HasError() { + return nil, nil, extra.Error } return keyAndOwner.Key, keyAndOwner.Owner, nil } @@ -56,53 +45,19 @@ type ServCommandResults struct { RepoID int64 } -// ErrServCommand is an error returned from ServCommmand. -type ErrServCommand struct { - Results ServCommandResults - Err string - StatusCode int -} - -func (err ErrServCommand) Error() string { - return err.Err -} - -// IsErrServCommand checks if an error is a ErrServCommand. -func IsErrServCommand(err error) bool { - _, ok := err.(ErrServCommand) - return ok -} - // ServCommand preps for a serv call -func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, error) { +func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, ResponseExtra) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d", keyID, url.PathEscape(ownerName), url.PathEscape(repoName), - mode) + mode, + ) for _, verb := range verbs { if verb != "" { reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb)) } } - - resp, err := newInternalRequest(ctx, reqURL, "GET").Response() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - var errServCommand ErrServCommand - if err := json.NewDecoder(resp.Body).Decode(&errServCommand); err != nil { - return nil, err - } - errServCommand.StatusCode = resp.StatusCode - return nil, errServCommand - } - var results ServCommandResults - if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { - return nil, err - } - return &results, nil + req := newInternalRequest(ctx, reqURL, "GET") + return requestJSONResp(req, &ServCommandResults{}) } |