summaryrefslogtreecommitdiff
path: root/internal/command/githttp/push.go
blob: 3377baff0f901bc0ce59209eb342dafb18eee737 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package githttp

import (
	"bytes"
	"context"
	"fmt"
	"io"

	"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/readwriter"
	"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
	"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/accessverifier"
	"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/git"
	"gitlab.com/gitlab-org/gitlab-shell/v14/internal/pktline"
)

const service = "git-receive-pack"

var receivePackHttpPrefix = []byte("001f# service=git-receive-pack\n0000")

type PushCommand struct {
	Config     *config.Config
	ReadWriter *readwriter.ReadWriter
	Response   *accessverifier.Response
}

// See Uploading Data > HTTP(S) section at:
// https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
//
// 1. Perform /info/refs?service=git-receive-pack request
// 2. Remove the header to make it consumable by SSH protocol
// 3. Send the result to the user via SSH (writeToStdout)
// 4. Read the send-pack data provided by user via SSH (stdinReader)
// 5. Perform /git-receive-pack request and send this data
// 6. Return the output to the user
func (c *PushCommand) Execute(ctx context.Context) error {
	data := c.Response.Payload.Data
	client, err := git.NewClient(c.Config, data.PrimaryRepo, data.RequestHeaders)
	if err != nil {
		return err
	}

	if err := c.requestInfoRefs(ctx, client); err != nil {
		return err
	}

	return c.requestReceivePack(ctx, client)
}

func (c *PushCommand) requestInfoRefs(ctx context.Context, client *git.Client) error {
	response, err := client.InfoRefs(ctx, service)
	if err != nil {
		return err
	}
	defer response.Body.Close()

	// Read the first bytes that contain 001f# service=git-receive-pack\n0000 string
	// to convert HTTP(S) Git response to the one expected by SSH
	p := make([]byte, len(receivePackHttpPrefix))
	_, err = response.Body.Read(p)
	if err != nil || !bytes.Equal(p, receivePackHttpPrefix) {
		return fmt.Errorf("Unexpected git-receive-pack response")
	}

	_, err = io.Copy(c.ReadWriter.Out, response.Body)

	return err
}

func (c *PushCommand) requestReceivePack(ctx context.Context, client *git.Client) error {
	pipeReader, pipeWriter := io.Pipe()
	go c.readFromStdin(pipeWriter)

	response, err := client.ReceivePack(ctx, pipeReader)
	if err != nil {
		return err
	}
	defer response.Body.Close()

	_, err = io.Copy(c.ReadWriter.Out, response.Body)

	return err
}

func (c *PushCommand) readFromStdin(pw *io.PipeWriter) {
	var needsPackData bool

	scanner := pktline.NewScanner(c.ReadWriter.In)
	for scanner.Scan() {
		line := scanner.Bytes()
		pw.Write(line)

		if pktline.IsFlush(line) {
			break
		}

		if !needsPackData && !pktline.IsRefRemoval(line) {
			needsPackData = true
		}
	}

	if needsPackData {
		io.Copy(pw, c.ReadWriter.In)
	}

	pw.Close()
}