diff options
Diffstat (limited to 'routers/private/hook_verification.go')
-rw-r--r-- | routers/private/hook_verification.go | 122 |
1 files changed, 122 insertions, 0 deletions
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 +} |