diff --git a/internal/api/proxy_git_push_ssh.go b/internal/api/proxy_git_push_ssh.go index 6a0b60959e73a7185864b635ed95046edfa26ca7..05d10d1dfb24c81b87d374e60517e92bd3c0a736 100644 --- a/internal/api/proxy_git_push_ssh.go +++ b/internal/api/proxy_git_push_ssh.go @@ -7,6 +7,11 @@ type ProxyGitPushSSH struct { // See GitlabShellCustomActionData below CustomActionData *CustomActionData `json:"gitlab_shell_custom_action_data"` + // Output contains any necessary content from say a previous action + // e.g. for an /info/refs?service=git-receive-pack request, Output will + // contain that output which is utilised in geo.newPushRequest() + Output string `json:"gitlab_shell_output"` + // Authorization header content Authorization string `json:"authorization"` } diff --git a/internal/geo/helpers.go b/internal/geo/helpers.go new file mode 100644 index 0000000000000000000000000000000000000000..0654d74459cf3d1b3a8719a85421bf2e613b98fe --- /dev/null +++ b/internal/geo/helpers.go @@ -0,0 +1,33 @@ +package geo + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "gitlab.com/gitlab-org/gitlab-workhorse/internal/api" +) + +func parseRailsResponse(proxyGitPushSSH string) (*api.ProxyGitPushSSH, error) { + proxyGitPushSSHData := &api.ProxyGitPushSSH{CustomActionData: &api.CustomActionData{}} + err := json.Unmarshal([]byte(proxyGitPushSSH), proxyGitPushSSHData) + + return proxyGitPushSSHData, err +} + +func performRequest(req *http.Request) (string, error) { + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + + rawBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + resp.Body.Close() + + return string(rawBody), nil +} diff --git a/internal/geo/helpers_test.go b/internal/geo/helpers_test.go new file mode 100644 index 0000000000000000000000000000000000000000..50f9bdee6f2430f093e1c6ee887c6cd7df336567 --- /dev/null +++ b/internal/geo/helpers_test.go @@ -0,0 +1,37 @@ +package geo + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseRailsResponse(t *testing.T) { + proxyGitPushSSHData, err := parseRailsResponse(`{ + "gitlab_shell_custom_action_data": { + "gl_id": "user-1", + "primary_repo": "http://primary.geo/user/repo.git" + }, + "authorization": "Fake abcd1234" + }`) + + assert.Nil(t, err) + assert.Equal(t, "user-1", proxyGitPushSSHData.CustomActionData.GlID) + assert.Equal(t, "http://primary.geo/user/repo.git", proxyGitPushSSHData.CustomActionData.PrimaryRepo) + assert.Equal(t, "Fake abcd1234", proxyGitPushSSHData.Authorization) +} +func TestPerformRequest(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("info_refs response")) + })) + defer ts.Close() + + req, _ := http.NewRequest("GET", ts.URL, nil) + rawBody, err := performRequest(req) + + assert.Nil(t, err) + assert.Equal(t, rawBody, "info_refs response") +} diff --git a/internal/geo/info_refs.go b/internal/geo/info_refs.go index 75e6f26f7a07bf7dcaa7de221b5c5c48d0a2b03f..79ba588bc219bc7bbedba0bd9fb81b5d872844ad 100644 --- a/internal/geo/info_refs.go +++ b/internal/geo/info_refs.go @@ -4,12 +4,9 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io/ioutil" "net/http" "regexp" - log "github.com/sirupsen/logrus" - "gitlab.com/gitlab-org/gitlab-workhorse/internal/api" ) @@ -44,7 +41,7 @@ func ProxyGitPushSSHInfoRefs(a *api.API) http.Handler { return } - rawBody, err := makeInfoRefsCall(req) + rawBody, err := performRequest(req) if err != nil { logHTTPError(w, err, "Failed to GET info_refs from primary") return @@ -60,13 +57,6 @@ func ProxyGitPushSSHInfoRefs(a *api.API) http.Handler { }) } -func parseRailsResponse(proxyGitPushSSH string) (*api.ProxyGitPushSSH, error) { - proxyGitPushSSHData := &api.ProxyGitPushSSH{CustomActionData: &api.CustomActionData{}} - err := json.Unmarshal([]byte(proxyGitPushSSH), proxyGitPushSSHData) - - return proxyGitPushSSHData, err -} - func newInfoRefsRequest(proxyGitPushSSHData *api.ProxyGitPushSSH) (*http.Request, error) { url := fmt.Sprintf("%s/info/refs?service=git-receive-pack", proxyGitPushSSHData.CustomActionData.PrimaryRepo) req, err := http.NewRequest("GET", url, nil) @@ -81,23 +71,6 @@ func newInfoRefsRequest(proxyGitPushSSHData *api.ProxyGitPushSSH) (*http.Request return req, nil } -func makeInfoRefsCall(req *http.Request) (string, error) { - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", err - } - - rawBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - - resp.Body.Close() - - return string(rawBody), nil -} - // HTTP(S) and SSH responses are very similar, except for the fragment below. // As we're performing a git HTTP(S) request here, we'll get a HTTP(s) // suitable git response. However, we're executing in the context of an @@ -119,13 +92,3 @@ func processInfoRefsResponse(rawBody string) (string, error) { return string(json), err } - -func logHTTPError(w http.ResponseWriter, err error, msg string) { - http.Error(w, msg, 500) - - log.WithFields(log.Fields{ - "msg": msg, - "code": 500, - "err": err, - }).Error("geo.logHTTPError") -} diff --git a/internal/geo/info_refs_test.go b/internal/geo/info_refs_test.go index 04d6f23714f32da423288502518fefbf1e9ded81..cf0fca15fdfa9ff6a74f3b6f4f602ef8e0773958 100644 --- a/internal/geo/info_refs_test.go +++ b/internal/geo/info_refs_test.go @@ -1,8 +1,6 @@ package geo import ( - "net/http" - "net/http/httptest" "testing" "github.com/stretchr/testify/assert" @@ -12,27 +10,13 @@ const glID = "user-1" const primaryRepo = "http://primary.geo/user/repo.git" const authorization = "Fake abcd1234" -func TestParseRailsResponse(t *testing.T) { - proxyGitPushSSHData, err := parseRailsResponse(`{ - "gitlab_shell_custom_action_data": { - "gl_id": "user-1", - "primary_repo": "http://primary.geo/user/repo.git" - }, - "authorization": "Fake abcd1234" - }`) - - assert.Nil(t, err) - assert.Equal(t, glID, proxyGitPushSSHData.CustomActionData.GlID) - assert.Equal(t, primaryRepo, proxyGitPushSSHData.CustomActionData.PrimaryRepo) - assert.Equal(t, authorization, proxyGitPushSSHData.Authorization) -} - func TestNewInfoRefsRequest(t *testing.T) { proxyGitPushSSHData, _ := parseRailsResponse(`{ "gitlab_shell_custom_action_data": { "gl_id": "user-1", "primary_repo": "http://primary.geo/user/repo.git" }, + "gitlab_shell_output": "", "authorization": "Fake abcd1234" }`) @@ -40,22 +24,8 @@ func TestNewInfoRefsRequest(t *testing.T) { assert.Nil(t, err) assert.Equal(t, infoRefsContentTypeHeader, req.Header.Get("Content-Type")) - assert.Equal(t, glID, req.Header.Get("Geo-GL-Id")) - assert.Equal(t, authorization, req.Header.Get("Authorization")) -} - -func TestMakeInfoRefsCall(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("info_refs response")) - })) - defer ts.Close() - - req, _ := http.NewRequest("GET", ts.URL, nil) - rawBody, err := makeInfoRefsCall(req) - - assert.Nil(t, err) - assert.Equal(t, rawBody, "info_refs response") + assert.Equal(t, "user-1", req.Header.Get("Geo-GL-Id")) + assert.Equal(t, "Fake abcd1234", req.Header.Get("Authorization")) } func TestProcessInfoRefsResponse(t *testing.T) { diff --git a/internal/geo/logger.go b/internal/geo/logger.go new file mode 100644 index 0000000000000000000000000000000000000000..b992795ace9e71864d844abfb2ec718132822d6b --- /dev/null +++ b/internal/geo/logger.go @@ -0,0 +1,17 @@ +package geo + +import ( + "net/http" + + log "github.com/sirupsen/logrus" +) + +func logHTTPError(w http.ResponseWriter, err error, msg string) { + http.Error(w, msg, 500) + + log.WithFields(log.Fields{ + "msg": msg, + "code": 500, + "err": err, + }).Error("geo.logHTTPError") +} diff --git a/internal/geo/push.go b/internal/geo/push.go new file mode 100644 index 0000000000000000000000000000000000000000..d0c21f1403abc47407a4344af9b77a26ef8e27b8 --- /dev/null +++ b/internal/geo/push.go @@ -0,0 +1,86 @@ +package geo + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "gitlab.com/gitlab-org/gitlab-workhorse/internal/api" +) + +const pushContentTypeHeader = "application/x-git-receive-pack-request" +const pushAcceptHeader = "application/x-git-receive-pack-result" + +type PushResponse struct { + Status bool `json:"status"` + Message string `json:"message"` + Result string `json:"result"` +} + +func ProxyGitPushSSHPush(a *api.API) http.Handler { + return a.CustomActionHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) { + + proxyGitPushSSHData, err := parseRailsResponse(a.ProxyGitPushSSH) + if err != nil { + logHTTPError(w, err, "Failed to parse ProxyGitPushSSH JSON") + return + } + + req, err := newPushRequest(proxyGitPushSSHData) + if err != nil { + logHTTPError(w, err, "Failed to create new GET push request") + return + } + + rawBody, err := performRequest(req) + if err != nil { + logHTTPError(w, err, "Failed to GET push from primary") + return + } + + jsonResponse, err := processPushResponse(rawBody) + if err != nil { + logHTTPError(w, err, "Failed to process push from primary") + return + } + + fmt.Fprint(w, jsonResponse) + }) +} + +func newPushRequest(proxyGitPushSSHData *api.ProxyGitPushSSH) (*http.Request, error) { + url := fmt.Sprintf("%s/git-receive-pack", proxyGitPushSSHData.CustomActionData.PrimaryRepo) + req, err := http.NewRequest("POST", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", pushContentTypeHeader) + req.Header.Set("Accept", pushAcceptHeader) + req.Header.Set("Geo-GL-Id", proxyGitPushSSHData.CustomActionData.GlID) + req.Header.Set("Authorization", proxyGitPushSSHData.Authorization) + + base64Result, err := base64.StdEncoding.DecodeString(proxyGitPushSSHData.Output) + if err != nil { + return nil, err + } + + req.Body = ioutil.NopCloser(strings.NewReader(string(base64Result))) + + return req, nil +} + +func processPushResponse(rawBody string) (string, error) { + base64Result := base64.URLEncoding.EncodeToString([]byte(rawBody)) + response := InfoRefsResponse{Status: true, Message: "", Result: base64Result} + + json, err := json.Marshal(response) + if err != nil { + return "", err + } + + return string(json), err +} diff --git a/internal/geo/push_test.go b/internal/geo/push_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ec281d527a2211484f9b29fcb764d51605faa3c3 --- /dev/null +++ b/internal/geo/push_test.go @@ -0,0 +1,40 @@ +package geo + +import ( + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewPushRequest(t *testing.T) { + proxyGitPushSSHData, _ := parseRailsResponse(`{ + "gitlab_shell_custom_action_data": { + "gl_id": "user-1", + "primary_repo": "http://primary.geo/user/repo.git" + }, + "gitlab_shell_output": "ZmFrZSBjb250ZW50IGhlcmU=\n", + "authorization": "Fake abcd1234" + }`) + + req, err := newPushRequest(proxyGitPushSSHData) + + assert.Nil(t, err) + assert.Equal(t, pushContentTypeHeader, req.Header.Get("Content-Type")) + assert.Equal(t, pushAcceptHeader, req.Header.Get("Accept")) + assert.Equal(t, "user-1", req.Header.Get("Geo-GL-Id")) + assert.Equal(t, "Fake abcd1234", req.Header.Get("Authorization")) + + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err) + assert.Equal(t, []uint8([]byte{0x66, 0x61, 0x6b, 0x65, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x20, 0x68, 0x65, 0x72, 0x65}), body) +} + +func TestProcessPushResponse(t *testing.T) { + rawBody := "fake response from push" + + json, err := processPushResponse(rawBody) + + assert.Nil(t, err) + assert.Equal(t, `{"status":true,"message":"","result":"ZmFrZSByZXNwb25zZSBmcm9tIHB1c2g="}`, json) +} diff --git a/internal/upstream/routes.go b/internal/upstream/routes.go index 647a411f8d3da7cf91c58d36c18941dc182e2503..2e734e0c384229aab32271eca80e4b66591457d7 100644 --- a/internal/upstream/routes.go +++ b/internal/upstream/routes.go @@ -181,6 +181,7 @@ func (u *upstream) configureRoutes() { // Geo git push to secondary (for SSH) route("POST", apiPattern+`v4/geo/proxy_git_push_ssh/info_refs\z`, geo.ProxyGitPushSSHInfoRefs(api)), + route("POST", apiPattern+`v4/geo/proxy_git_push_ssh/push\z`, geo.ProxyGitPushSSHPush(api)), // Explicitly proxy API requests route("", apiPattern, proxy),