diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /routers/private | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
57 files changed, 2757 insertions, 0 deletions
diff --git a/routers/private/actions.go b/routers/private/actions.go new file mode 100644 index 0000000..425c480 --- /dev/null +++ b/routers/private/actions.go @@ -0,0 +1,97 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + gocontext "context" + "errors" + "fmt" + "net/http" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" +) + +// GenerateActionsRunnerToken generates a new runner token for a given scope +func GenerateActionsRunnerToken(ctx *context.PrivateContext) { + var genRequest private.GenerateTokenRequest + rd := ctx.Req.Body + defer rd.Close() + + if err := json.NewDecoder(rd).Decode(&genRequest); err != nil { + log.Error("JSON Decode failed: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + + owner, repo, err := parseScope(ctx, genRequest.Scope) + if err != nil { + log.Error("parseScope failed: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + } + + token, err := actions_model.GetLatestRunnerToken(ctx, owner, repo) + if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) { + token, err = actions_model.NewRunnerToken(ctx, owner, repo) + if err != nil { + errMsg := fmt.Sprintf("error while creating runner token: %v", err) + log.Error("NewRunnerToken failed: %v", errMsg) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: errMsg, + }) + return + } + } else if err != nil { + errMsg := fmt.Sprintf("could not get unactivated runner token: %v", err) + log.Error("GetLatestRunnerToken failed: %v", errMsg) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: errMsg, + }) + return + } + + ctx.PlainText(http.StatusOK, token.Token) +} + +func ParseScope(ctx gocontext.Context, scope string) (ownerID, repoID int64, err error) { + return parseScope(ctx, scope) +} + +func parseScope(ctx gocontext.Context, scope string) (ownerID, repoID int64, err error) { + ownerID = 0 + repoID = 0 + if scope == "" { + return ownerID, repoID, nil + } + + ownerName, repoName, found := strings.Cut(scope, "/") + + u, err := user_model.GetUserByName(ctx, ownerName) + if err != nil { + return ownerID, repoID, err + } + ownerID = u.ID + + if !found { + return ownerID, repoID, nil + } + + r, err := repo_model.GetRepositoryByName(ctx, u.ID, repoName) + if err != nil { + return ownerID, repoID, err + } + repoID = r.ID + return ownerID, repoID, nil +} diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go new file mode 100644 index 0000000..33890be --- /dev/null +++ b/routers/private/default_branch.go @@ -0,0 +1,40 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "fmt" + "net/http" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/private" + gitea_context "code.gitea.io/gitea/services/context" +) + +// SetDefaultBranch updates the default branch +func SetDefaultBranch(ctx *gitea_context.PrivateContext) { + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + branch := ctx.Params(":branch") + + ctx.Repo.Repository.DefaultBranch = branch + if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { + if !git.IsErrUnsupportedVersion(err) { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + } + + if err := repo_model.UpdateDefaultBranch(ctx, ctx.Repo.Repository); err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + ctx.PlainText(http.StatusOK, "success") +} diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go new file mode 100644 index 0000000..11d1161 --- /dev/null +++ b/routers/private/hook_post_receive.go @@ -0,0 +1,368 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "context" + "fmt" + "net/http" + "strconv" + "time" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" + pull_model "code.gitea.io/gitea/models/pull" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/pushoptions" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + timeutil "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + gitea_context "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" +) + +// HookPostReceive updates services and users +func HookPostReceive(ctx *gitea_context.PrivateContext) { + opts := web.GetForm(ctx).(*private.HookOptions) + + // We don't rely on RepoAssignment here because: + // a) we don't need the git repo in this function + // OUT OF DATE: we do need the git repo to sync the branch to the db now. + // b) our update function will likely change the repository in the db so we will need to refresh it + // c) we don't always need the repo + + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + + // defer getting the repository at this point - as we should only retrieve it if we're going to call update + var ( + repo *repo_model.Repository + gitRepo *git.Repository + ) + defer gitRepo.Close() // it's safe to call Close on a nil pointer + + updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs)) + wasEmpty := false + + for i := range opts.OldCommitIDs { + refFullName := opts.RefFullNames[i] + + // Only trigger activity updates for changes to branches or + // tags. Updates to other refs (eg, refs/notes, refs/changes, + // or other less-standard refs spaces are ignored since there + // may be a very large number of them). + if refFullName.IsBranch() || refFullName.IsTag() { + if repo == nil { + repo = loadRepository(ctx, ownerName, repoName) + if ctx.Written() { + // Error handled in loadRepository + return + } + wasEmpty = repo.IsEmpty + } + + option := &repo_module.PushUpdateOptions{ + RefFullName: refFullName, + OldCommitID: opts.OldCommitIDs[i], + NewCommitID: opts.NewCommitIDs[i], + PusherID: opts.UserID, + PusherName: opts.UserName, + RepoUserName: ownerName, + RepoName: repoName, + TimeNano: time.Now().UnixNano(), + } + updates = append(updates, option) + if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") { + // put the master/main branch first + // FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates. + // If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once. + // See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27 + // If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch. + copy(updates[1:], updates) + updates[0] = option + } + } + } + + if repo != nil && len(updates) > 0 { + branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates)) + for _, update := range updates { + if !update.RefFullName.IsBranch() { + continue + } + if repo == nil { + repo = loadRepository(ctx, ownerName, repoName) + if ctx.Written() { + return + } + wasEmpty = repo.IsEmpty + } + + if update.IsDelRef() { + if err := git_model.AddDeletedBranch(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil { + log.Error("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + } else { + branchesToSync = append(branchesToSync, update) + } + } + if len(branchesToSync) > 0 { + var err error + gitRepo, err = gitrepo.OpenRepository(ctx, repo) + if err != nil { + log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + + var ( + branchNames = make([]string, 0, len(branchesToSync)) + commitIDs = make([]string, 0, len(branchesToSync)) + ) + for _, update := range branchesToSync { + branchNames = append(branchNames, update.RefFullName.BranchName()) + commitIDs = append(commitIDs, update.NewCommitID) + } + + if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil { + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + } + + if err := repo_service.PushUpdates(updates); err != nil { + log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates)) + for i, update := range updates { + log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.RefFullName.BranchName()) + } + log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) + + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + } + + // handle pull request merging, a pull request action should push at least 1 commit + if opts.PushTrigger == repo_module.PushTriggerPRMergeToBase { + handlePullRequestMerging(ctx, opts, ownerName, repoName, updates) + if ctx.Written() { + return + } + } + + // Handle Push Options + if !opts.GetGitPushOptions().Empty() { + // load the repository + if repo == nil { + repo = loadRepository(ctx, ownerName, repoName) + if ctx.Written() { + // Error handled in loadRepository + return + } + wasEmpty = repo.IsEmpty + } + + repo.IsPrivate = opts.GetGitPushOptions().GetBool(pushoptions.RepoPrivate, repo.IsPrivate) + repo.IsTemplate = opts.GetGitPushOptions().GetBool(pushoptions.RepoTemplate, repo.IsTemplate) + if err := repo_model.UpdateRepositoryCols(ctx, repo, "is_private", "is_template"); err != nil { + log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err), + }) + } + } + + results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs)) + + // We have to reload the repo in case its state is changed above + repo = nil + var baseRepo *repo_model.Repository + + // Now handle the pull request notification trailers + for i := range opts.OldCommitIDs { + refFullName := opts.RefFullNames[i] + newCommitID := opts.NewCommitIDs[i] + + // post update for agit pull request + // FIXME: use pr.Flow to test whether it's an Agit PR or a GH PR + if git.SupportProcReceive && refFullName.IsPull() { + if repo == nil { + repo = loadRepository(ctx, ownerName, repoName) + if ctx.Written() { + return + } + } + + pullIndex, _ := strconv.ParseInt(refFullName.PullName(), 10, 64) + if pullIndex <= 0 { + continue + } + + pr, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, pullIndex) + if err != nil && !issues_model.IsErrPullRequestNotExist(err) { + log.Error("Failed to get PR by index %v Error: %v", pullIndex, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get PR by index %v Error: %v", pullIndex, err), + }) + return + } + if pr == nil { + continue + } + + results = append(results, private.HookPostReceiveBranchResult{ + Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx), + Create: false, + Branch: "", + URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index), + }) + continue + } + + // If we've pushed a branch (and not deleted it) + if !git.IsEmptyCommitID(newCommitID, nil) && refFullName.IsBranch() { + // First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo + if repo == nil { + repo = loadRepository(ctx, ownerName, repoName) + if ctx.Written() { + return + } + + baseRepo = repo + + if repo.IsFork { + if err := repo.GetBaseRepo(ctx); err != nil { + log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err), + RepoWasEmpty: wasEmpty, + }) + return + } + if repo.BaseRepo.AllowsPulls(ctx) { + baseRepo = repo.BaseRepo + } + } + + if !baseRepo.AllowsPulls(ctx) { + // We can stop there's no need to go any further + ctx.JSON(http.StatusOK, private.HookPostReceiveResult{ + RepoWasEmpty: wasEmpty, + }) + return + } + } + + branch := refFullName.BranchName() + + // If our branch is the default branch of an unforked repo - there's no PR to create or refer to + if !repo.IsFork && branch == baseRepo.DefaultBranch { + results = append(results, private.HookPostReceiveBranchResult{}) + continue + } + + pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub) + if err != nil && !issues_model.IsErrPullRequestNotExist(err) { + log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf( + "Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err), + RepoWasEmpty: wasEmpty, + }) + return + } + + if pr == nil { + if repo.IsFork { + branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch) + } + results = append(results, private.HookPostReceiveBranchResult{ + Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx), + Create: true, + Branch: branch, + URL: fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)), + }) + } else { + results = append(results, private.HookPostReceiveBranchResult{ + Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx), + Create: false, + Branch: branch, + URL: fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index), + }) + } + } + } + ctx.JSON(http.StatusOK, private.HookPostReceiveResult{ + Results: results, + RepoWasEmpty: wasEmpty, + }) +} + +func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) { + return cache.GetWithContextCache(ctx, "hook_post_receive_user", id, func() (*user_model.User, error) { + return user_model.GetUserByID(ctx, id) + }) +} + +// handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit +func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, ownerName, repoName string, updates []*repo_module.PushUpdateOptions) { + if len(updates) == 0 { + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID), + }) + return + } + + pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID) + if err != nil { + log.Error("GetPullRequestByID[%d]: %v", opts.PullRequestID, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "GetPullRequestByID failed"}) + return + } + + pusher, err := loadContextCacheUser(ctx, opts.UserID) + if err != nil { + log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Load pusher user failed"}) + return + } + + pr.MergedCommitID = updates[len(updates)-1].NewCommitID + pr.MergedUnix = timeutil.TimeStampNow() + pr.Merger = pusher + pr.MergerID = pusher.ID + err = db.WithTx(ctx, func(ctx context.Context) error { + // Removing an auto merge pull and ignore if not exist + if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { + return fmt.Errorf("DeleteScheduledAutoMerge[%d]: %v", opts.PullRequestID, err) + } + if _, err := pr.SetMerged(ctx); err != nil { + return fmt.Errorf("SetMerged failed: %s/%s Error: %v", ownerName, repoName, err) + } + return nil + }) + if err != nil { + log.Error("Failed to update PR to merged: %v", err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"}) + } +} diff --git a/routers/private/hook_post_receive_test.go b/routers/private/hook_post_receive_test.go new file mode 100644 index 0000000..bfd647e --- /dev/null +++ b/routers/private/hook_post_receive_test.go @@ -0,0 +1,50 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + pull_model "code.gitea.io/gitea/models/pull" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/private" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandlePullRequestMerging(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + pr, err := issues_model.GetUnmergedPullRequest(db.DefaultContext, 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub) + require.NoError(t, err) + require.NoError(t, pr.LoadBaseRepo(db.DefaultContext)) + + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + err = pull_model.ScheduleAutoMerge(db.DefaultContext, user1, pr.ID, repo_model.MergeStyleSquash, "squash merge a pr") + require.NoError(t, err) + + autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID}) + + ctx, resp := contexttest.MockPrivateContext(t, "/") + handlePullRequestMerging(ctx, &private.HookOptions{ + PullRequestID: pr.ID, + UserID: 2, + }, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*repo_module.PushUpdateOptions{ + {NewCommitID: "01234567"}, + }) + assert.Empty(t, resp.Body.String()) + pr, err = issues_model.GetPullRequestByID(db.DefaultContext, pr.ID) + require.NoError(t, err) + assert.True(t, pr.HasMerged) + assert.EqualValues(t, "01234567", pr.MergedCommitID) + + unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{ID: autoMerge.ID}) +} diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go new file mode 100644 index 0000000..4b8439d --- /dev/null +++ b/routers/private/hook_pre_receive.go @@ -0,0 +1,629 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "errors" + "fmt" + "net/http" + "os" + + "code.gitea.io/gitea/models" + asymkey_model "code.gitea.io/gitea/models/asymkey" + git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" + perm_model "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + quota_model "code.gitea.io/gitea/models/quota" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web" + gitea_context "code.gitea.io/gitea/services/context" + pull_service "code.gitea.io/gitea/services/pull" +) + +type preReceiveContext struct { + *gitea_context.PrivateContext + + // loadedPusher indicates that where the following information are loaded + loadedPusher bool + user *user_model.User // it's the org user if a DeployKey is used + userPerm access_model.Permission + deployKeyAccessMode perm_model.AccessMode + + canCreatePullRequest bool + checkedCanCreatePullRequest bool + + canWriteCode bool + checkedCanWriteCode bool + + protectedTags []*git_model.ProtectedTag + gotProtectedTags bool + + env []string + + opts *private.HookOptions + + isOverQuota bool + + branchName string +} + +// CanWriteCode returns true if pusher can write code +func (ctx *preReceiveContext) CanWriteCode() bool { + if !ctx.checkedCanWriteCode { + if !ctx.loadPusherAndPermission() { + return false + } + ctx.canWriteCode = issues_model.CanMaintainerWriteToBranch(ctx, ctx.userPerm, ctx.branchName, ctx.user) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite + ctx.checkedCanWriteCode = true + } + return ctx.canWriteCode +} + +// AssertCanWriteCode returns true if pusher can write code +func (ctx *preReceiveContext) AssertCanWriteCode() bool { + if !ctx.CanWriteCode() { + if ctx.Written() { + return false + } + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "User permission denied for writing.", + }) + return false + } + return true +} + +// CanCreatePullRequest returns true if pusher can create pull requests +func (ctx *preReceiveContext) CanCreatePullRequest() bool { + if !ctx.checkedCanCreatePullRequest { + if !ctx.loadPusherAndPermission() { + return false + } + ctx.canCreatePullRequest = ctx.userPerm.CanRead(unit.TypePullRequests) + ctx.checkedCanCreatePullRequest = true + } + return ctx.canCreatePullRequest +} + +// AssertCreatePullRequest returns true if can create pull requests +func (ctx *preReceiveContext) AssertCreatePullRequest() bool { + if !ctx.CanCreatePullRequest() { + if ctx.Written() { + return false + } + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "User permission denied for creating pull-request.", + }) + return false + } + return true +} + +var errPermissionDenied = errors.New("permission denied for changing repo settings") + +func (ctx *preReceiveContext) canChangeSettings() error { + if !ctx.loadPusherAndPermission() { + return errPermissionDenied + } + + if !ctx.userPerm.IsOwner() && !ctx.userPerm.IsAdmin() { + return errPermissionDenied + } + + if ctx.Repo.Repository.IsFork { + return errPermissionDenied + } + + return nil +} + +func (ctx *preReceiveContext) validatePushOptions() error { + opts := web.GetForm(ctx).(*private.HookOptions) + + if opts.GetGitPushOptions().ChangeRepoSettings() { + return ctx.canChangeSettings() + } + + return nil +} + +func (ctx *preReceiveContext) assertPushOptions() bool { + if err := ctx.validatePushOptions(); err != nil { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("options validation failed: %v", err), + }) + return false + } + return true +} + +func (ctx *preReceiveContext) checkQuota() error { + if !setting.Quota.Enabled { + ctx.isOverQuota = false + return nil + } + + if !ctx.loadPusherAndPermission() { + ctx.isOverQuota = true + return nil + } + + ok, err := quota_model.EvaluateForUser(ctx, ctx.PrivateContext.Repo.Repository.OwnerID, quota_model.LimitSubjectSizeReposAll) + if err != nil { + log.Error("quota_model.EvaluateForUser: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + UserMsg: "Error checking user quota", + }) + return err + } + + ctx.isOverQuota = !ok + return nil +} + +func (ctx *preReceiveContext) quotaExceeded() { + ctx.JSON(http.StatusRequestEntityTooLarge, private.Response{ + UserMsg: "Quota exceeded", + }) +} + +// HookPreReceive checks whether a individual commit is acceptable +func HookPreReceive(ctx *gitea_context.PrivateContext) { + opts := web.GetForm(ctx).(*private.HookOptions) + + ourCtx := &preReceiveContext{ + PrivateContext: ctx, + env: generateGitEnv(opts), // Generate git environment for checking commits + opts: opts, + } + + if !ourCtx.assertPushOptions() { + log.Trace("Git push options validation failed") + return + } + log.Trace("Git push options validation succeeded") + + if err := ourCtx.checkQuota(); err != nil { + return + } + + // Iterate across the provided old commit IDs + for i := range opts.OldCommitIDs { + oldCommitID := opts.OldCommitIDs[i] + newCommitID := opts.NewCommitIDs[i] + refFullName := opts.RefFullNames[i] + + switch { + case refFullName.IsBranch(): + preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName) + case refFullName.IsTag(): + preReceiveTag(ourCtx, oldCommitID, newCommitID, refFullName) + case git.SupportProcReceive && refFullName.IsFor(): + preReceiveFor(ourCtx, oldCommitID, newCommitID, refFullName) + default: + if ourCtx.isOverQuota { + ourCtx.quotaExceeded() + return + } + ourCtx.AssertCanWriteCode() + } + if ctx.Written() { + return + } + } + + ctx.PlainText(http.StatusOK, "ok") +} + +func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { + branchName := refFullName.BranchName() + ctx.branchName = branchName + + if !ctx.AssertCanWriteCode() { + return + } + + repo := ctx.Repo.Repository + gitRepo := ctx.Repo.GitRepo + objectFormat := ctx.Repo.GetObjectFormat() + + if branchName == repo.DefaultBranch && newCommitID == objectFormat.EmptyObjectID().String() { + log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), + }) + return + } + + protectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName) + if err != nil { + log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + + // Allow pushes to non-protected branches + if protectBranch == nil { + // ...unless the user is over quota, and the operation is not a delete + if newCommitID != objectFormat.EmptyObjectID().String() && ctx.isOverQuota { + ctx.quotaExceeded() + } + + return + } + protectBranch.Repo = repo + + // This ref is a protected branch. + // + // First of all we need to enforce absolutely: + // + // 1. Detect and prevent deletion of the branch + if newCommitID == objectFormat.EmptyObjectID().String() { + log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("branch %s is protected from deletion", branchName), + }) + return + } + + // 2. Disallow force pushes to protected branches + if oldCommitID != objectFormat.EmptyObjectID().String() { + output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+newCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ctx.env}) + if err != nil { + log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Fail to detect force push: %v", err), + }) + return + } else if len(output) > 0 { + log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName), + }) + return + } + } + + // 3. Enforce require signed commits + if protectBranch.RequireSignedCommits { + err := verifyCommits(oldCommitID, newCommitID, gitRepo, ctx.env) + if err != nil { + if !isErrUnverifiedCommit(err) { + log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err), + }) + return + } + unverifiedCommit := err.(*errUnverifiedCommit).sha + log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), + }) + return + } + } + + // Now there are several tests which can be overridden: + // + // 4. Check protected file patterns - this is overridable from the UI + changedProtectedfiles := false + protectedFilePath := "" + + globs := protectBranch.GetProtectedFilePatterns() + if len(globs) > 0 { + _, err := pull_service.CheckFileProtection(gitRepo, oldCommitID, newCommitID, globs, 1, ctx.env) + if err != nil { + if !models.IsErrFilePathProtected(err) { + log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), + }) + return + } + + changedProtectedfiles = true + protectedFilePath = err.(models.ErrFilePathProtected).Path + } + } + + // 5. Check if the doer is allowed to push + var canPush bool + if ctx.opts.DeployKeyID != 0 { + canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) + } else { + user, err := user_model.GetUserByID(ctx, ctx.opts.UserID) + if err != nil { + log.Error("Unable to GetUserByID for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to GetUserByID for commits from %s to %s: %v", oldCommitID, newCommitID, err), + }) + return + } + canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user) + } + + // 6. If we're not allowed to push directly + if !canPush { + // Is this is a merge from the UI/API? + if ctx.opts.PullRequestID == 0 { + // 6a. If we're not merging from the UI/API then there are two ways we got here: + // + // We are changing a protected file and we're not allowed to do that + if changedProtectedfiles { + log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + }) + return + } + + // Allow commits that only touch unprotected files + globs := protectBranch.GetUnprotectedFilePatterns() + if len(globs) > 0 { + unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(gitRepo, oldCommitID, newCommitID, globs, ctx.env) + if err != nil { + log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), + }) + return + } + if unprotectedFilesOnly { + // Commit only touches unprotected files, this is allowed + return + } + } + + // Or we're simply not able to push to this protected branch + log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + }) + return + } + // 6b. Merge (from UI or API) + + // Get the PR, user and permissions for the user in the repository + pr, err := issues_model.GetPullRequestByID(ctx, ctx.opts.PullRequestID) + if err != nil { + log.Error("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err), + }) + return + } + + // although we should have called `loadPusherAndPermission` before, here we call it explicitly again because we need to access ctx.user below + if !ctx.loadPusherAndPermission() { + // if error occurs, loadPusherAndPermission had written the error response + return + } + + // Now check if the user is allowed to merge PRs for this repository + // Note: we can use ctx.perm and ctx.user directly as they will have been loaded above + allowedMerge, err := pull_service.IsUserAllowedToMerge(ctx, pr, ctx.userPerm, ctx.user) + if err != nil { + log.Error("Error calculating if allowed to merge: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err), + }) + return + } + + if !allowedMerge { + log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", ctx.opts.UserID, branchName, repo, pr.Index) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + }) + return + } + + // If we're an admin for the instance, we can ignore checks + if ctx.user.IsAdmin { + return + } + + // It's not allowed t overwrite protected files. Unless if the user is an + // admin and the protected branch rule doesn't apply to admins. + if changedProtectedfiles && (!ctx.userPerm.IsAdmin() || protectBranch.ApplyToAdmins) { + log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + }) + return + } + + // Check all status checks and reviews are ok + if pb, err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil { + if models.IsErrDisallowedToMerge(err) { + // Allow this if the rule doesn't apply to admins and the user is an admin. + if ctx.userPerm.IsAdmin() && !pb.ApplyToAdmins { + return + } + log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error()) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()), + }) + return + } + log.Error("Unable to check if mergeable: protected branch %s in %-v and pr #%d. Error: %v", ctx.opts.UserID, branchName, repo, pr.Index, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", ctx.opts.PullRequestID, err), + }) + return + } + } +} + +func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { //nolint:unparam + if !ctx.AssertCanWriteCode() { + return + } + + tagName := refFullName.TagName() + + if !ctx.gotProtectedTags { + var err error + ctx.protectedTags, err = git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID) + if err != nil { + log.Error("Unable to get protected tags for %-v Error: %v", ctx.Repo.Repository, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + ctx.gotProtectedTags = true + } + + isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID) + if err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + if !isAllowed { + log.Warn("Forbidden: Tag %s in %-v is protected", tagName, ctx.Repo.Repository) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Tag %s is protected", tagName), + }) + return + } + + // If the user is over quota, and the push isn't a tag deletion, deny it + if ctx.isOverQuota { + objectFormat := ctx.Repo.GetObjectFormat() + if newCommitID != objectFormat.EmptyObjectID().String() { + ctx.quotaExceeded() + return + } + } +} + +func preReceiveFor(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { //nolint:unparam + if !ctx.AssertCreatePullRequest() { + return + } + + if ctx.Repo.Repository.IsEmpty { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Can't create pull request for an empty repository.", + }) + return + } + + if ctx.opts.IsWiki { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Pull requests are not supported on the wiki.", + }) + return + } + + baseBranchName := refFullName.ForBranchName() + + baseBranchExist := false + if ctx.Repo.GitRepo.IsBranchExist(baseBranchName) { + baseBranchExist = true + } + + if !baseBranchExist { + for p, v := range baseBranchName { + if v == '/' && ctx.Repo.GitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 { + baseBranchExist = true + break + } + } + } + + if !baseBranchExist { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Unexpected ref: %s", refFullName), + }) + return + } +} + +func generateGitEnv(opts *private.HookOptions) (env []string) { + env = os.Environ() + if opts.GitAlternativeObjectDirectories != "" { + env = append(env, + private.GitAlternativeObjectDirectories+"="+opts.GitAlternativeObjectDirectories) + } + if opts.GitObjectDirectory != "" { + env = append(env, + private.GitObjectDirectory+"="+opts.GitObjectDirectory) + } + if opts.GitQuarantinePath != "" { + env = append(env, + private.GitQuarantinePath+"="+opts.GitQuarantinePath) + } + return env +} + +// loadPusherAndPermission returns false if an error occurs, and it writes the error response +func (ctx *preReceiveContext) loadPusherAndPermission() bool { + if ctx.loadedPusher { + return true + } + + if ctx.opts.UserID == user_model.ActionsUserID { + ctx.user = user_model.NewActionsUser() + ctx.userPerm.AccessMode = perm_model.AccessMode(ctx.opts.ActionPerm) + if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil { + log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get User id %d Error: %v", ctx.opts.UserID, err), + }) + return false + } + ctx.userPerm.Units = ctx.Repo.Repository.Units + ctx.userPerm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) + for _, u := range ctx.Repo.Repository.Units { + ctx.userPerm.UnitsMode[u.Type] = ctx.userPerm.AccessMode + } + } else { + user, err := user_model.GetUserByID(ctx, ctx.opts.UserID) + if err != nil { + log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get User id %d Error: %v", ctx.opts.UserID, err), + }) + return false + } + ctx.user = user + userPerm, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, user) + if err != nil { + log.Error("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err), + }) + return false + } + ctx.userPerm = userPerm + } + + if ctx.opts.DeployKeyID != 0 { + deployKey, err := asymkey_model.GetDeployKeyByID(ctx, ctx.opts.DeployKeyID) + if err != nil { + log.Error("Unable to get DeployKey id %d Error: %v", ctx.opts.DeployKeyID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get DeployKey id %d Error: %v", ctx.opts.DeployKeyID, err), + }) + return false + } + ctx.deployKeyAccessMode = deployKey.Mode + } + + ctx.loadedPusher = true + return true +} diff --git a/routers/private/hook_proc_receive.go b/routers/private/hook_proc_receive.go new file mode 100644 index 0000000..e4aabd8 --- /dev/null +++ b/routers/private/hook_proc_receive.go @@ -0,0 +1,43 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "net/http" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/agit" + gitea_context "code.gitea.io/gitea/services/context" +) + +// HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present +func HookProcReceive(ctx *gitea_context.PrivateContext) { + opts := web.GetForm(ctx).(*private.HookOptions) + if !git.SupportProcReceive { + ctx.Status(http.StatusNotFound) + return + } + + results, err := agit.ProcReceive(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, opts) + if err != nil { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { + ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) + } else { + log.Error(err.Error()) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + } + + return + } + + ctx.JSON(http.StatusOK, private.HookProcReceiveResult{ + Results: results, + }) +} diff --git a/routers/private/hook_verification.go b/routers/private/hook_verification.go new file mode 100644 index 0000000..764c976 --- /dev/null +++ b/routers/private/hook_verification.go @@ -0,0 +1,122 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" +) + +// This file contains commit verification functions for refs passed across in hooks + +func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []string) error { + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + log.Error("Unable to create os.Pipe for %s", repo.Path) + return err + } + defer func() { + _ = stdoutReader.Close() + _ = stdoutWriter.Close() + }() + + var command *git.Command + objectFormat, _ := repo.GetObjectFormat() + if oldCommitID == objectFormat.EmptyObjectID().String() { + // When creating a new branch, the oldCommitID is empty, by using "newCommitID --not --all": + // List commits that are reachable by following the newCommitID, exclude "all" existing heads/tags commits + // So, it only lists the new commits received, doesn't list the commits already present in the receiving repository + command = git.NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(newCommitID).AddArguments("--not", "--all") + } else { + command = git.NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(oldCommitID + "..." + newCommitID) + } + // This is safe as force pushes are already forbidden + err = command.Run(&git.RunOpts{ + Env: env, + Dir: repo.Path, + Stdout: stdoutWriter, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env) + if err != nil { + log.Error("readAndVerifyCommitsFromShaReader failed: %v", err) + cancel() + } + _ = stdoutReader.Close() + return err + }, + }) + if err != nil && !isErrUnverifiedCommit(err) { + log.Error("Unable to check commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err) + } + return err +} + +func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository, env []string) error { + scanner := bufio.NewScanner(input) + for scanner.Scan() { + line := scanner.Text() + err := readAndVerifyCommit(line, repo, env) + if err != nil { + return err + } + } + return scanner.Err() +} + +func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error { + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + log.Error("Unable to create pipe for %s: %v", repo.Path, err) + return err + } + defer func() { + _ = stdoutReader.Close() + _ = stdoutWriter.Close() + }() + + commitID := git.MustIDFromString(sha) + + return git.NewCommand(repo.Ctx, "cat-file", "commit").AddDynamicArguments(sha). + Run(&git.RunOpts{ + Env: env, + Dir: repo.Path, + Stdout: stdoutWriter, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + commit, err := git.CommitFromReader(repo, commitID, stdoutReader) + if err != nil { + return err + } + verification := asymkey_model.ParseCommitWithSignature(ctx, commit) + if !verification.Verified { + cancel() + return &errUnverifiedCommit{ + commit.ID.String(), + } + } + return nil + }, + }) +} + +type errUnverifiedCommit struct { + sha string +} + +func (e *errUnverifiedCommit) Error() string { + return fmt.Sprintf("Unverified commit: %s", e.sha) +} + +func isErrUnverifiedCommit(err error) bool { + _, ok := err.(*errUnverifiedCommit) + return ok +} diff --git a/routers/private/hook_verification_test.go b/routers/private/hook_verification_test.go new file mode 100644 index 0000000..5f0d1d0 --- /dev/null +++ b/routers/private/hook_verification_test.go @@ -0,0 +1,46 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "context" + "testing" + + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" + + "github.com/stretchr/testify/require" +) + +var testReposDir = "tests/repos/" + +func TestVerifyCommits(t *testing.T) { + unittest.PrepareTestEnv(t) + + gitRepo, err := git.OpenRepository(context.Background(), testReposDir+"repo1_hook_verification") + defer gitRepo.Close() + require.NoError(t, err) + + objectFormat, err := gitRepo.GetObjectFormat() + require.NoError(t, err) + + testCases := []struct { + base, head string + verified bool + }{ + {"72920278f2f999e3005801e5d5b8ab8139d3641c", "d766f2917716d45be24bfa968b8409544941be32", true}, + {objectFormat.EmptyObjectID().String(), "93eac826f6188f34646cea81bf426aa5ba7d3bfe", true}, // New branch with verified commit + {"9779d17a04f1e2640583d35703c62460b2d86e0a", "72920278f2f999e3005801e5d5b8ab8139d3641c", false}, + {objectFormat.EmptyObjectID().String(), "9ce3f779ae33f31fce17fac3c512047b75d7498b", false}, // New branch with unverified commit + } + + for _, tc := range testCases { + err = verifyCommits(tc.base, tc.head, gitRepo, nil) + if tc.verified { + require.NoError(t, err) + } else { + require.Error(t, err) + } + } +} diff --git a/routers/private/internal.go b/routers/private/internal.go new file mode 100644 index 0000000..311f59b --- /dev/null +++ b/routers/private/internal.go @@ -0,0 +1,85 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// Package private contains all internal routes. The package name "internal" isn't usable because Golang reserves it for disabling cross-package usage. +package private + +import ( + "crypto/subtle" + "net/http" + "strings" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + + "gitea.com/go-chi/binding" + chi_middleware "github.com/go-chi/chi/v5/middleware" +) + +// CheckInternalToken check internal token is set +func CheckInternalToken(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + tokens := req.Header.Get("Authorization") + fields := strings.SplitN(tokens, " ", 2) + if setting.InternalToken == "" { + log.Warn(`The INTERNAL_TOKEN setting is missing from the configuration file: %q, internal API can't work.`, setting.CustomConf) + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + if len(fields) != 2 || fields[0] != "Bearer" || subtle.ConstantTimeCompare([]byte(fields[1]), []byte(setting.InternalToken)) == 0 { + log.Debug("Forbidden attempt to access internal url: Authorization header: %s", tokens) + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + } else { + next.ServeHTTP(w, req) + } + }) +} + +// bind binding an obj to a handler +func bind[T any](_ T) any { + return func(ctx *context.PrivateContext) { + theObj := new(T) // create a new form obj for every request but not use obj directly + binding.Bind(ctx.Req, theObj) + web.SetForm(ctx, theObj) + } +} + +// Routes registers all internal APIs routes to web application. +// These APIs will be invoked by internal commands for example `gitea serv` and etc. +func Routes() *web.Route { + r := web.NewRoute() + r.Use(context.PrivateContexter()) + r.Use(CheckInternalToken) + // Log the real ip address of the request from SSH is really helpful for diagnosing sometimes. + // Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers. + r.Use(chi_middleware.RealIP) + + r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent) + r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) + r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) + r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive) + r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext, bind(private.HookOptions{}), HookPostReceive) + r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext, RepoAssignment, bind(private.HookOptions{}), HookProcReceive) + r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) + r.Get("/serv/none/{keyid}", ServNoCommand) + r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) + r.Post("/manager/shutdown", Shutdown) + r.Post("/manager/restart", Restart) + r.Post("/manager/reload-templates", ReloadTemplates) + r.Post("/manager/flush-queues", bind(private.FlushOptions{}), FlushQueues) + r.Post("/manager/pause-logging", PauseLogging) + r.Post("/manager/resume-logging", ResumeLogging) + r.Post("/manager/release-and-reopen-logging", ReleaseReopenLogging) + r.Post("/manager/set-log-sql", SetLogSQL) + r.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger) + r.Post("/manager/remove-logger/{logger}/{writer}", RemoveLogger) + r.Get("/manager/processes", Processes) + r.Post("/mail/send", SendEmail) + r.Post("/restore_repo", RestoreRepo) + r.Post("/actions/generate_actions_runner_token", GenerateActionsRunnerToken) + + return r +} diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go new file mode 100644 index 0000000..e8ee8ba --- /dev/null +++ b/routers/private/internal_repo.go @@ -0,0 +1,69 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "context" + "fmt" + "net/http" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + gitea_context "code.gitea.io/gitea/services/context" +) + +// This file contains common functions relating to setting the Repository for the internal routes + +// RepoAssignment assigns the repository and gitrepository to the private context +func RepoAssignment(ctx *gitea_context.PrivateContext) context.CancelFunc { + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + + repo := loadRepository(ctx, ownerName, repoName) + if ctx.Written() { + // Error handled in loadRepository + return nil + } + + gitRepo, err := gitrepo.OpenRepository(ctx, repo) + if err != nil { + log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return nil + } + + ctx.Repo = &gitea_context.Repository{ + Repository: repo, + GitRepo: gitRepo, + } + + // We opened it, we should close it + cancel := func() { + // If it's been set to nil then assume someone else has closed it. + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + } + } + + return cancel +} + +func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *repo_model.Repository { + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) + if err != nil { + log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return nil + } + if repo.OwnerName == "" { + repo.OwnerName = ownerName + } + return repo +} diff --git a/routers/private/key.go b/routers/private/key.go new file mode 100644 index 0000000..5b8f238 --- /dev/null +++ b/routers/private/key.go @@ -0,0 +1,61 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "net/http" + + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/services/context" +) + +// UpdatePublicKeyInRepo update public key and deploy key updates +func UpdatePublicKeyInRepo(ctx *context.PrivateContext) { + keyID := ctx.ParamsInt64(":id") + repoID := ctx.ParamsInt64(":repoid") + if err := asymkey_model.UpdatePublicKeyUpdated(ctx, keyID); err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + + deployKey, err := asymkey_model.GetDeployKeyByRepo(ctx, keyID, repoID) + if err != nil { + if asymkey_model.IsErrDeployKeyNotExist(err) { + ctx.PlainText(http.StatusOK, "success") + return + } + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + deployKey.UpdatedUnix = timeutil.TimeStampNow() + if err = asymkey_model.UpdateDeployKeyCols(ctx, deployKey, "updated_unix"); err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + + ctx.PlainText(http.StatusOK, "success") +} + +// AuthorizedPublicKeyByContent searches content as prefix (leak e-mail part) +// and returns public key found. +func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) { + content := ctx.FormString("content") + + publicKey, err := asymkey_model.SearchPublicKeyByContent(ctx, content) + if err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + ctx.PlainText(http.StatusOK, publicKey.AuthorizedString()) +} diff --git a/routers/private/mail.go b/routers/private/mail.go new file mode 100644 index 0000000..cf3abb3 --- /dev/null +++ b/routers/private/mail.go @@ -0,0 +1,91 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + stdCtx "context" + "fmt" + "net/http" + "strconv" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/mailer" +) + +// SendEmail pushes messages to mail queue +// +// It doesn't wait before each message will be processed +func SendEmail(ctx *context.PrivateContext) { + if setting.MailService == nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: "Mail service is not enabled.", + }) + return + } + + var mail private.Email + rd := ctx.Req.Body + defer rd.Close() + + if err := json.NewDecoder(rd).Decode(&mail); err != nil { + log.Error("JSON Decode failed: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + + var emails []string + if len(mail.To) > 0 { + for _, uname := range mail.To { + user, err := user_model.GetUserByName(ctx, uname) + if err != nil { + err := fmt.Sprintf("Failed to get user information: %v", err) + log.Error(err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err, + }) + return + } + + if user != nil && len(user.Email) > 0 { + emails = append(emails, user.Email) + } + } + } else { + err := db.Iterate(ctx, nil, func(ctx stdCtx.Context, user *user_model.User) error { + if len(user.Email) > 0 && user.IsActive { + emails = append(emails, user.Email) + } + return nil + }) + if err != nil { + err := fmt.Sprintf("Failed to find users: %v", err) + log.Error(err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err, + }) + return + } + } + + sendEmail(ctx, mail.Subject, mail.Message, emails) +} + +func sendEmail(ctx *context.PrivateContext, subject, message string, to []string) { + for _, email := range to { + msg := mailer.NewMessage(email, subject, message) + mailer.SendAsync(msg) + } + + wasSent := strconv.Itoa(len(to)) + + ctx.PlainText(http.StatusOK, wasSent) +} diff --git a/routers/private/main_test.go b/routers/private/main_test.go new file mode 100644 index 0000000..a6bec72 --- /dev/null +++ b/routers/private/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/routers/private/manager.go b/routers/private/manager.go new file mode 100644 index 0000000..a6aa03e --- /dev/null +++ b/routers/private/manager.go @@ -0,0 +1,195 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/graceful/releasereopen" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" +) + +// ReloadTemplates reloads all the templates +func ReloadTemplates(ctx *context.PrivateContext) { + err := templates.ReloadHTMLTemplates() + if err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + UserMsg: fmt.Sprintf("Template error: %v", err), + }) + return + } + ctx.PlainText(http.StatusOK, "success") +} + +// FlushQueues flushes all the Queues +func FlushQueues(ctx *context.PrivateContext) { + opts := web.GetForm(ctx).(*private.FlushOptions) + if opts.NonBlocking { + // Save the hammer ctx here - as a new one is created each time you call this. + baseCtx := graceful.GetManager().HammerContext() + go func() { + err := queue.GetManager().FlushAll(baseCtx, opts.Timeout) + if err != nil { + log.Error("Flushing request timed-out with error: %v", err) + } + }() + ctx.JSON(http.StatusAccepted, private.Response{ + UserMsg: "Flushing", + }) + return + } + err := queue.GetManager().FlushAll(ctx, opts.Timeout) + if err != nil { + ctx.JSON(http.StatusRequestTimeout, private.Response{ + UserMsg: fmt.Sprintf("%v", err), + }) + } + ctx.PlainText(http.StatusOK, "success") +} + +// PauseLogging pauses logging +func PauseLogging(ctx *context.PrivateContext) { + log.GetManager().PauseAll() + ctx.PlainText(http.StatusOK, "success") +} + +// ResumeLogging resumes logging +func ResumeLogging(ctx *context.PrivateContext) { + log.GetManager().ResumeAll() + ctx.PlainText(http.StatusOK, "success") +} + +// ReleaseReopenLogging releases and reopens logging files +func ReleaseReopenLogging(ctx *context.PrivateContext) { + if err := releasereopen.GetManager().ReleaseReopen(); err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Error during release and reopen: %v", err), + }) + return + } + ctx.PlainText(http.StatusOK, "success") +} + +// SetLogSQL re-sets database SQL logging +func SetLogSQL(ctx *context.PrivateContext) { + db.SetLogSQL(ctx, ctx.FormBool("on")) + ctx.PlainText(http.StatusOK, "success") +} + +// RemoveLogger removes a logger +func RemoveLogger(ctx *context.PrivateContext) { + logger := ctx.Params("logger") + writer := ctx.Params("writer") + err := log.GetManager().GetLogger(logger).RemoveWriter(writer) + if err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to remove log writer: %s %s %v", logger, writer, err), + }) + return + } + ctx.PlainText(http.StatusOK, fmt.Sprintf("Removed %s %s", logger, writer)) +} + +// AddLogger adds a logger +func AddLogger(ctx *context.PrivateContext) { + opts := web.GetForm(ctx).(*private.LoggerOptions) + + if len(opts.Logger) == 0 { + opts.Logger = log.DEFAULT + } + + writerMode := log.WriterMode{} + writerType := opts.Mode + + var flags string + var ok bool + if flags, ok = opts.Config["flags"].(string); !ok { + switch opts.Logger { + case "access": + flags = "" + case "router": + flags = "date,time" + default: + flags = "stdflags" + } + } + writerMode.Flags = log.FlagsFromString(flags) + + if writerMode.Colorize, ok = opts.Config["colorize"].(bool); !ok && opts.Mode == "console" { + if _, ok := opts.Config["stderr"]; ok { + writerMode.Colorize = log.CanColorStderr + } else { + writerMode.Colorize = log.CanColorStdout + } + } + + writerMode.Level = setting.Log.Level + if level, ok := opts.Config["level"].(string); ok { + writerMode.Level = log.LevelFromString(level) + } + + writerMode.StacktraceLevel = setting.Log.StacktraceLogLevel + if stacktraceLevel, ok := opts.Config["level"].(string); ok { + writerMode.StacktraceLevel = log.LevelFromString(stacktraceLevel) + } + + writerMode.Prefix, _ = opts.Config["prefix"].(string) + writerMode.Expression, _ = opts.Config["expression"].(string) + + switch writerType { + case "console": + writerOption := log.WriterConsoleOption{} + writerOption.Stderr, _ = opts.Config["stderr"].(bool) + writerMode.WriterOption = writerOption + case "file": + writerOption := log.WriterFileOption{} + fileName, _ := opts.Config["filename"].(string) + writerOption.FileName = setting.LogPrepareFilenameForWriter(fileName, opts.Writer+".log") + writerOption.LogRotate, _ = opts.Config["rotate"].(bool) + maxSizeShift, _ := opts.Config["maxsize"].(int) + if maxSizeShift == 0 { + maxSizeShift = 28 + } + writerOption.MaxSize = 1 << maxSizeShift + writerOption.DailyRotate, _ = opts.Config["daily"].(bool) + writerOption.MaxDays, _ = opts.Config["maxdays"].(int) + if writerOption.MaxDays == 0 { + writerOption.MaxDays = 7 + } + writerOption.Compress, _ = opts.Config["compress"].(bool) + writerOption.CompressionLevel, _ = opts.Config["compressionLevel"].(int) + if writerOption.CompressionLevel == 0 { + writerOption.CompressionLevel = -1 + } + writerMode.WriterOption = writerOption + case "conn": + writerOption := log.WriterConnOption{} + writerOption.ReconnectOnMsg, _ = opts.Config["reconnectOnMsg"].(bool) + writerOption.Reconnect, _ = opts.Config["reconnect"].(bool) + writerOption.Protocol, _ = opts.Config["net"].(string) + writerOption.Addr, _ = opts.Config["address"].(string) + writerMode.WriterOption = writerOption + default: + panic(fmt.Sprintf("invalid log writer mode: %s", writerType)) + } + writer, err := log.NewEventWriter(opts.Writer, writerType, writerMode) + if err != nil { + log.Error("Failed to create new log writer: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to create new log writer: %v", err), + }) + return + } + log.GetManager().GetLogger(opts.Logger).AddWriters(writer) + ctx.PlainText(http.StatusOK, "success") +} diff --git a/routers/private/manager_process.go b/routers/private/manager_process.go new file mode 100644 index 0000000..9a0298a --- /dev/null +++ b/routers/private/manager_process.go @@ -0,0 +1,160 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "bytes" + "fmt" + "io" + "net/http" + "runtime" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + process_module "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/services/context" +) + +// Processes prints out the processes +func Processes(ctx *context.PrivateContext) { + pid := ctx.FormString("cancel-pid") + if pid != "" { + process_module.GetManager().Cancel(process_module.IDType(pid)) + runtime.Gosched() + time.Sleep(100 * time.Millisecond) + } + + flat := ctx.FormBool("flat") + noSystem := ctx.FormBool("no-system") + stacktraces := ctx.FormBool("stacktraces") + json := ctx.FormBool("json") + + var processes []*process_module.Process + goroutineCount := int64(0) + var processCount int + var err error + if stacktraces { + processes, processCount, goroutineCount, err = process_module.GetManager().ProcessStacktraces(flat, noSystem) + if err != nil { + log.Error("Unable to get stacktrace: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get stacktraces: %v", err), + }) + return + } + } else { + processes, processCount = process_module.GetManager().Processes(flat, noSystem) + } + + if json { + ctx.JSON(http.StatusOK, map[string]any{ + "TotalNumberOfGoroutines": goroutineCount, + "TotalNumberOfProcesses": processCount, + "Processes": processes, + }) + return + } + + ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") + ctx.Resp.WriteHeader(http.StatusOK) + + if err := writeProcesses(ctx.Resp, processes, processCount, goroutineCount, "", flat); err != nil { + log.Error("Unable to write out process stacktrace: %v", err) + if !ctx.Written() { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get stacktraces: %v", err), + }) + } + return + } +} + +func writeProcesses(out io.Writer, processes []*process_module.Process, processCount int, goroutineCount int64, indent string, flat bool) error { + if goroutineCount > 0 { + if _, err := fmt.Fprintf(out, "%sTotal Number of Goroutines: %d\n", indent, goroutineCount); err != nil { + return err + } + } + if _, err := fmt.Fprintf(out, "%sTotal Number of Processes: %d\n", indent, processCount); err != nil { + return err + } + if len(processes) > 0 { + if err := writeProcess(out, processes[0], " ", flat); err != nil { + return err + } + } + if len(processes) > 1 { + for _, process := range processes[1:] { + if _, err := fmt.Fprintf(out, "%s | \n", indent); err != nil { + return err + } + if err := writeProcess(out, process, " ", flat); err != nil { + return err + } + } + } + return nil +} + +func writeProcess(out io.Writer, process *process_module.Process, indent string, flat bool) error { + sb := &bytes.Buffer{} + if flat { + if process.ParentPID != "" { + _, _ = fmt.Fprintf(sb, "%s+ PID: %s\t\tType: %s\n", indent, process.PID, process.Type) + } else { + _, _ = fmt.Fprintf(sb, "%s+ PID: %s:%s\tType: %s\n", indent, process.ParentPID, process.PID, process.Type) + } + } else { + _, _ = fmt.Fprintf(sb, "%s+ PID: %s\tType: %s\n", indent, process.PID, process.Type) + } + indent += "| " + + _, _ = fmt.Fprintf(sb, "%sDescription: %s\n", indent, process.Description) + _, _ = fmt.Fprintf(sb, "%sStart: %s\n", indent, process.Start) + + if len(process.Stacks) > 0 { + _, _ = fmt.Fprintf(sb, "%sGoroutines:\n", indent) + for _, stack := range process.Stacks { + indent := indent + " " + _, _ = fmt.Fprintf(sb, "%s+ Description: %s", indent, stack.Description) + if stack.Count > 1 { + _, _ = fmt.Fprintf(sb, "* %d", stack.Count) + } + _, _ = fmt.Fprintf(sb, "\n") + indent += "| " + if len(stack.Labels) > 0 { + _, _ = fmt.Fprintf(sb, "%sLabels: %q:%q", indent, stack.Labels[0].Name, stack.Labels[0].Value) + + if len(stack.Labels) > 1 { + for _, label := range stack.Labels[1:] { + _, _ = fmt.Fprintf(sb, ", %q:%q", label.Name, label.Value) + } + } + _, _ = fmt.Fprintf(sb, "\n") + } + _, _ = fmt.Fprintf(sb, "%sStack:\n", indent) + indent += " " + for _, entry := range stack.Entry { + _, _ = fmt.Fprintf(sb, "%s+ %s\n", indent, entry.Function) + _, _ = fmt.Fprintf(sb, "%s| %s:%d\n", indent, entry.File, entry.Line) + } + } + } + if _, err := out.Write(sb.Bytes()); err != nil { + return err + } + sb.Reset() + if len(process.Children) > 0 { + if _, err := fmt.Fprintf(out, "%sChildren:\n", indent); err != nil { + return err + } + for _, child := range process.Children { + if err := writeProcess(out, child, indent+" ", flat); err != nil { + return err + } + } + } + return nil +} diff --git a/routers/private/manager_unix.go b/routers/private/manager_unix.go new file mode 100644 index 0000000..0c63ebc --- /dev/null +++ b/routers/private/manager_unix.go @@ -0,0 +1,25 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build !windows + +package private + +import ( + "net/http" + + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/services/context" +) + +// Restart causes the server to perform a graceful restart +func Restart(ctx *context.PrivateContext) { + graceful.GetManager().DoGracefulRestart() + ctx.PlainText(http.StatusOK, "success") +} + +// Shutdown causes the server to perform a graceful shutdown +func Shutdown(ctx *context.PrivateContext) { + graceful.GetManager().DoGracefulShutdown() + ctx.PlainText(http.StatusOK, "success") +} diff --git a/routers/private/manager_windows.go b/routers/private/manager_windows.go new file mode 100644 index 0000000..f1b9365 --- /dev/null +++ b/routers/private/manager_windows.go @@ -0,0 +1,27 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build windows + +package private + +import ( + "net/http" + + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/services/context" +) + +// Restart is not implemented for Windows based servers as they can't fork +func Restart(ctx *context.PrivateContext) { + ctx.JSON(http.StatusNotImplemented, private.Response{ + UserMsg: "windows servers cannot be gracefully restarted - shutdown and restart manually", + }) +} + +// Shutdown causes the server to perform a graceful shutdown +func Shutdown(ctx *context.PrivateContext) { + graceful.GetManager().DoGracefulShutdown() + ctx.PlainText(http.StatusOK, "success") +} diff --git a/routers/private/restore_repo.go b/routers/private/restore_repo.go new file mode 100644 index 0000000..4e95d30 --- /dev/null +++ b/routers/private/restore_repo.go @@ -0,0 +1,53 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "io" + "net/http" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/private" + myCtx "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/migrations" +) + +// RestoreRepo restore a repository from data +func RestoreRepo(ctx *myCtx.PrivateContext) { + bs, err := io.ReadAll(ctx.Req.Body) + if err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + params := struct { + RepoDir string + OwnerName string + RepoName string + Units []string + Validation bool + }{} + if err = json.Unmarshal(bs, ¶ms); err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + + if err := migrations.RestoreRepository( + ctx, + params.RepoDir, + params.OwnerName, + params.RepoName, + params.Units, + params.Validation, + ); err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + } else { + ctx.PlainText(http.StatusOK, "success") + } +} diff --git a/routers/private/serv.go b/routers/private/serv.go new file mode 100644 index 0000000..ef3920d --- /dev/null +++ b/routers/private/serv.go @@ -0,0 +1,397 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "fmt" + "net/http" + "strings" + + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" + wiki_service "code.gitea.io/gitea/services/wiki" +) + +// ServNoCommand returns information about the provided keyid +func ServNoCommand(ctx *context.PrivateContext) { + keyID := ctx.ParamsInt64(":keyid") + if keyID <= 0 { + ctx.JSON(http.StatusBadRequest, private.Response{ + UserMsg: fmt.Sprintf("Bad key id: %d", keyID), + }) + } + results := private.KeyAndOwner{} + + key, err := asymkey_model.GetPublicKeyByID(ctx, keyID) + if err != nil { + if asymkey_model.IsErrKeyNotExist(err) { + ctx.JSON(http.StatusUnauthorized, private.Response{ + UserMsg: fmt.Sprintf("Cannot find key: %d", keyID), + }) + return + } + log.Error("Unable to get public key: %d Error: %v", keyID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + results.Key = key + + if key.Type == asymkey_model.KeyTypeUser || key.Type == asymkey_model.KeyTypePrincipal { + user, err := user_model.GetUserByID(ctx, key.OwnerID) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.JSON(http.StatusUnauthorized, private.Response{ + UserMsg: fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), + }) + return + } + log.Error("Unable to get owner with id: %d for public key: %d Error: %v", key.OwnerID, keyID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + if !user.IsActive || user.ProhibitLogin { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Your account is disabled.", + }) + return + } + results.Owner = user + } + ctx.JSON(http.StatusOK, &results) +} + +// ServCommand returns information about the provided keyid +func ServCommand(ctx *context.PrivateContext) { + keyID := ctx.ParamsInt64(":keyid") + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + mode := perm.AccessMode(ctx.FormInt("mode")) + + // Set the basic parts of the results to return + results := private.ServCommandResults{ + RepoName: repoName, + OwnerName: ownerName, + KeyID: keyID, + } + + // Now because we're not translating things properly let's just default some English strings here + modeString := "read" + if mode > perm.AccessModeRead { + modeString = "write to" + } + + // The default unit we're trying to look at is code + unitType := unit.TypeCode + + // Unless we're a wiki... + if strings.HasSuffix(repoName, ".wiki") { + // in which case we need to look at the wiki + unitType = unit.TypeWiki + // And we'd better munge the reponame and tell downstream we're looking at a wiki + results.IsWiki = true + results.RepoName = repoName[:len(repoName)-5] + } + + owner, err := user_model.GetUserByName(ctx, results.OwnerName) + if err != nil { + if user_model.IsErrUserNotExist(err) { + // User is fetching/cloning a non-existent repository + log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), + }) + return + } + log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), + }) + return + } + if !owner.IsOrganization() && !owner.IsActive { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Repository cannot be accessed, you could retry it later", + }) + return + } + + // Now get the Repository and set the results section + repoExist := true + repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + repoExist = false + for _, verb := range ctx.FormStrings("verb") { + if verb == "git-upload-pack" { + // User is fetching/cloning a non-existent repository + log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), + }) + return + } + } + } else { + log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), + }) + return + } + } + + if repoExist { + repo.Owner = owner + repo.OwnerName = ownerName + results.RepoID = repo.ID + + if repo.IsBeingCreated() { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: "Repository is being created, you could retry after it finished", + }) + return + } + + if repo.IsBroken() { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: "Repository is in a broken state", + }) + return + } + + // We can shortcut at this point if the repo is a mirror + if mode > perm.AccessModeRead && repo.IsMirror { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), + }) + return + } + } + + // Get the Public Key represented by the keyID + key, err := asymkey_model.GetPublicKeyByID(ctx, keyID) + if err != nil { + if asymkey_model.IsErrKeyNotExist(err) { + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Cannot find key: %d", keyID), + }) + return + } + log.Error("Unable to get public key: %d Error: %v", keyID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), + }) + return + } + results.KeyName = key.Name + results.KeyID = key.ID + results.UserID = key.OwnerID + + // If repo doesn't exist, deploy key doesn't make sense + if !repoExist && key.Type == asymkey_model.KeyTypeDeploy { + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), + }) + return + } + + // Deploy Keys have ownerID set to 0 therefore we can't use the owner + // So now we need to check if the key is a deploy key + // We'll keep hold of the deploy key here for permissions checking + var deployKey *asymkey_model.DeployKey + var user *user_model.User + if key.Type == asymkey_model.KeyTypeDeploy { + var err error + deployKey, err = asymkey_model.GetDeployKeyByRepo(ctx, key.ID, repo.ID) + if err != nil { + if asymkey_model.IsErrDeployKeyNotExist(err) { + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + }) + return + } + log.Error("Unable to get deploy for public (deploy) key: %d in %-v Error: %v", key.ID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), + }) + return + } + results.DeployKeyID = deployKey.ID + results.KeyName = deployKey.Name + + // FIXME: Deploy keys aren't really the owner of the repo pushing changes + // however we don't have good way of representing deploy keys in hook.go + // so for now use the owner of the repository + results.UserName = results.OwnerName + results.UserID = repo.OwnerID + if !repo.Owner.KeepEmailPrivate { + results.UserEmail = repo.Owner.Email + } + } else { + // Get the user represented by the Key + var err error + user, err = user_model.GetUserByID(ctx, key.OwnerID) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.JSON(http.StatusUnauthorized, private.Response{ + UserMsg: fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), + }) + return + } + log.Error("Unable to get owner: %d for public key: %d:%s Error: %v", key.OwnerID, key.ID, key.Name, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), + }) + return + } + + if !user.IsActive || user.ProhibitLogin { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Your account is disabled.", + }) + return + } + + results.UserName = user.Name + if !user.KeepEmailPrivate { + results.UserEmail = user.Email + } + } + + // Don't allow pushing if the repo is archived + if repoExist && mode > perm.AccessModeRead && repo.IsArchived { + ctx.JSON(http.StatusUnauthorized, private.Response{ + UserMsg: fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), + }) + return + } + + // Permissions checking: + if repoExist && + (mode > perm.AccessModeRead || + repo.IsPrivate || + owner.Visibility.IsPrivate() || + (user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey + setting.Service.RequireSignInView) { + if key.Type == asymkey_model.KeyTypeDeploy { + if deployKey.Mode < mode { + ctx.JSON(http.StatusUnauthorized, private.Response{ + UserMsg: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + }) + return + } + } else { + // Because of the special ref "refs/for" we will need to delay write permission check + if git.SupportProcReceive && unitType == unit.TypeCode { + mode = perm.AccessModeRead + } + + perm, err := access_model.GetUserRepoPermission(ctx, repo, user) + if err != nil { + log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), + }) + return + } + + userMode := perm.UnitAccessMode(unitType) + + if userMode < mode { + log.Warn("Failed authentication attempt for %s with key %s (not authorized to %s %s/%s) from %s", user.Name, key.Name, modeString, ownerName, repoName, ctx.RemoteAddr()) + ctx.JSON(http.StatusUnauthorized, private.Response{ + UserMsg: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), + }) + return + } + } + } + + // We already know we aren't using a deploy key + if !repoExist { + owner, err := user_model.GetUserByName(ctx, ownerName) + if err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), + }) + return + } + + if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Push to create is not enabled for organizations.", + }) + return + } + if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Push to create is not enabled for users.", + }) + return + } + + repo, err = repo_service.PushCreateRepo(ctx, user, owner, results.RepoName) + if err != nil { + log.Error("pushCreateRepo: %v", err) + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), + }) + return + } + results.RepoID = repo.ID + } + + if results.IsWiki { + // Ensure the wiki is enabled before we allow access to it + if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil { + if repo_model.IsErrUnitTypeNotExist(err) { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "repository wiki is disabled", + }) + return + } + log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + + // Finally if we're trying to touch the wiki we should init it + if err = wiki_service.InitWiki(ctx, repo); err != nil { + log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + } + log.Debug("Serv Results:\nIsWiki: %t\nDeployKeyID: %d\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d", + results.IsWiki, + results.DeployKeyID, + results.KeyID, + results.KeyName, + results.UserName, + results.UserID, + results.OwnerName, + results.RepoName, + results.RepoID) + + ctx.JSON(http.StatusOK, results) + // We will update the keys in a different call. +} diff --git a/routers/private/ssh_log.go b/routers/private/ssh_log.go new file mode 100644 index 0000000..5bec632 --- /dev/null +++ b/routers/private/ssh_log.go @@ -0,0 +1,33 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "net/http" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" +) + +// SSHLog hook to response ssh log +func SSHLog(ctx *context.PrivateContext) { + if !setting.Log.EnableSSHLog { + ctx.Status(http.StatusOK) + return + } + + opts := web.GetForm(ctx).(*private.SSHLogOption) + + if opts.IsError { + log.Error("ssh: %v", opts.Message) + ctx.Status(http.StatusOK) + return + } + + log.Debug("ssh: %v", opts.Message) + ctx.Status(http.StatusOK) +} diff --git a/routers/private/tests/repos/repo1_hook_verification/HEAD b/routers/private/tests/repos/repo1_hook_verification/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/routers/private/tests/repos/repo1_hook_verification/config b/routers/private/tests/repos/repo1_hook_verification/config new file mode 100644 index 0000000..64280b8 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = false + bare = true + symlinks = false + ignorecase = true diff --git a/routers/private/tests/repos/repo1_hook_verification/info/refs b/routers/private/tests/repos/repo1_hook_verification/info/refs new file mode 100644 index 0000000..ee593c4 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/info/refs @@ -0,0 +1 @@ +d766f2917716d45be24bfa968b8409544941be32 refs/heads/main diff --git a/routers/private/tests/repos/repo1_hook_verification/logs/HEAD b/routers/private/tests/repos/repo1_hook_verification/logs/HEAD new file mode 100644 index 0000000..5c549b9 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/logs/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 d766f2917716d45be24bfa968b8409544941be32 Gitea <gitea@fake.local> 1693148474 +0800 push diff --git a/routers/private/tests/repos/repo1_hook_verification/logs/refs/heads/main b/routers/private/tests/repos/repo1_hook_verification/logs/refs/heads/main new file mode 100644 index 0000000..5c549b9 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/logs/refs/heads/main @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 d766f2917716d45be24bfa968b8409544941be32 Gitea <gitea@fake.local> 1693148474 +0800 push diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/08/cbc8f0e903b0916025ae7577564b7ed39ecb2c b/routers/private/tests/repos/repo1_hook_verification/objects/08/cbc8f0e903b0916025ae7577564b7ed39ecb2c Binary files differnew file mode 100644 index 0000000..d55278f --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/08/cbc8f0e903b0916025ae7577564b7ed39ecb2c diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/0b/5987362fe3fabdd4406babdc819642ee2f5a2a b/routers/private/tests/repos/repo1_hook_verification/objects/0b/5987362fe3fabdd4406babdc819642ee2f5a2a Binary files differnew file mode 100644 index 0000000..d8b6020 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/0b/5987362fe3fabdd4406babdc819642ee2f5a2a diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/13/b0f23f673b161f4b5cb66f051cb93c99729e1e b/routers/private/tests/repos/repo1_hook_verification/objects/13/b0f23f673b161f4b5cb66f051cb93c99729e1e Binary files differnew file mode 100644 index 0000000..77936d8 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/13/b0f23f673b161f4b5cb66f051cb93c99729e1e diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/23/33a51fdb238b7023a62ae3dcc58994061a7c86 b/routers/private/tests/repos/repo1_hook_verification/objects/23/33a51fdb238b7023a62ae3dcc58994061a7c86 Binary files differnew file mode 100644 index 0000000..5ec09ac --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/23/33a51fdb238b7023a62ae3dcc58994061a7c86 diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/2b/df04adb23d2b40b6085efb230856e5e2a775b7 b/routers/private/tests/repos/repo1_hook_verification/objects/2b/df04adb23d2b40b6085efb230856e5e2a775b7 Binary files differnew file mode 100644 index 0000000..355b88e --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/2b/df04adb23d2b40b6085efb230856e5e2a775b7 diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/65/a457425a679cbe9adf0d2741785d3ceabb44a7 b/routers/private/tests/repos/repo1_hook_verification/objects/65/a457425a679cbe9adf0d2741785d3ceabb44a7 Binary files differnew file mode 100644 index 0000000..ba1f06f --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/65/a457425a679cbe9adf0d2741785d3ceabb44a7 diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/72/920278f2f999e3005801e5d5b8ab8139d3641c b/routers/private/tests/repos/repo1_hook_verification/objects/72/920278f2f999e3005801e5d5b8ab8139d3641c new file mode 100644 index 0000000..4705fbf --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/72/920278f2f999e3005801e5d5b8ab8139d3641c @@ -0,0 +1,2 @@ +xK +1]AS$"32ooW{!`JC%.$r]sѱe$mM)(O`btlE[:;4H1_rayl~EL@cXvM":MۃG_}?
\ No newline at end of file diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/8b/903ede7c494725624bf842ec890f6342dababd b/routers/private/tests/repos/repo1_hook_verification/objects/8b/903ede7c494725624bf842ec890f6342dababd Binary files differnew file mode 100644 index 0000000..fd3c3a4 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/8b/903ede7c494725624bf842ec890f6342dababd diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/93/eac826f6188f34646cea81bf426aa5ba7d3bfe b/routers/private/tests/repos/repo1_hook_verification/objects/93/eac826f6188f34646cea81bf426aa5ba7d3bfe Binary files differnew file mode 100644 index 0000000..80abd3a --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/93/eac826f6188f34646cea81bf426aa5ba7d3bfe diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/97/79d17a04f1e2640583d35703c62460b2d86e0a b/routers/private/tests/repos/repo1_hook_verification/objects/97/79d17a04f1e2640583d35703c62460b2d86e0a new file mode 100644 index 0000000..7f3293a --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/97/79d17a04f1e2640583d35703c62460b2d86e0a @@ -0,0 +1,2 @@ +x1 +!ES{AwGGa 9EQgW#AZ/p((Bhۼ&:pLY`U-z\ZM:xJ/G}:3
\ No newline at end of file diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/9c/e3f779ae33f31fce17fac3c512047b75d7498b b/routers/private/tests/repos/repo1_hook_verification/objects/9c/e3f779ae33f31fce17fac3c512047b75d7498b Binary files differnew file mode 100644 index 0000000..61a9ee8 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/9c/e3f779ae33f31fce17fac3c512047b75d7498b diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/a9/f76e70a663e40091749a97eeac5f57a6fec141 b/routers/private/tests/repos/repo1_hook_verification/objects/a9/f76e70a663e40091749a97eeac5f57a6fec141 Binary files differnew file mode 100644 index 0000000..fb79dc9 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/a9/f76e70a663e40091749a97eeac5f57a6fec141 diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/ba/0caedd359ebe310ef431335576e20f2b84e9b9 b/routers/private/tests/repos/repo1_hook_verification/objects/ba/0caedd359ebe310ef431335576e20f2b84e9b9 Binary files differnew file mode 100644 index 0000000..1801a7f --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/ba/0caedd359ebe310ef431335576e20f2b84e9b9 diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/bb/87653e0819460e79b5f075f2563f583cbbf18c b/routers/private/tests/repos/repo1_hook_verification/objects/bb/87653e0819460e79b5f075f2563f583cbbf18c Binary files differnew file mode 100644 index 0000000..a765d66 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/bb/87653e0819460e79b5f075f2563f583cbbf18c diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/cb/a4c30c196a0e03e7bdf6eeb8393d14b9d073aa b/routers/private/tests/repos/repo1_hook_verification/objects/cb/a4c30c196a0e03e7bdf6eeb8393d14b9d073aa new file mode 100644 index 0000000..c7de09f --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/cb/a4c30c196a0e03e7bdf6eeb8393d14b9d073aa @@ -0,0 +1,3 @@ +xA +0E]$LxL2] +
\}e[:{MZ5b8$vfR37];bt 3$,tXG>m
p1w͗-7pĄpZsDZL̾Le@
\ No newline at end of file diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/d7/66f2917716d45be24bfa968b8409544941be32 b/routers/private/tests/repos/repo1_hook_verification/objects/d7/66f2917716d45be24bfa968b8409544941be32 new file mode 100644 index 0000000..1544584 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/d7/66f2917716d45be24bfa968b8409544941be32 @@ -0,0 +1,3 @@ +xˮFE3+zn%44!%QxۀsA`8{2IMjdf2"$e +-( !J"a@BaHo3V<$/)$JJDBH{#RROnfFO +q[2̇~zjjL}prmFqh`@ثf?3[7) ^uֿ,l7zr|&Ou49:Qj1x6Q%tsV|(V,aL,G~r@`$[! Xˊep[8 o(kZγyeйYkd63;3 RiދdYDk91V]/C#&poFb}uW&]+m xaqdIX3
3KI#i_rgĩ7=`@[&A̤Lo3~M8MGt>xvQ(aWo"srzeŭ}QD֨fK)mr>>̚$F8x
^Jk{mczI*^Mb m6M~hp
{0]?nUwgɠJ б<72
\ No newline at end of file diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/e3/7e5d19823e42fad252f6341b1f77a7bc6ee451 b/routers/private/tests/repos/repo1_hook_verification/objects/e3/7e5d19823e42fad252f6341b1f77a7bc6ee451 Binary files differnew file mode 100644 index 0000000..b3f925e --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/e3/7e5d19823e42fad252f6341b1f77a7bc6ee451 diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 b/routers/private/tests/repos/repo1_hook_verification/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 Binary files differnew file mode 100644 index 0000000..7112238 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/e8/4e452c3a20c02dee17ec2f63c0cb9ef6c759eb b/routers/private/tests/repos/repo1_hook_verification/objects/e8/4e452c3a20c02dee17ec2f63c0cb9ef6c759eb Binary files differnew file mode 100644 index 0000000..24580f8 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/e8/4e452c3a20c02dee17ec2f63c0cb9ef6c759eb diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/info/packs b/routers/private/tests/repos/repo1_hook_verification/objects/info/packs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/info/packs @@ -0,0 +1 @@ + diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/08/cbc8f0e903b0916025ae7577564b7ed39ecb2c b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/08/cbc8f0e903b0916025ae7577564b7ed39ecb2c Binary files differnew file mode 100644 index 0000000..d55278f --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/08/cbc8f0e903b0916025ae7577564b7ed39ecb2c diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/0b/5987362fe3fabdd4406babdc819642ee2f5a2a b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/0b/5987362fe3fabdd4406babdc819642ee2f5a2a Binary files differnew file mode 100644 index 0000000..d8b6020 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/0b/5987362fe3fabdd4406babdc819642ee2f5a2a diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/13/b0f23f673b161f4b5cb66f051cb93c99729e1e b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/13/b0f23f673b161f4b5cb66f051cb93c99729e1e Binary files differnew file mode 100644 index 0000000..77936d8 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/13/b0f23f673b161f4b5cb66f051cb93c99729e1e diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/23/33a51fdb238b7023a62ae3dcc58994061a7c86 b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/23/33a51fdb238b7023a62ae3dcc58994061a7c86 Binary files differnew file mode 100644 index 0000000..5ec09ac --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/23/33a51fdb238b7023a62ae3dcc58994061a7c86 diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/8b/903ede7c494725624bf842ec890f6342dababd b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/8b/903ede7c494725624bf842ec890f6342dababd Binary files differnew file mode 100644 index 0000000..fd3c3a4 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/8b/903ede7c494725624bf842ec890f6342dababd diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/93/eac826f6188f34646cea81bf426aa5ba7d3bfe b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/93/eac826f6188f34646cea81bf426aa5ba7d3bfe Binary files differnew file mode 100644 index 0000000..80abd3a --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/93/eac826f6188f34646cea81bf426aa5ba7d3bfe diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/9c/e3f779ae33f31fce17fac3c512047b75d7498b b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/9c/e3f779ae33f31fce17fac3c512047b75d7498b Binary files differnew file mode 100644 index 0000000..61a9ee8 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/9c/e3f779ae33f31fce17fac3c512047b75d7498b diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/a9/f76e70a663e40091749a97eeac5f57a6fec141 b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/a9/f76e70a663e40091749a97eeac5f57a6fec141 Binary files differnew file mode 100644 index 0000000..fb79dc9 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/a9/f76e70a663e40091749a97eeac5f57a6fec141 diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/bb/87653e0819460e79b5f075f2563f583cbbf18c b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/bb/87653e0819460e79b5f075f2563f583cbbf18c Binary files differnew file mode 100644 index 0000000..a765d66 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/bb/87653e0819460e79b5f075f2563f583cbbf18c diff --git a/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/cb/a4c30c196a0e03e7bdf6eeb8393d14b9d073aa b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/cb/a4c30c196a0e03e7bdf6eeb8393d14b9d073aa new file mode 100644 index 0000000..c7de09f --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/objects/tmp_objdir-incoming-a21648/cb/a4c30c196a0e03e7bdf6eeb8393d14b9d073aa @@ -0,0 +1,3 @@ +xA +0E]$LxL2] +
\}e[:{MZ5b8$vfR37];bt 3$,tXG>m
p1w͗-7pĄpZsDZL̾Le@
\ No newline at end of file diff --git a/routers/private/tests/repos/repo1_hook_verification/refs/heads/main b/routers/private/tests/repos/repo1_hook_verification/refs/heads/main new file mode 100644 index 0000000..2186e82 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification/refs/heads/main @@ -0,0 +1 @@ +d766f2917716d45be24bfa968b8409544941be32 diff --git a/routers/private/tests/repos/repo1_hook_verification_dummy_gpg_key.txt b/routers/private/tests/repos/repo1_hook_verification_dummy_gpg_key.txt new file mode 100644 index 0000000..3061c75 --- /dev/null +++ b/routers/private/tests/repos/repo1_hook_verification_dummy_gpg_key.txt @@ -0,0 +1,127 @@ +# GPG key for abcde@gitea.com + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +4akc +=OYIo +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQWGBGTrY3UBDAC2HLBqmMplAV15qSnC7g1c4dV406f5EHNhFr95Nup2My6b2eaf +Tlvedv77s8PT/I7F3fy4apOZs5A7w2SsPlLMcQ3ev4uGOsxRtkq5RLy1Yb6SNueX +0Da2UVKR5KTC5Q6BWaqxwS0IjKOLZ/xz0Pbe/ClV3bZSKBEY2omkVo3Z0HZ771vB +2clPRvGJ/IdeKOsZ3ZytSFXfyiJBdARmeSPmydXLil8+Ibq5iLAeow5PK8hK1TCO +nKHzLWNqcNq70tyjoHvcGi70iGjoVEEUgPCLLuU8WmzTJwlvA3BuDzjtaO7TLo/j +dE6iqkHtMSS8x+43sAH6hcFRCWAVh/0Uq7n36uGDfNxGnX3YrmX3LR9x5IsBES1r +GGWbpxio4o5GIf/Xd+JgDd9rzJCqRuZ3/sW/TxK38htWaVNZV0kMkHUCTc1ctzWp +Cm635hbFCHBhPYIp+/z206khkAKDbz/CNuU91Wazsh7KO07wrwDtxfDDbInJ8TfH +E2TGjzjQzgChfmcAEQEAAf4HAwKN54iG/XBl5/UViAmmiESRj3u+uJC9EztalVbj +156bjamUHBYIoCH4SBB0l0bR/o9ZN3vE4ZvyF3OyJ0AKF9epjWIuz7S+QIm1NLzk +IqwRyfGPsktwtZOF1CsathN4RyJL5/3nB9g4BLYfRARe9lwU0C0HQjBwAVj8m6RN ++wMTHZqW7tUN75npgPRLUI30H3GPVm3yLfS88Ol8nd31r7V0JsXZ2/mM9CWF4sUy +o1DW3P/rBn49s/x2qL/acEL+5PK7suFBP8Pjp5cwGjnSehoWeOclXgstkg3OEryY +2JP74muDVmaEVOAk7wiRjUD7HYuEOm/MbphFyen7QtO8WtN3IRKgNm19v5Skd4AF +NW9ZAdQOk2yHw7zyRk7HOPmEbEstbyE1RYWIfgZGjJlEJ2DI5ABwVJJ3W6DRPiZ3 +owd/JxBUVu/wigIjbg6z6ZQd/bn1XwKyhyTtgyTyILzE1gqtO7xs1XmK3wcww794 +cVLjqSnAdaeXMt4P+sDA17Wqky0f/jQ9kq7/tv7ipq9jvp9RaQ1ccRsz+mGgBVl+ +oLg4klKN47ZQGt0SQpLzHLL8SHzY0dz5US+Z2J+hdZia6jEmfilY9r4WPe7djMYz +Na908DmcbjfAg4XHPqVRXjgraUiT2YTo2LOV2dHn7550hJ/JshpOVqrJUrjhCgDN +usEMK3KXJkFvf6zflMv3t8HMD2SGBfpCJSwDaW+mrmtpR6a5laoZxg/009qZqgpj +FuenLuZmgYrHXozMXllwi6MLvSE/ioXrK4fqvpAwzOk6ArqZdWfxoJDYNQKXVL7z +Arniq9Ctaag8hr5T+JoZ9wNPNVF/LuEwPTWDur4qpU07KqWt9OFKPsEDNzxVZfNM +vtSCYvQ1uUH3CbPLQvPpd5TnyhjwKYtTzyW4OcuZHrWIZp9fZi5QdhWxobqGQiBk ++nRNFe0FPVEN0VcNdYJIDKcDLsOYCkGy08tucZnbKtr8JaK7XBSOo9Frg1i/j4Aa +GnXWlkMTVAkuxLZPATTOgdBoYmHMYKQvw31aFBrf3QU9c3EEg9UPYFMErVIeBHBB +BS+E7QZToHScCG1zezlr4rdqarkz0Yvzc3aduoSAOJHDf/Il+tOkepMne1y5fi72 +5UT1yWGbXXkTCV/pM6s0pLaEvNHmGvPQ6VGbJ//5w+42PFD1d7yEai53OgSZNs7B ++Ie/6Vq5GYzTM0bT3/o7/O1Zi56y791YKaas9wgxOhmMIZ0hsTecQJLJZGotUlOv +V7fZUhPRc4ksUeCyM3G0E89ilFtY6NuPcWQ8yMeS4sRRLmie+iaT+kNvAqL5mXvg +WNLhFIXPC1gpGLB8lpT5YEY647aPjQEig7QXYWJjZGUgPGFiY2RlQGdpdGVhLmNv +bT6JAc4EEwEIADgWIQRo/BkcvP70fnQCv16xVDFkJim4JgUCZOtjdQIbAwULCQgH +AgYVCgkICwIEFgIDAQIeAQIXgAAKCRCxVDFkJim4Js6+C/9yIjHqcyM88hQAYQUo +iPYfgJ0f2NsDAi/XypyDaFbRy9Wqm3oKvMr9L9G5xgOXshjRaRWOpODAwLmtVrJf +OV5BhxLEcBcO2hDdM3ycp8Gt7+Fx/o0cUjPiiC18hh3K5LRfeE7oYynSJDgjoDNu +zIMuyoWuJPNc+IcE4roND55qyyyC9ObrTLz1GgGm1bXtkHhZ1NdOfQ4q8M48K39J +n7pmnmSX3R74CSU6flh/o9AtzGLjU70JUOLFcWnR5D0iEI8mOsdfEHr+p+CvDVG9 +l4unPhMunT+QOUwV2DEmqo9P+yIert1ucVTDoSf+FrRaKUHg8r1Tt6T4/4GyIeSx +G72NImK0h8jz+bADPZhxuG4UR1Mj8bilqhWgODFPi/5DrDsNMWq1pEvjn6f4pCUx +0IDTnPTniOXtafXtAD4Rz0rwJWYqgeJFHgjXzaxBiOE1bhS26NPEvyAa0T9Tj3E7 +3ICMESAmVad2JqO/mVxkLDGWdpXM7qB8bO2YGMOplrTvWaadBYYEZOtjdQEMAOwe +vO46JxBo91RCbT7RQ2uz3ZwRKb+P/jIEFST6x8tkCjon31zh6HicBDPNntqXTzSt +goHQb7vGhHPV4dxAfrOtVyoHwpi1/+x1jjtZoyIzLEz6RNK/Onu2y/tC5JBnSd5Q +RdHJgzPm20F8iNZR37c0Mi24fIH4y01aVLfNeBpRt7lWJ+opo2bM3Rh7jJdMpynK +kTcA6o9XP6IgW/dzpOayosclpHhWiJwKV4CovIX/bxawk7sz10Nb4QzcxlWexWnJ +xNRHIcAkZ9KTXTBpBkBpHCZqsI3+rQoQn5oQAr9JGWJSd4Fmgw7mFjmIF4bjfa2h +/BpCoBqE+/25chvWfYkQwrCcyUwD1QYPUBwNvLB+PWb9kYEHD3mLgSSR+fjdG9Xd +Mevu4lT91Gqo/6KJzgzClSs7GoQtb+SZ4deUFw1tlmEQS/BGhbtTb/1566iDidGV +5EnSmL/E4/3CbGQqNog8gremF0G0SlWTjD9RMBY13IgisWCC6R4CdkXIYnCWbwAR +AQAB/gcDAgtreHsdznsa9bAha2g+J5zygs7rp95KvqRm4SGrgWPnngMewrHXrJAx +REUQFbOYJKvb6+SB47N8BTIh/nEY/B6dpvC36QSHB0XAgkktiOhdS2rTlrq+bKse +rZzoM/jbcxS3/cwi4VWH4lQhz7TLZtQxFZDuwyiik8/m5KscMxQrbYJg++4KpFQQ +En7RRUO0hEaYdnqQ9t3M8SWLwZn2yK3hzBE0gkQ8CJA3Zokv3DO7FSsAX823O25B +X7NgIpmbHCeYK6YV0gjQUKP1o3Sf7DhJzO1iltg0+obNTDl9RoeFgxTVORCdUlGA +kPdgoBbAGtadpZlCMThn7FlIn+ogqwQpAcoSTZjX31SOQBBpgMW9yf3GTNk2Nvrn +08zIA0hnUWFfc4VY6fbjbX5bF0jpoJ3XG6Hwa1VVRwQGFLxFV23TbZ+baLLuxEBx +A86XDC5zWFMwF/7aYL8oeXgoI+499u9G4Gw9G87va7rQXlTQJcHQRqu9YaGcxwOi +UslhNtVWz52iIURappUfFaGBRGUvtx2DOTgn4m099nnPaKDUiLmc4bFIHwzyA7Pl +RdAmLosrxSyIxHdlUOS/KshucXXKGVoYkJqGLXNQCY6x2zbyBPX9/a/0P59UP/WU +qwAHuGbXlToGhSKZzC8KmVs12tyQsAZ/47D+G29kEcRlaey1+N3Uor1jN7D66uyj +M1jYFhBudNIuuTR8sfrYjmbYIj8y0bgvF4RN6sU1padoTETadWNyIcFiRMZQ0oQd +KJBa3CxdqQZ2EU4a5jkA4UTQE13IySh7eNbYP5VwBgr3Z59gcbouKfFxKBhmPHF2 +BAmC0VXI2BgqKNqM6QgVj5UKrp41AX4D+iIhyKa0D3rapuIywXg1AtsrAlrOU/Ig +tQCj/a0NjIVJpLqVKBUdd4Eea69fDCJGIoaDNyp7qwo+nA1O2oDbc32EryJYUkHm +XMoLmx5y+/rxRsRevBv0ojwu3zsx2K93M1wHYd0z+SJsU8QGFinoFgYcmNp/tgMW +WtHBN4AijDuDSZAyG+MrWIj3NS4mbajx+utEIn3DC/ofFPlTmgX3OvpOPG1hnhBH +xSZUME+znOnqJMpUqnna4jbHEPwvRIXUY6InFKgl1Bu4grww/oo3qi7NwWL0Mcdy +qabWhdlEz5N/QBBPWVQllelgI+xTmZoCRUhh1mn+PM900vXXeM/DIALnxEXs9I/m +l4wPdLZlCdaKZS8vv33adyS6i9gWfI3NPWxZ2TyqC7nf5D5OK1zKSu3iWx17nXn2 +ak5hZnaXfzTxuZL3E8KZD/qsDm80c2PXFitogJTih37N6A8UQOJPtWbkfvPiwUvI +gw0oouggn0iJQVNoiQG2BBgBCAAgFiEEaPwZHLz+9H50Ar9esVQxZCYpuCYFAmTr +Y3UCGwwACgkQsVQxZCYpuCb1AAv/dI5YtGxBXaHAMj+lOLmZi5w4t0M7Zafa8tNn +WrBwj4KixiXEt52i5YKxuaVD3+/cMqidSDp0M5Cxx0wcmnmg+mdFFcowtXIXuk1T +GTcHcOCPoXgF6gfoGimNNE1Aw1+EnC4/TbjMCKEM7b2QZ7/CgkBxZJWbScN4Jtaw +ory9LEQqo0/epYJwf+79GHIJrpODAPiPJEMKmlej23KyoFuusOi17C0vHCf3GZNj +4F2So3LOrcs51qTlOum2MdL5oTdqffatzs6p4u5bHBxyRugQlQggTRSK+TXLdxnF +Xr9ukXjIC2mFir7CCnZHw4e+2JwZfaAom0ZX+pLwrReSop4BPPU2YDzt3XCUk0S9 +kpiOsN7iFWUMCFreIE50DOxt9406kSGopYKVaifbDl4MdLXM4v+oucLe7/yOViT/ +dm4FcIytIR+jzC8MaLQTB23euzm2wOjI1YOwv7Il6PWZyDdU+tyzXcaJ7wSFBeQF +ZZtqph2TItCeV04HoaKHHc254akc +=PPG4 +-----END PGP PRIVATE KEY BLOCK----- |