As we reevaluate how to best support and maintain Staging Ref in the future, we encourage development teams using this environment to highlight their use cases in the following issue: https://gitlab.com/gitlab-com/gl-infra/software-delivery/framework/software-delivery-framework-issue-tracker/-/issues/36.

Skip to content
Snippets Groups Projects
Commit 77f2e87c authored by Igor Drozdov's avatar Igor Drozdov
Browse files

Merge branch '656-add-pull-githttp-command' into 'main'

parents d8e35452 642e94db
No related branches found
No related tags found
No related merge requests found
package githttp
import (
"bytes"
"context"
"fmt"
"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"
"io"
)
const pullService = "git-upload-pack"
var uploadPackHttpPrefix = []byte("001e# service=git-upload-pack\n0000")
type PullCommand 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-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
func (c *PullCommand) Execute(ctx context.Context) error {
data := c.Response.Payload.Data
client := &git.Client{Url: data.PrimaryRepo, Headers: data.RequestHeaders}
if err := c.requestInfoRefs(ctx, client); err != nil {
return err
}
return c.requestUploadPack(ctx, client)
}
func (c *PullCommand) requestInfoRefs(ctx context.Context, client *git.Client) error {
response, err := client.InfoRefs(ctx, pullService)
if err != nil {
return err
}
defer response.Body.Close()
// Read the first bytes that contain 001e# service=git-upload-pack\n0000 string
// to convert HTTP(S) Git response to the one expected by SSH
p := make([]byte, len(uploadPackHttpPrefix))
_, err = response.Body.Read(p)
if err != nil || !bytes.Equal(p, uploadPackHttpPrefix) {
return fmt.Errorf("Unexpected git-upload-pack response")
}
_, err = io.Copy(c.ReadWriter.Out, response.Body)
return err
}
func (c *PullCommand) requestUploadPack(ctx context.Context, client *git.Client) error {
pipeReader, pipeWriter := io.Pipe()
go c.readFromStdin(pipeWriter)
response, err := client.UploadPack(ctx, pipeReader)
if err != nil {
return err
}
defer response.Body.Close()
_, err = io.Copy(c.ReadWriter.Out, response.Body)
return err
}
func (c *PullCommand) readFromStdin(pw *io.PipeWriter) {
scanner := pktline.NewScanner(c.ReadWriter.In)
for scanner.Scan() {
line := scanner.Bytes()
if pktline.IsDone(line) {
pw.Write(line)
break
}
pw.Write(line)
}
pw.Close()
}
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 cloneResponse = `0090want 11d731b83788cd556abea7b465c6bee52d89923c multi_ack_detailed side-band-64k thin-pack ofs-delta deepen-since deepen-not agent=git/2.41.0
0032want e56497bb5f03a90a51293fc6d516788730953899
00000009done
`
func TestPullExecute(t *testing.T) {
url := setupPull(t, http.StatusOK)
output := &bytes.Buffer{}
input := strings.NewReader(cloneResponse)
cmd := &PullCommand{
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 TestPullExecuteWithFailedInfoRefs(t *testing.T) {
testCases := []struct {
desc string
statusCode int
responseContent string
expectedErr string
}{
{
desc: "request failed",
statusCode: http.StatusForbidden,
expectedErr: "Remote repository is unavailable",
}, {
desc: "unexpected response",
statusCode: http.StatusOK,
responseContent: "unexpected response",
expectedErr: "Unexpected git-upload-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-upload-pack", r.URL.Query().Get("service"))
w.WriteHeader(tc.statusCode)
w.Write([]byte(tc.responseContent))
},
},
}
url := testserver.StartHttpServer(t, requests)
cmd := &PullCommand{
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 TestExecuteWithFailedUploadPack(t *testing.T) {
url := setupPull(t, http.StatusForbidden)
output := &bytes.Buffer{}
input := strings.NewReader(cloneResponse)
cmd := &PullCommand{
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, "Remote repository is unavailable", err.Error())
}
func setupPull(t *testing.T, uploadPackStatusCode int) string {
infoRefs := "001e# service=git-upload-pack\n" + flush + infoRefsWithoutPrefix
requests := []testserver.TestRequestHandler{
{
Path: "/info/refs",
Handler: func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "git-upload-pack", r.URL.Query().Get("service"))
w.Write([]byte(infoRefs))
},
},
{
Path: "/git-upload-pack",
Handler: func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
require.True(t, strings.HasSuffix(string(body), "0009done\n"))
w.WriteHeader(uploadPackStatusCode)
},
},
}
return testserver.StartHttpServer(t, requests)
}
Loading
Loading
@@ -2,6 +2,7 @@ package uploadpack
import (
"context"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/githttp"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
Loading
Loading
@@ -37,6 +38,16 @@ func (c *Command) Execute(ctx context.Context) (context.Context, error) {
ctxWithLogData := context.WithValue(ctx, "logData", logData)
if response.IsCustomAction() {
if response.Payload.Data.GeoProxyFetchDirectToPrimary {
cmd := githttp.PullCommand{
Config: c.Config,
ReadWriter: c.ReadWriter,
Response: response,
}
return ctxWithLogData, cmd.Execute(ctx)
}
customAction := customaction.Command{
Config: c.Config,
ReadWriter: c.ReadWriter,
Loading
Loading
Loading
Loading
@@ -39,6 +39,17 @@ func (c *Client) ReceivePack(ctx context.Context, body io.Reader) (*http.Respons
return c.do(request)
}
func (c *Client) UploadPack(ctx context.Context, body io.Reader) (*http.Response, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.Url+"/git-upload-pack", body)
if err != nil {
return nil, err
}
request.Header.Add("Content-Type", "application/x-git-upload-pack-request")
request.Header.Add("Accept", "application/x-git-upload-pack-result")
return c.do(request)
}
func (c *Client) do(request *http.Request) (*http.Response, error) {
for k, v := range c.Headers {
request.Header.Add(k, v)
Loading
Loading
Loading
Loading
@@ -50,6 +50,20 @@ func TestReceivePack(t *testing.T) {
require.Equal(t, "git-receive-pack: content", string(body))
}
func TestUploadPack(t *testing.T) {
client := setup(t)
refsBody := "0032want 0a53e9ddeaddad63ad106860237bbf53411d11a7\n"
response, err := client.UploadPack(context.Background(), bytes.NewReader([]byte(refsBody)))
require.NoError(t, err)
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
require.NoError(t, err)
require.Equal(t, "git-upload-pack: content", string(body))
}
func TestFailedHTTPRequest(t *testing.T) {
client := &Client{
Url: testserver.StartHttpServer(t, []testserver.TestRequestHandler{}),
Loading
Loading
@@ -83,7 +97,6 @@ func setup(t *testing.T) *Client {
require.Equal(t, customHeaders["Header-One"], r.Header.Get("Header-One"))
require.Equal(t, "application/x-git-receive-pack-request", r.Header.Get("Content-Type"))
require.Equal(t, "application/x-git-receive-pack-result", r.Header.Get("Accept"))
require.Equal(t, customHeaders["Header-One"], r.Header.Get("Header-One"))
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
Loading
Loading
@@ -93,6 +106,21 @@ func setup(t *testing.T) *Client {
w.Write(body)
},
},
{
Path: "/git-upload-pack",
Handler: func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, customHeaders["Authorization"], r.Header.Get("Authorization"))
require.Equal(t, customHeaders["Header-One"], r.Header.Get("Header-One"))
require.Equal(t, "application/x-git-upload-pack-request", r.Header.Get("Content-Type"))
require.Equal(t, "application/x-git-upload-pack-result", r.Header.Get("Accept"))
_, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
w.Write([]byte("git-upload-pack: content"))
},
},
}
client := &Client{
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment