diff options
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/hook.go | 346 | ||||
-rw-r--r-- | cmd/hook_test.go | 41 | ||||
-rw-r--r-- | cmd/serv.go | 8 |
3 files changed, 394 insertions, 1 deletions
diff --git a/cmd/hook.go b/cmd/hook.go index 87f1f37562..fb43add8d4 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -38,6 +38,7 @@ var ( subcmdHookPreReceive, subcmdHookUpdate, subcmdHookPostReceive, + subcmdHookProcReceive, }, } @@ -74,6 +75,18 @@ var ( }, }, } + // Note: new hook since git 2.29 + subcmdHookProcReceive = cli.Command{ + Name: "proc-receive", + Usage: "Delegate proc-receive Git hook", + Description: "This command should only be called by Git", + Action: runHookProcReceive, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "debug", + }, + }, + } ) type delayWriter struct { @@ -205,6 +218,11 @@ Gitea or set your environment appropriately.`, "") } } + supportProcRecive := false + if git.CheckGitVersionAtLeast("2.29") == nil { + supportProcRecive = true + } + for scanner.Scan() { // TODO: support news feeds for wiki if isWiki { @@ -223,7 +241,9 @@ Gitea or set your environment appropriately.`, "") lastline++ // If the ref is a branch or tag, check if it's protected - if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { + // if supportProcRecive all ref should be checked because + // permission check was delayed + if supportProcRecive || strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { oldCommitIDs[count] = oldCommitID newCommitIDs[count] = newCommitID refFullNames[count] = refFullName @@ -463,3 +483,327 @@ func pushOptions() map[string]string { } return opts } + +func runHookProcReceive(c *cli.Context) error { + setup("hooks/proc-receive.log", c.Bool("debug")) + + if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { + if setting.OnlyAllowPushIfGiteaEnvironmentSet { + return fail(`Rejecting changes as Gitea environment not set. +If you are pushing over SSH you must push with a key managed by +Gitea or set your environment appropriately.`, "") + } + return nil + } + + ctx, cancel := installSignals() + defer cancel() + + if git.CheckGitVersionAtLeast("2.29") != nil { + return fail("Internal Server Error", "git not support proc-receive.") + } + + reader := bufio.NewReader(os.Stdin) + repoUser := os.Getenv(models.EnvRepoUsername) + repoName := os.Getenv(models.EnvRepoName) + pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64) + pusherName := os.Getenv(models.EnvPusherName) + + // 1. Version and features negotiation. + // S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n) + // S: flush-pkt + // H: PKT-LINE(version=1\0push-options...) + // H: flush-pkt + + rs, err := readPktLine(reader, pktLineTypeData) + if err != nil { + return err + } + + const VersionHead string = "version=1" + + var ( + hasPushOptions bool + response = []byte(VersionHead) + requestOptions []string + ) + + index := bytes.IndexByte(rs.Data, byte(0)) + if index >= len(rs.Data) { + return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data)) + } + + if index < 0 { + if len(rs.Data) == 10 && rs.Data[9] == '\n' { + index = 9 + } else { + return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data)) + } + } + + if string(rs.Data[0:index]) != VersionHead { + return fail("Internal Server Error", "Received unsupported version: %s", string(rs.Data[0:index])) + } + requestOptions = strings.Split(string(rs.Data[index+1:]), " ") + + for _, option := range requestOptions { + if strings.HasPrefix(option, "push-options") { + response = append(response, byte(0)) + response = append(response, []byte("push-options")...) + hasPushOptions = true + } + } + response = append(response, '\n') + + _, err = readPktLine(reader, pktLineTypeFlush) + if err != nil { + return err + } + + err = writeDataPktLine(os.Stdout, response) + if err != nil { + return err + } + + err = writeFlushPktLine(os.Stdout) + if err != nil { + return err + } + + // 2. receive commands from server. + // S: PKT-LINE(<old-oid> <new-oid> <ref>) + // S: ... ... + // S: flush-pkt + // # [receive push-options] + // S: PKT-LINE(push-option) + // S: ... ... + // S: flush-pkt + hookOptions := private.HookOptions{ + UserName: pusherName, + UserID: pusherID, + } + hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize) + hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize) + hookOptions.RefFullNames = make([]string, 0, hookBatchSize) + + for { + // note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed + rs, err = readPktLine(reader, pktLineTypeUnknow) + if err != nil { + return err + } + + if rs.Type == pktLineTypeFlush { + break + } + t := strings.SplitN(string(rs.Data), " ", 3) + if len(t) != 3 { + continue + } + hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0]) + hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1]) + hookOptions.RefFullNames = append(hookOptions.RefFullNames, t[2]) + } + + hookOptions.GitPushOptions = make(map[string]string) + + if hasPushOptions { + for { + rs, err = readPktLine(reader, pktLineTypeUnknow) + if err != nil { + return err + } + + if rs.Type == pktLineTypeFlush { + break + } + + kv := strings.SplitN(string(rs.Data), "=", 2) + if len(kv) == 2 { + hookOptions.GitPushOptions[kv[0]] = kv[1] + } + } + } + + // 3. run hook + resp, err := private.HookProcReceive(ctx, repoUser, repoName, hookOptions) + if err != nil { + return fail("Internal Server Error", "run proc-receive hook failed :%v", err) + } + + // 4. response result to service + // # a. OK, but has an alternate reference. The alternate reference name + // # and other status can be given in option directives. + // H: PKT-LINE(ok <ref>) + // H: PKT-LINE(option refname <refname>) + // H: PKT-LINE(option old-oid <old-oid>) + // H: PKT-LINE(option new-oid <new-oid>) + // H: PKT-LINE(option forced-update) + // H: ... ... + // H: flush-pkt + // # b. NO, I reject it. + // H: PKT-LINE(ng <ref> <reason>) + // # c. Fall through, let 'receive-pack' to execute it. + // H: PKT-LINE(ok <ref>) + // H: PKT-LINE(option fall-through) + + for _, rs := range resp.Results { + if len(rs.Err) > 0 { + err = writeDataPktLine(os.Stdout, []byte("ng "+rs.OriginalRef+" "+rs.Err)) + if err != nil { + return err + } + continue + } + + if rs.IsNotMatched { + err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef)) + if err != nil { + return err + } + err = writeDataPktLine(os.Stdout, []byte("option fall-through")) + if err != nil { + return err + } + continue + } + + err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef)) + if err != nil { + return err + } + err = writeDataPktLine(os.Stdout, []byte("option refname "+rs.Ref)) + if err != nil { + return err + } + if rs.OldOID != git.EmptySHA { + err = writeDataPktLine(os.Stdout, []byte("option old-oid "+rs.OldOID)) + if err != nil { + return err + } + } + err = writeDataPktLine(os.Stdout, []byte("option new-oid "+rs.NewOID)) + if err != nil { + return err + } + if rs.IsForcePush { + err = writeDataPktLine(os.Stdout, []byte("option forced-update")) + if err != nil { + return err + } + } + } + err = writeFlushPktLine(os.Stdout) + + return err +} + +// git PKT-Line api +// pktLineType message type of pkt-line +type pktLineType int64 + +const ( + // UnKnow type + pktLineTypeUnknow pktLineType = 0 + // flush-pkt "0000" + pktLineTypeFlush pktLineType = iota + // data line + pktLineTypeData +) + +// gitPktLine pkt-line api +type gitPktLine struct { + Type pktLineType + Length uint64 + Data []byte +} + +func readPktLine(in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) { + var ( + err error + r *gitPktLine + ) + + // read prefix + lengthBytes := make([]byte, 4) + for i := 0; i < 4; i++ { + lengthBytes[i], err = in.ReadByte() + if err != nil { + return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err) + } + } + + r = new(gitPktLine) + r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32) + if err != nil { + return nil, fail("Internal Server Error", "Pkt-Line format is wrong :%v", err) + } + + if r.Length == 0 { + if requestType == pktLineTypeData { + return nil, fail("Internal Server Error", "Pkt-Line format is wrong") + } + r.Type = pktLineTypeFlush + return r, nil + } + + if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush { + return nil, fail("Internal Server Error", "Pkt-Line format is wrong") + } + + r.Data = make([]byte, r.Length-4) + for i := range r.Data { + r.Data[i], err = in.ReadByte() + if err != nil { + return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err) + } + } + + r.Type = pktLineTypeData + + return r, nil +} + +func writeFlushPktLine(out io.Writer) error { + l, err := out.Write([]byte("0000")) + if err != nil { + return fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + if l != 4 { + return fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + + return nil +} + +func writeDataPktLine(out io.Writer, data []byte) error { + hexchar := []byte("0123456789abcdef") + hex := func(n uint64) byte { + return hexchar[(n)&15] + } + + length := uint64(len(data) + 4) + tmp := make([]byte, 4) + tmp[0] = hex(length >> 12) + tmp[1] = hex(length >> 8) + tmp[2] = hex(length >> 4) + tmp[3] = hex(length) + + lr, err := out.Write(tmp) + if err != nil { + return fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + if 4 != lr { + return fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + + lr, err = out.Write(data) + if err != nil { + return fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + if int(length-4) != lr { + return fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + + return nil +} diff --git a/cmd/hook_test.go b/cmd/hook_test.go new file mode 100644 index 0000000000..92c7e82a9a --- /dev/null +++ b/cmd/hook_test.go @@ -0,0 +1,41 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "bufio" + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPktLine(t *testing.T) { + // test read + s := strings.NewReader("0000") + r := bufio.NewReader(s) + result, err := readPktLine(r, pktLineTypeFlush) + assert.NoError(t, err) + assert.Equal(t, pktLineTypeFlush, result.Type) + + s = strings.NewReader("0006a\n") + r = bufio.NewReader(s) + result, err = readPktLine(r, pktLineTypeData) + assert.NoError(t, err) + assert.Equal(t, pktLineTypeData, result.Type) + assert.Equal(t, []byte("a\n"), result.Data) + + // test write + w := bytes.NewBuffer([]byte{}) + err = writeFlushPktLine(w) + assert.NoError(t, err) + assert.Equal(t, []byte("0000"), w.Bytes()) + + w.Reset() + err = writeDataPktLine(w, []byte("a\nb")) + assert.NoError(t, err) + assert.Equal(t, []byte("0007a\nb"), w.Bytes()) +} diff --git a/cmd/serv.go b/cmd/serv.go index 4479902ea1..2173a3a38b 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -17,6 +17,7 @@ import ( "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/pprof" @@ -146,6 +147,13 @@ func runServ(c *cli.Context) error { } if len(words) < 2 { + if git.CheckGitVersionAtLeast("2.29") == nil { + // for AGit Flow + if cmd == "ssh_info" { + fmt.Print(`{"type":"gitea","version":1}`) + return nil + } + } return fail("Too few arguments", "Too few arguments in cmd: %s", cmd) } |