first-commit
This commit is contained in:
25
modules/private/actions.go
Normal file
25
modules/private/actions.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
type GenerateTokenRequest struct {
|
||||
Scope string
|
||||
}
|
||||
|
||||
// GenerateActionsRunnerToken calls the internal GenerateActionsRunnerToken function
|
||||
func GenerateActionsRunnerToken(ctx context.Context, scope string) (*ResponseText, ResponseExtra) {
|
||||
reqURL := setting.LocalURL + "api/internal/actions/generate_actions_runner_token"
|
||||
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", GenerateTokenRequest{
|
||||
Scope: scope,
|
||||
})
|
||||
|
||||
return requestJSONResp(req, &ResponseText{})
|
||||
}
|
132
modules/private/hook.go
Normal file
132
modules/private/hook.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// Git environment variables
|
||||
const (
|
||||
GitAlternativeObjectDirectories = "GIT_ALTERNATE_OBJECT_DIRECTORIES"
|
||||
GitObjectDirectory = "GIT_OBJECT_DIRECTORY"
|
||||
GitQuarantinePath = "GIT_QUARANTINE_PATH"
|
||||
GitPushOptionCount = "GIT_PUSH_OPTION_COUNT"
|
||||
)
|
||||
|
||||
// HookOptions represents the options for the Hook calls
|
||||
type HookOptions struct {
|
||||
OldCommitIDs []string
|
||||
NewCommitIDs []string
|
||||
RefFullNames []git.RefName
|
||||
UserID int64
|
||||
UserName string
|
||||
GitObjectDirectory string
|
||||
GitAlternativeObjectDirectories string
|
||||
GitQuarantinePath string
|
||||
GitPushOptions GitPushOptions
|
||||
PullRequestID int64
|
||||
PushTrigger repository.PushTrigger
|
||||
DeployKeyID int64 // if the pusher is a DeployKey, then UserID is the repo's org user.
|
||||
IsWiki bool
|
||||
ActionPerm int
|
||||
}
|
||||
|
||||
// SSHLogOption ssh log options
|
||||
type SSHLogOption struct {
|
||||
IsError bool
|
||||
Message string
|
||||
}
|
||||
|
||||
// HookPostReceiveResult represents an individual result from PostReceive
|
||||
type HookPostReceiveResult struct {
|
||||
Results []HookPostReceiveBranchResult
|
||||
RepoWasEmpty bool
|
||||
Err string
|
||||
}
|
||||
|
||||
// HookPostReceiveBranchResult represents an individual branch result from PostReceive
|
||||
type HookPostReceiveBranchResult struct {
|
||||
Message bool
|
||||
Create bool
|
||||
Branch string
|
||||
URL string
|
||||
}
|
||||
|
||||
// HookProcReceiveResult represents an individual result from ProcReceive
|
||||
type HookProcReceiveResult struct {
|
||||
Results []HookProcReceiveRefResult
|
||||
Err string
|
||||
}
|
||||
|
||||
// HookProcReceiveRefResult represents an individual result from ProcReceive
|
||||
type HookProcReceiveRefResult struct {
|
||||
OldOID string
|
||||
NewOID string
|
||||
Ref string
|
||||
OriginalRef git.RefName
|
||||
IsForcePush bool
|
||||
IsNotMatched bool
|
||||
Err string
|
||||
IsCreatePR bool
|
||||
URL string
|
||||
ShouldShowMessage bool
|
||||
HeadBranch string
|
||||
}
|
||||
|
||||
func newInternalRequestAPIForHooks(ctx context.Context, hookName, ownerName, repoName string, opts HookOptions) *httplib.Request {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/%s/%s/%s", hookName, url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
|
||||
// This "timeout" applies to http.Client's timeout: A Timeout of zero means no timeout.
|
||||
// This "timeout" was previously set to `time.Duration(60+len(opts.OldCommitIDs))` seconds, but it caused unnecessary timeout failures.
|
||||
// It should be good enough to remove the client side timeout, only respect the "ctx" and server side timeout.
|
||||
req.SetReadWriteTimeout(0)
|
||||
return req
|
||||
}
|
||||
|
||||
// HookPreReceive check whether the provided commits are allowed
|
||||
func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra {
|
||||
req := newInternalRequestAPIForHooks(ctx, "pre-receive", ownerName, repoName, opts)
|
||||
_, extra := requestJSONResp(req, &ResponseText{})
|
||||
return extra
|
||||
}
|
||||
|
||||
// HookPostReceive updates services and users
|
||||
func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) {
|
||||
req := newInternalRequestAPIForHooks(ctx, "post-receive", ownerName, repoName, opts)
|
||||
return requestJSONResp(req, &HookPostReceiveResult{})
|
||||
}
|
||||
|
||||
// HookProcReceive proc-receive hook
|
||||
func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) {
|
||||
req := newInternalRequestAPIForHooks(ctx, "proc-receive", ownerName, repoName, opts)
|
||||
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) 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 := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
_, extra := requestJSONResp(req, &ResponseText{})
|
||||
return extra
|
||||
}
|
||||
|
||||
// SSHLog sends ssh error log response
|
||||
func SSHLog(ctx context.Context, isErr bool, msg string) error {
|
||||
reqURL := setting.LocalURL + "api/internal/ssh/log"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg})
|
||||
_, extra := requestJSONResp(req, &ResponseText{})
|
||||
return extra.Error
|
||||
}
|
103
modules/private/internal.go
Normal file
103
modules/private/internal.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/proxyprotocol"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// Response is used for internal request response (for user message and error message)
|
||||
type Response struct {
|
||||
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 {
|
||||
sshConnEnv := strings.TrimSpace(os.Getenv("SSH_CONNECTION"))
|
||||
if len(sshConnEnv) == 0 {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
return strings.Fields(sshConnEnv)[0]
|
||||
}
|
||||
|
||||
func NewInternalRequest(ctx context.Context, url, method 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)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(url, setting.LocalURL) {
|
||||
log.Fatal("Invalid internal request URL: %q", url)
|
||||
}
|
||||
|
||||
req := httplib.NewRequest(url, method).
|
||||
SetContext(ctx).
|
||||
Header("X-Real-IP", getClientIP()).
|
||||
Header("X-Gitea-Internal-Auth", "Bearer "+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) {
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr)
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
if setting.LocalUseProxyProtocol {
|
||||
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return conn, err
|
||||
},
|
||||
})
|
||||
} else if setting.LocalUseProxyProtocol {
|
||||
req.SetTransport(&http.Transport{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, err
|
||||
},
|
||||
})
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) *httplib.Request {
|
||||
req := NewInternalRequest(ctx, url, method)
|
||||
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 newInternalRequestAPI")
|
||||
}
|
||||
|
||||
req.SetTimeout(10*time.Second, 60*time.Second)
|
||||
return req
|
||||
}
|
30
modules/private/key.go
Normal file
30
modules/private/key.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// UpdatePublicKeyInRepo update public key and if necessary deploy key updates
|
||||
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)
|
||||
req := newInternalRequestAPI(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) (*ResponseText, ResponseExtra) {
|
||||
// Ask for running deliver hook and test pull request tasks.
|
||||
reqURL := setting.LocalURL + "api/internal/ssh/authorized_keys"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
req.Param("content", content)
|
||||
return requestJSONResp(req, &ResponseText{})
|
||||
}
|
33
modules/private/mail.go
Normal file
33
modules/private/mail.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// Email structure holds a data for sending general emails
|
||||
type Email struct {
|
||||
Subject string
|
||||
Message string
|
||||
To []string
|
||||
}
|
||||
|
||||
// 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, it's supposed to send emails to every user present in DB
|
||||
func SendEmail(ctx context.Context, subject, message string, to []string) (*ResponseText, ResponseExtra) {
|
||||
reqURL := setting.LocalURL + "api/internal/mail/send"
|
||||
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", Email{
|
||||
Subject: subject,
|
||||
Message: message,
|
||||
To: to,
|
||||
})
|
||||
|
||||
return requestJSONResp(req, &ResponseText{})
|
||||
}
|
120
modules/private/manager.go
Normal file
120
modules/private/manager.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// Shutdown calls the internal shutdown function
|
||||
func Shutdown(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/shutdown"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Shutting down")
|
||||
}
|
||||
|
||||
// Restart calls the internal restart function
|
||||
func Restart(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/restart"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Restarting")
|
||||
}
|
||||
|
||||
// ReloadTemplates calls the internal reload-templates function
|
||||
func ReloadTemplates(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/reload-templates"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Reloaded")
|
||||
}
|
||||
|
||||
// FlushOptions represents the options for the flush call
|
||||
type FlushOptions struct {
|
||||
Timeout time.Duration
|
||||
NonBlocking bool
|
||||
}
|
||||
|
||||
// FlushQueues calls the internal flush-queues function
|
||||
func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/flush-queues"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking})
|
||||
if timeout > 0 {
|
||||
req.SetReadWriteTimeout(timeout + 10*time.Second)
|
||||
}
|
||||
return requestJSONClientMsg(req, "Flushed")
|
||||
}
|
||||
|
||||
// PauseLogging pauses logging
|
||||
func PauseLogging(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/pause-logging"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Logging Paused")
|
||||
}
|
||||
|
||||
// ResumeLogging resumes logging
|
||||
func ResumeLogging(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/resume-logging"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Logging Restarted")
|
||||
}
|
||||
|
||||
// ReleaseReopenLogging releases and reopens logging files
|
||||
func ReleaseReopenLogging(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Logging Restarted")
|
||||
}
|
||||
|
||||
// SetLogSQL sets database logging
|
||||
func SetLogSQL(ctx context.Context, on bool) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on)
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Log SQL setting set")
|
||||
}
|
||||
|
||||
// LoggerOptions represents the options for the add logger call
|
||||
type LoggerOptions struct {
|
||||
Logger string
|
||||
Writer string
|
||||
Mode string
|
||||
Config map[string]any
|
||||
}
|
||||
|
||||
// AddLogger adds a logger
|
||||
func AddLogger(ctx context.Context, logger, writer, mode string, config map[string]any) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/add-logger"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", LoggerOptions{
|
||||
Logger: logger,
|
||||
Writer: writer,
|
||||
Mode: mode,
|
||||
Config: config,
|
||||
})
|
||||
return requestJSONClientMsg(req, "Added")
|
||||
}
|
||||
|
||||
// RemoveLogger removes a logger
|
||||
func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(logger), url.PathEscape(writer))
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(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) 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 := newInternalRequestAPI(ctx, reqURL, "GET")
|
||||
callback := func(resp *http.Response, extra *ResponseExtra) {
|
||||
_, extra.Error = io.Copy(out, resp.Body)
|
||||
}
|
||||
_, extra := requestJSONResp(req, &responseCallback{callback})
|
||||
return extra
|
||||
}
|
45
modules/private/pushoptions.go
Normal file
45
modules/private/pushoptions.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
)
|
||||
|
||||
// GitPushOptions is a wrapper around a map[string]string
|
||||
type GitPushOptions map[string]string
|
||||
|
||||
// GitPushOptions keys
|
||||
const (
|
||||
GitPushOptionRepoPrivate = "repo.private"
|
||||
GitPushOptionRepoTemplate = "repo.template"
|
||||
GitPushOptionForcePush = "force-push"
|
||||
)
|
||||
|
||||
// Bool checks for a key in the map and parses as a boolean
|
||||
// An option without value is considered true, eg: "-o force-push" or "-o repo.private"
|
||||
func (g GitPushOptions) Bool(key string) optional.Option[bool] {
|
||||
if val, ok := g[key]; ok {
|
||||
if val == "" {
|
||||
return optional.Some(true)
|
||||
}
|
||||
if b, err := strconv.ParseBool(val); err == nil {
|
||||
return optional.Some(b)
|
||||
}
|
||||
}
|
||||
return optional.None[bool]()
|
||||
}
|
||||
|
||||
// AddFromKeyValue adds a key value pair to the map by "key=value" format or "key" for empty value
|
||||
func (g GitPushOptions) AddFromKeyValue(line string) {
|
||||
kv := strings.SplitN(line, "=", 2)
|
||||
if len(kv) == 2 {
|
||||
g[kv[0]] = kv[1]
|
||||
} else {
|
||||
g[kv[0]] = ""
|
||||
}
|
||||
}
|
30
modules/private/pushoptions_test.go
Normal file
30
modules/private/pushoptions_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGitPushOptions(t *testing.T) {
|
||||
o := GitPushOptions{}
|
||||
|
||||
v := o.Bool("no-such")
|
||||
assert.False(t, v.Has())
|
||||
assert.False(t, v.Value())
|
||||
|
||||
o.AddFromKeyValue("opt1=a=b")
|
||||
o.AddFromKeyValue("opt2=false")
|
||||
o.AddFromKeyValue("opt3=true")
|
||||
o.AddFromKeyValue("opt4")
|
||||
|
||||
assert.Equal(t, "a=b", o["opt1"])
|
||||
assert.False(t, o.Bool("opt1").Value())
|
||||
assert.True(t, o.Bool("opt2").Has())
|
||||
assert.False(t, o.Bool("opt2").Value())
|
||||
assert.True(t, o.Bool("opt3").Value())
|
||||
assert.True(t, o.Bool("opt4").Value())
|
||||
}
|
128
modules/private/request.go
Normal file
128
modules/private/request.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"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 struct {
|
||||
Callback 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)
|
||||
}
|
||||
|
||||
// requestJSONResp 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.
|
||||
// Caller should check the ResponseExtra.HasError() first to see whether the request fails.
|
||||
//
|
||||
// * 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 cb, ok := v.(*responseCallback); ok {
|
||||
// pass the response to callback, and let the callback update the ResponseExtra
|
||||
extra.StatusCode = resp.StatusCode
|
||||
cb.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
|
||||
}
|
||||
|
||||
// requestJSONClientMsg sends a request to the gitea server, server only responds text message status=200 with "success" body
|
||||
// If the request succeeds (200), the argument clientSuccessMsg will be used as ResponseExtra.UserMsg.
|
||||
func requestJSONClientMsg(req *httplib.Request, clientSuccessMsg string) ResponseExtra {
|
||||
_, extra := requestJSONResp(req, &ResponseText{})
|
||||
if extra.HasError() {
|
||||
return extra
|
||||
}
|
||||
extra.UserMsg = clientSuccessMsg
|
||||
return extra
|
||||
}
|
36
modules/private/restore_repo.go
Normal file
36
modules/private/restore_repo.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// RestoreParams structure holds a data for restore repository
|
||||
type RestoreParams struct {
|
||||
RepoDir string
|
||||
OwnerName string
|
||||
RepoName string
|
||||
Units []string
|
||||
Validation bool
|
||||
}
|
||||
|
||||
// RestoreRepo calls the internal RestoreRepo function
|
||||
func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/restore_repo"
|
||||
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", RestoreParams{
|
||||
RepoDir: repoDir,
|
||||
OwnerName: ownerName,
|
||||
RepoName: repoName,
|
||||
Units: units,
|
||||
Validation: validation,
|
||||
})
|
||||
req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout
|
||||
return requestJSONClientMsg(req, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName))
|
||||
}
|
61
modules/private/serv.go
Normal file
61
modules/private/serv.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/setting"
|
||||
)
|
||||
|
||||
// KeyAndOwner is the response from ServNoCommand
|
||||
type KeyAndOwner struct {
|
||||
Key *asymkey_model.PublicKey `json:"key"`
|
||||
Owner *user_model.User `json:"user"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
req := newInternalRequestAPI(ctx, reqURL, "GET")
|
||||
keyAndOwner, extra := requestJSONResp(req, &KeyAndOwner{})
|
||||
if extra.HasError() {
|
||||
return nil, nil, extra.Error
|
||||
}
|
||||
return keyAndOwner.Key, keyAndOwner.Owner, nil
|
||||
}
|
||||
|
||||
// ServCommandResults are the results of a call to the private route serv
|
||||
type ServCommandResults struct {
|
||||
IsWiki bool
|
||||
DeployKeyID int64
|
||||
KeyID int64 // public key
|
||||
KeyName string // this field is ambiguous, it can be the name of DeployKey, or the name of the PublicKey
|
||||
UserName string
|
||||
UserEmail string
|
||||
UserID int64
|
||||
OwnerName string
|
||||
RepoName string
|
||||
RepoID int64
|
||||
}
|
||||
|
||||
// ServCommand preps for a serv call
|
||||
func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verb, lfsVerb 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,
|
||||
)
|
||||
reqURL += "&verb=" + url.QueryEscape(verb)
|
||||
// reqURL += "&lfs_verb=" + url.QueryEscape(lfsVerb) // TODO: actually there is no use of this parameter. In the future, the URL construction should be more flexible
|
||||
_ = lfsVerb
|
||||
req := newInternalRequestAPI(ctx, reqURL, "GET")
|
||||
return requestJSONResp(req, &ServCommandResults{})
|
||||
}
|
Reference in New Issue
Block a user