diff options
author | Jacob Vosmaer (GitLab) <jacob@gitlab.com> | 2017-01-12 23:24:50 +0300 |
---|---|---|
committer | Jacob Vosmaer (GitLab) <jacob@gitlab.com> | 2017-01-12 23:24:50 +0300 |
commit | 634bbbe031593e979786a1a4dcd42a449adc3bc6 (patch) | |
tree | 9850f51bf2a59c16835e9627bee11acf9082537e | |
parent | 7b80a75e0c266af6ced2bc43d827fface0078d27 (diff) | |
parent | 7b82af677652fc5b586eb80e2ad87eeed74932aa (diff) |
Merge branch 'feature/upload-receive-pack' into 'master'
Implement git-{upload,receive}-pack handler
Closes #43
See merge request !38
-rw-r--r-- | helper/helper.go | 40 | ||||
-rw-r--r-- | router/home.go (renamed from handler/home.go) | 2 | ||||
-rw-r--r-- | router/home_test.go (renamed from handler/home_test.go) | 4 | ||||
-rw-r--r-- | router/info_refs.go | 103 | ||||
-rw-r--r-- | router/info_refs_test.go | 126 | ||||
-rw-r--r-- | router/router.go | 5 |
6 files changed, 274 insertions, 6 deletions
diff --git a/helper/helper.go b/helper/helper.go new file mode 100644 index 000000000..9886afd69 --- /dev/null +++ b/helper/helper.go @@ -0,0 +1,40 @@ +package helper + +import ( + "log" + "net/http" + "os/exec" + "syscall" +) + +func Fail500(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, "Internal server error", 500) + printError(r, err) +} + +func LogError(r *http.Request, err error) { + printError(r, err) +} + +func printError(r *http.Request, err error) { + if r != nil { + log.Printf("error: %s %q: %v", r.Method, r.RequestURI, err) + } else { + log.Printf("error: %v", err) + } +} + +func CleanUpProcessGroup(cmd *exec.Cmd) { + if cmd == nil { + return + } + + process := cmd.Process + if process != nil && process.Pid > 0 { + // Send SIGTERM to the process group of cmd + syscall.Kill(-process.Pid, syscall.SIGTERM) + } + + // reap our child process + cmd.Wait() +} diff --git a/handler/home.go b/router/home.go index a57a55da5..54d09d3c0 100644 --- a/handler/home.go +++ b/router/home.go @@ -1,4 +1,4 @@ -package handler +package router import ( "net/http" diff --git a/handler/home_test.go b/router/home_test.go index ba1c0e689..d947d60dc 100644 --- a/handler/home_test.go +++ b/router/home_test.go @@ -1,4 +1,4 @@ -package handler +package router import ( "net/http" @@ -14,7 +14,7 @@ func TestGetHome(t *testing.T) { t.Fatal("Creating 'GET /' request failed!") } - http.HandlerFunc(Home).ServeHTTP(recorder, req) + NewRouter().ServeHTTP(recorder, req) if recorder.Code != http.StatusOK { t.Fatal("Server error: Returned ", recorder.Code, " instead of ", http.StatusOK) diff --git a/router/info_refs.go b/router/info_refs.go new file mode 100644 index 000000000..2f5db24d3 --- /dev/null +++ b/router/info_refs.go @@ -0,0 +1,103 @@ +package router + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "syscall" + + "gitlab.com/gitlab-org/gitaly/helper" + + "github.com/gorilla/mux" +) + +const ( + gitalyRepoPathHeader = "Gitaly-Repo-Path" + gitlabIdHeader = "Gitaly-GL-Id" +) + +func GetInfoRefs(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + rpc := vars["service"] + + glId := r.Header.Get(gitlabIdHeader) + if glId == "" { + helper.Fail500(w, r, fmt.Errorf("GetInfoRefs: %s header was not found", gitlabIdHeader)) + return + } + repoPath := r.Header.Get(gitalyRepoPathHeader) + if repoPath == "" { + helper.Fail500(w, r, fmt.Errorf("GetInfoRefs: %s header was not found", gitalyRepoPathHeader)) + return + } + + // Prepare our Git subprocess + cmd := gitCommand(glId, "git", rpc, "--stateless-rpc", "--advertise-refs", repoPath) + + stdout, err := cmd.StdoutPipe() + if err != nil { + helper.Fail500(w, r, fmt.Errorf("GetInfoRefs: stdout: %v", err)) + return + } + defer stdout.Close() + + if err := cmd.Start(); err != nil { + helper.Fail500(w, r, fmt.Errorf("GetInfoRefs: start %v: %v", cmd.Args, err)) + return + } + defer helper.CleanUpProcessGroup(cmd) // Ensure brute force subprocess clean-up + + // Start writing the response + w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", rpc)) + w.Header().Set("Cache-Control", "no-cache") + w.WriteHeader(200) // Don't bother with HTTP 500 from this point on, just return + + if err := pktLine(w, fmt.Sprintf("# service=git-%s\n", rpc)); err != nil { + helper.LogError(r, fmt.Errorf("GetInfoRefs: pktLine: %v", err)) + return + } + + if err := pktFlush(w); err != nil { + helper.LogError(r, fmt.Errorf("GetInfoRefs: pktFlush: %v", err)) + return + } + + if _, err := io.Copy(w, stdout); err != nil { + helper.LogError(r, fmt.Errorf("GetInfoRefs: copy output of %v: %v", cmd.Args, err)) + return + } + + if err := cmd.Wait(); err != nil { + helper.LogError(r, fmt.Errorf("GetInfoRefs: wait for %v: %v", cmd.Args, err)) + return + } +} + +func gitCommand(glId string, name string, args ...string) *exec.Cmd { + cmd := exec.Command(name, args...) + // Start the command in its own process group (nice for signalling) + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + // Explicitly set the environment for the Git command + cmd.Env = []string{ + fmt.Sprintf("HOME=%s", os.Getenv("HOME")), + fmt.Sprintf("PATH=%s", os.Getenv("PATH")), + fmt.Sprintf("LD_LIBRARY_PATH=%s", os.Getenv("LD_LIBRARY_PATH")), + fmt.Sprintf("GL_ID=%s", glId), + fmt.Sprintf("GL_PROTOCOL=http"), + } + // If we don't do something with cmd.Stderr, Git errors will be lost + cmd.Stderr = os.Stderr + return cmd +} + +func pktLine(w io.Writer, s string) error { + _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s) + return err +} + +func pktFlush(w io.Writer) error { + _, err := fmt.Fprint(w, "0000") + return err +} diff --git a/router/info_refs_test.go b/router/info_refs_test.go new file mode 100644 index 000000000..fc280bbe3 --- /dev/null +++ b/router/info_refs_test.go @@ -0,0 +1,126 @@ +package router + +import ( + "bytes" + "log" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path" + "strings" + "testing" +) + +const testRepoRoot = "testdata/data" +const testRepo = "group/test.git" + +func TestMain(m *testing.M) { + source := "https://gitlab.com/gitlab-org/gitlab-test.git" + clonePath := path.Join(testRepoRoot, testRepo) + if _, err := os.Stat(clonePath); err != nil { + testCmd := exec.Command("git", "clone", "--bare", source, clonePath) + testCmd.Stdout = os.Stdout + testCmd.Stderr = os.Stderr + + if err := testCmd.Run(); err != nil { + log.Printf("Test setup: failed to run %v", testCmd) + os.Exit(-1) + } + } + + os.Exit(func() int { + return m.Run() + }()) +} + +func TestSuccessfulUploadPackRequest(t *testing.T) { + recorder := httptest.NewRecorder() + + resource := "/projects/1/git-http/info-refs/upload-pack" + req, err := http.NewRequest("GET", resource, &bytes.Buffer{}) + if err != nil { + t.Fatal("Failed creating a request to %s", resource) + } + + req.Header.Add("Gitaly-Repo-Path", path.Join(testRepoRoot, testRepo)) + req.Header.Add("Gitaly-GL-Id", "user-123") + + NewRouter().ServeHTTP(recorder, req) + + if recorder.Code != 200 { + t.Errorf("GET %q: expected 200, got %d", resource, recorder.Code) + } + + response := recorder.Body.String() + assertGitRefAdvertisement(t, resource, response, "001e# service=git-upload-pack", "0000", []string{ + "003ef4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 refs/tags/v1.0.0", + "00416f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 refs/tags/v1.0.0^{}", + }) +} + +func TestSuccessfulReceivePackRequest(t *testing.T) { + recorder := httptest.NewRecorder() + + resource := "/projects/1/git-http/info-refs/receive-pack" + req, err := http.NewRequest("GET", resource, &bytes.Buffer{}) + if err != nil { + t.Fatal("Failed creating a request to %s", resource) + } + + req.Header.Add("Gitaly-Repo-Path", path.Join(testRepoRoot, testRepo)) + req.Header.Add("Gitaly-GL-Id", "user-123") + + NewRouter().ServeHTTP(recorder, req) + + if recorder.Code != 200 { + t.Errorf("GET %q: expected 200, got %d", resource, recorder.Code) + } + + response := recorder.Body.String() + assertGitRefAdvertisement(t, resource, response, "001f# service=git-receive-pack", "0000", []string{ + "003ef4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 refs/tags/v1.0.0", + "003e8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b refs/tags/v1.1.0", + }) +} + +func TestFailedUploadPackRequestDueToMissingHeaders(t *testing.T) { + recorder := httptest.NewRecorder() + + resource := "/projects/1/git-http/info-refs/upload-pack" + req, err := http.NewRequest("GET", resource, &bytes.Buffer{}) + if err != nil { + t.Fatal("Failed creating a request to %s", resource) + } + + for _, headerName := range []string{"Gitaly-Repo-Path", "Gitaly-GL-Id"} { + req.Header.Set(headerName, "Dummy Value") + + NewRouter().ServeHTTP(recorder, req) + + if recorder.Code != 500 { + t.Errorf("GET %q: expected 200, got %d", resource, recorder.Code) + } + + req.Header.Del(headerName) + } +} + +func assertGitRefAdvertisement(t *testing.T, requestPath, responseBody string, firstLine, lastLine string, middleLines []string) { + responseLines := strings.Split(responseBody, "\n") + + if responseLines[0] != firstLine { + t.Errorf("GET %q: expected response first line to be %q, found %q", requestPath, firstLine, responseLines[0]) + } + + lastIndex := len(responseLines) - 1 + if responseLines[lastIndex] != lastLine { + t.Errorf("GET %q: expected response last line to be %q, found %q", requestPath, lastLine, responseLines[lastIndex]) + } + + for _, ref := range middleLines { + if !strings.Contains(responseBody, ref) { + t.Errorf("GET %q: expected response to contain %q, found none", requestPath, ref) + } + } +} diff --git a/router/router.go b/router/router.go index 0c929cb21..29e214f81 100644 --- a/router/router.go +++ b/router/router.go @@ -6,14 +6,13 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" - - "gitlab.com/gitlab-org/gitaly/handler" ) func NewRouter() http.Handler { r := mux.NewRouter() - r.HandleFunc("/", handler.Home) + r.HandleFunc("/", Home) + r.HandleFunc("/projects/{id:[0-9]+}/git-http/info-refs/{service:(upload|receive)-pack}", GetInfoRefs) return handlers.LoggingHandler(os.Stdout, r) } |