diff options
Diffstat (limited to 'internal/command/githttp')
-rw-r--r-- | internal/command/githttp/push.go | 106 | ||||
-rw-r--r-- | internal/command/githttp/push_test.go | 150 |
2 files changed, 256 insertions, 0 deletions
diff --git a/internal/command/githttp/push.go b/internal/command/githttp/push.go new file mode 100644 index 0000000..3377baf --- /dev/null +++ b/internal/command/githttp/push.go @@ -0,0 +1,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() +} diff --git a/internal/command/githttp/push_test.go b/internal/command/githttp/push_test.go new file mode 100644 index 0000000..1363abd --- /dev/null +++ b/internal/command/githttp/push_test.go @@ -0,0 +1,150 @@ +package githttp + +import ( + "bytes" + "context" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver" + "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" +) + +var ( + flush = "0000" + infoRefsWithoutPrefix = "00c4e56497bb5f03a90a51293fc6d516788730953899 refs/heads/'test'report-status " + + "report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta push-options object-format=sha1 " + + "agent=git/2.38.3.gl200\n" + flush +) + +func TestExecute(t *testing.T) { + url, input := setup(t, http.StatusOK) + output := &bytes.Buffer{} + + cmd := &PushCommand{ + Config: &config.Config{GitlabUrl: url}, + ReadWriter: &readwriter.ReadWriter{Out: output, In: input}, + Response: &accessverifier.Response{ + Payload: accessverifier.CustomPayload{ + Data: accessverifier.CustomPayloadData{PrimaryRepo: url}, + }, + }, + } + + require.NoError(t, cmd.Execute(context.Background())) + require.Equal(t, infoRefsWithoutPrefix, output.String()) +} + +func TestExecuteWithFailedInfoRefs(t *testing.T) { + testCases := []struct { + desc string + statusCode int + responseContent string + expectedErr string + }{ + { + desc: "request failed", + statusCode: http.StatusForbidden, + expectedErr: "Internal API error (403)", + }, { + desc: "unexpected response", + statusCode: http.StatusOK, + responseContent: "unexpected response", + expectedErr: "Unexpected git-receive-pack response", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + requests := []testserver.TestRequestHandler{ + { + Path: "/info/refs", + Handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "git-receive-pack", r.URL.Query().Get("service")) + + w.WriteHeader(tc.statusCode) + w.Write([]byte(tc.responseContent)) + }, + }, + } + + url := testserver.StartHttpServer(t, requests) + + cmd := &PushCommand{ + Config: &config.Config{GitlabUrl: url}, + Response: &accessverifier.Response{ + Payload: accessverifier.CustomPayload{ + Data: accessverifier.CustomPayloadData{PrimaryRepo: url}, + }, + }, + } + + err := cmd.Execute(context.Background()) + require.Error(t, err) + require.Equal(t, tc.expectedErr, err.Error()) + }) + } +} + +func TestExecuteWithFailedReceivePack(t *testing.T) { + url, input := setup(t, http.StatusForbidden) + output := &bytes.Buffer{} + + cmd := &PushCommand{ + Config: &config.Config{GitlabUrl: url}, + ReadWriter: &readwriter.ReadWriter{Out: output, In: input}, + Response: &accessverifier.Response{ + Payload: accessverifier.CustomPayload{ + Data: accessverifier.CustomPayloadData{PrimaryRepo: url}, + }, + }, + } + + err := cmd.Execute(context.Background()) + require.Error(t, err) + require.Equal(t, "Internal API error (403)", err.Error()) +} + +func setup(t *testing.T, receivePackStatusCode int) (string, io.Reader) { + infoRefs := "001f# service=git-receive-pack\n" + flush + infoRefsWithoutPrefix + receivePackPrefix := "00ab4c9d98d7750fa65db8ddcc60a89ef919f7a179f9 df505c066e4e63a801268a84627d7e8f7e033c7a " + + "refs/heads/main123 report-status-v2 side-band-64k object-format=sha1 agent=git/2.39.1" + receivePackData := "PACK some data" + + // Imitate sending data via multiple packets + input := io.MultiReader( + strings.NewReader(receivePackPrefix), + strings.NewReader(flush), + strings.NewReader(receivePackData), + ) + + requests := []testserver.TestRequestHandler{ + { + Path: "/info/refs", + Handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "git-receive-pack", r.URL.Query().Get("service")) + + w.Write([]byte(infoRefs)) + }, + }, + { + Path: "/git-receive-pack", + Handler: func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + defer r.Body.Close() + + require.Equal(t, receivePackPrefix+flush+receivePackData, string(body)) + w.WriteHeader(receivePackStatusCode) + }, + }, + } + + return testserver.StartHttpServer(t, requests), input +} |