internal/command/githttp/pull.go (80 lines of code) (raw):
// Package githttp provides functionality to handle Git operations over HTTP(S) and SSH,
// including executing Git commands like git-upload-pack and converting responses to the
// expected format for SSH protocols. It integrates with GitLab's internal components
// for secure access verification and data transfer.
package githttp
import (
"context"
"io"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
"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"
"gitlab.com/gitlab-org/labkit/log"
)
const pullService = "git-upload-pack"
var uploadPackHTTPPrefix = []byte("001e# service=git-upload-pack\n0000")
// PullCommand handles the execution of a Git pull operation over HTTP(S) or SSH
type PullCommand struct {
Config *config.Config
ReadWriter *readwriter.ReadWriter
Args *commandargs.Shell
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-upload-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-upload-pack request and send this data
// 6. Return the output to the user
// ForInfoRefs returns the necessary Pull specifics for client.InfoRefs()
func (c *PullCommand) ForInfoRefs() (*readwriter.ReadWriter, string, []byte) {
return c.ReadWriter, pullService, uploadPackHTTPPrefix
}
// Execute runs the pull command by determining the appropriate method (HTTP/SSH)
func (c *PullCommand) Execute(ctx context.Context) error {
data := c.Response.Payload.Data
client := &git.Client{URL: data.PrimaryRepo, Headers: data.RequestHeaders}
// For Git over SSH routing
if data.GeoProxyFetchSSHDirectToPrimary {
log.ContextLogger(ctx).Info("Using Git over SSH upload pack")
client.Headers["Git-Protocol"] = c.Args.Env.GitProtocolVersion
return c.requestSSHUploadPack(ctx, client)
}
if err := requestInfoRefs(ctx, client, c); err != nil {
return err
}
return c.requestUploadPack(ctx, client, data.GeoProxyFetchDirectToPrimaryWithOptions)
}
func (c *PullCommand) requestSSHUploadPack(ctx context.Context, client *git.Client) error {
response, err := client.SSHUploadPack(ctx, io.NopCloser(c.ReadWriter.In))
if err != nil {
return err
}
defer response.Body.Close() //nolint:errcheck
_, err = io.Copy(c.ReadWriter.Out, response.Body)
return err
}
func (c *PullCommand) requestUploadPack(ctx context.Context, client *git.Client, geoProxyFetchDirectToPrimaryWithOptions bool) error {
pipeReader, pipeWriter := io.Pipe()
go c.readFromStdin(pipeWriter, geoProxyFetchDirectToPrimaryWithOptions)
response, err := client.UploadPack(ctx, pipeReader)
if err != nil {
return err
}
defer response.Body.Close() //nolint:errcheck
_, err = io.Copy(c.ReadWriter.Out, response.Body)
return err
}
func (c *PullCommand) readFromStdin(pw *io.PipeWriter, geoProxyFetchDirectToPrimaryWithOptions bool) {
scanner := pktline.NewScanner(c.ReadWriter.In)
for scanner.Scan() {
line := scanner.Bytes()
_, err := pw.Write(line)
if err != nil {
log.WithError(err).Error("failed to write line")
}
if pktline.IsDone(line) {
break
}
if pktline.IsFlush(line) && geoProxyFetchDirectToPrimaryWithOptions {
_, err := pw.Write(pktline.PktDone())
if err != nil {
log.WithError(err).Error("failed to write packet done line")
}
break
}
}
err := pw.Close()
if err != nil {
log.WithError(err).Error("failed to close writer")
}
}