From 9ed11af60e78711bd801f7008192a4594d25cc08 Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Sun, 23 Aug 2015 20:29:26 +0200
Subject: [PATCH] Put program initialization in a separate file

---
 README.md     |   2 +-
 githandler.go | 286 ++++++++++++++++++++++++++++++++++++++++++++++++++
 main.go       | 279 +-----------------------------------------------
 main_test.go  |   2 +-
 4 files changed, 293 insertions(+), 276 deletions(-)
 create mode 100644 githandler.go

diff --git a/README.md b/README.md
index 83ae98ec0298..2f2504198173 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ You can try out the Git server without authentication as follows:
 # Start a fake auth backend that allows everything/everybody
 go run support/say-yes.go &
 # Start gitlab-git-http-server
-go run main.go /path/to/git-repos
+go build && ./gitlab-git-http-server /path/to/git-repos
 ```
 
 Now if you have a Git repository in `/path/to/git-repos/my-repo.git`,
diff --git a/githandler.go b/githandler.go
new file mode 100644
index 000000000000..6dae7a5ee5ea
--- /dev/null
+++ b/githandler.go
@@ -0,0 +1,286 @@
+/*
+The gitHandler type implements http.Handler.
+
+All code for handling Git HTTP requests is in this file.
+*/
+
+package main
+
+import (
+	"compress/gzip"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"path"
+	"strings"
+	"syscall"
+)
+
+type gitHandler struct {
+	httpClient  *http.Client
+	repoRoot    string
+	authBackend string
+}
+
+type gitService struct {
+	method     string
+	suffix     string
+	handleFunc func(gitEnv, string, string, http.ResponseWriter, *http.Request)
+	rpc        string
+}
+
+type gitEnv struct {
+	GL_ID string
+}
+
+// Routing table
+var gitServices = [...]gitService{
+	gitService{"GET", "/info/refs", handleGetInfoRefs, ""},
+	gitService{"POST", "/git-upload-pack", handlePostRPC, "git-upload-pack"},
+	gitService{"POST", "/git-receive-pack", handlePostRPC, "git-receive-pack"},
+}
+
+func newGitHandler(repoRoot, authBackend string) *gitHandler {
+	return &gitHandler{&http.Client{}, repoRoot, authBackend}
+}
+
+func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	var env gitEnv
+	var g gitService
+
+	log.Print(r.Method, " ", r.URL)
+
+	// Look for a matching Git service
+	foundService := false
+	for _, g = range gitServices {
+		if r.Method == g.method && strings.HasSuffix(r.URL.Path, g.suffix) {
+			foundService = true
+			break
+		}
+	}
+	if !foundService {
+		// The protocol spec in git/Documentation/technical/http-protocol.txt
+		// says we must return 403 if no matching service is found.
+		http.Error(w, "Forbidden", 403)
+		return
+	}
+
+	// Ask the auth backend if the request is allowed, and what the
+	// user ID (GL_ID) is.
+	authResponse, err := h.doAuthRequest(r)
+	if err != nil {
+		fail500(w, err)
+		return
+	}
+	defer authResponse.Body.Close()
+
+	if authResponse.StatusCode != 200 {
+		// The Git request is not allowed by the backend. Maybe the
+		// client needs to send HTTP Basic credentials.  Forward the
+		// response from the auth backend to our client. This includes
+		// the 'WWW-Authentication' header that acts as a hint that
+		// Basic auth credentials are needed.
+		for k, v := range authResponse.Header {
+			w.Header()[k] = v
+		}
+		w.WriteHeader(authResponse.StatusCode)
+		io.Copy(w, authResponse.Body)
+		return
+	}
+
+	// The auth backend validated the client request and told us who
+	// the user is according to them (GL_ID). We must extract this
+	// information from the auth response body.
+	dec := json.NewDecoder(authResponse.Body)
+	if err := dec.Decode(&env); err != nil {
+		fail500(w, err)
+		return
+	}
+	// Don't hog a TCP connection in CLOSE_WAIT, we can already close it now
+	authResponse.Body.Close()
+
+	// About path traversal: the Go net/http HTTP server, or
+	// rather ServeMux, makes the following promise: "ServeMux
+	// also takes care of sanitizing the URL request path, redirecting
+	// any request containing . or .. elements to an equivalent
+	// .- and ..-free URL.". In other words, we may assume that
+	// r.URL.Path does not contain '/../', so there is no possibility
+	// of path traversal here.
+	repoPath := path.Join(h.repoRoot, strings.TrimSuffix(r.URL.Path, g.suffix))
+	if !looksLikeRepo(repoPath) {
+		http.Error(w, "Not Found", 404)
+		return
+	}
+
+	g.handleFunc(env, g.rpc, repoPath, w, r)
+}
+
+func looksLikeRepo(p string) bool {
+	// If /path/to/foo.git/objects exists then let's assume it is a valid Git
+	// repository.
+	if _, err := os.Stat(path.Join(p, "objects")); err != nil {
+		log.Print(err)
+		return false
+	}
+	return true
+}
+
+func (h *gitHandler) doAuthRequest(r *http.Request) (result *http.Response, err error) {
+	url := h.authBackend + r.URL.RequestURI()
+	authReq, err := http.NewRequest(r.Method, url, nil)
+	if err != nil {
+		return nil, err
+	}
+	// Forward all headers from our client to the auth backend. This includes
+	// HTTP Basic authentication credentials (the 'Authorization' header).
+	for k, v := range r.Header {
+		authReq.Header[k] = v
+	}
+	return h.httpClient.Do(authReq)
+}
+
+func handleGetInfoRefs(env gitEnv, _ string, path string, w http.ResponseWriter, r *http.Request) {
+	rpc := r.URL.Query().Get("service")
+	if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") {
+		// The 'dumb' Git HTTP protocol is not supported
+		http.Error(w, "Not Found", 404)
+		return
+	}
+
+	// Prepare our Git subprocess
+	cmd := gitCommand(env, "git", subCommand(rpc), "--stateless-rpc", "--advertise-refs", path)
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		fail500(w, err)
+		return
+	}
+	defer stdout.Close()
+	if err := cmd.Start(); err != nil {
+		fail500(w, err)
+		return
+	}
+	defer cleanUpProcessGroup(cmd) // Ensure brute force subprocess clean-up
+
+	// Start writing the response
+	w.Header().Add("Content-Type", fmt.Sprintf("application/x-%s-advertisement", rpc))
+	w.Header().Add("Cache-Control", "no-cache")
+	w.WriteHeader(200) // Don't bother with HTTP 500 from this point on, just panic
+	if err := pktLine(w, fmt.Sprintf("# service=%s\n", rpc)); err != nil {
+		panic(err)
+	}
+	if err := pktFlush(w); err != nil {
+		panic(err)
+	}
+	if _, err := io.Copy(w, stdout); err != nil {
+		panic(err)
+	}
+	if err := cmd.Wait(); err != nil {
+		panic(err)
+	}
+}
+
+func handlePostRPC(env gitEnv, rpc string, path string, w http.ResponseWriter, r *http.Request) {
+	var body io.Reader
+	var err error
+
+	// The client request body may have been gzipped.
+	if r.Header.Get("Content-Encoding") == "gzip" {
+		body, err = gzip.NewReader(r.Body)
+		if err != nil {
+			fail500(w, err)
+			return
+		}
+	} else {
+		body = r.Body
+	}
+
+	// Prepare our Git subprocess
+	cmd := gitCommand(env, "git", subCommand(rpc), "--stateless-rpc", path)
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		fail500(w, err)
+		return
+	}
+	defer stdout.Close()
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		fail500(w, err)
+		return
+	}
+	defer stdin.Close()
+	if err := cmd.Start(); err != nil {
+		fail500(w, err)
+		return
+	}
+	defer cleanUpProcessGroup(cmd) // Ensure brute force subprocess clean-up
+
+	// Write the client request body to Git's standard input
+	if _, err := io.Copy(stdin, body); err != nil {
+		fail500(w, err)
+		return
+	}
+	stdin.Close()
+
+	// Start writing the response
+	w.Header().Add("Content-Type", fmt.Sprintf("application/x-%s-result", rpc))
+	w.Header().Add("Cache-Control", "no-cache")
+	w.WriteHeader(200) // Don't bother with HTTP 500 from this point on, just panic
+	if _, err := io.Copy(w, stdout); err != nil {
+		panic(err)
+	}
+	if err := cmd.Wait(); err != nil {
+		panic(err)
+	}
+}
+
+func fail500(w http.ResponseWriter, err error) {
+	http.Error(w, "Internal server error", 500)
+	log.Print(err)
+}
+
+// Git subprocess helpers
+func subCommand(rpc string) string {
+	return strings.TrimPrefix(rpc, "git-")
+}
+
+func gitCommand(env gitEnv, 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("PATH=%s", os.Getenv("PATH")),
+		fmt.Sprintf("GL_ID=%s", env.GL_ID),
+	}
+	return cmd
+}
+
+func cleanUpProcessGroup(cmd *exec.Cmd) {
+	if cmd == nil {
+		return
+	}
+
+	process := cmd.Process
+	if process != nil && process.Pid > 0 {
+		// Send SIGKILL (kill -9) to the process group of cmd
+		syscall.Kill(-process.Pid, syscall.SIGKILL)
+	}
+
+	// reap our child process
+	cmd.Wait()
+}
+
+// Git HTTP line protocol functions
+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/main.go b/main.go
index b894f9cbe32e..e08da1afec8a 100644
--- a/main.go
+++ b/main.go
@@ -6,58 +6,30 @@ from Git clients that use the 'smart' Git HTTP protocol (git-upload-pack
 and git-receive-pack). It is intended to be deployed behind NGINX
 (for request routing and SSL termination) with access to a GitLab
 backend (for authentication and authorization) and local disk access
-to Git repositories managed by GitLab.
+to Git repositories managed by GitLab. In GitLab, this role was previously
+performed by gitlab-grack.
 
-This HTTP server replaces gitlab-grack.
+This file contains the main() function. Actual Git HTTP requests are handled by
+the gitHandler type, implemented in githandler.go.
 */
 package main
 
 import (
-	"compress/gzip"
-	"encoding/json"
 	"flag"
 	"fmt"
-	"io"
 	"log"
 	"net"
 	"net/http"
 	"os"
-	"os/exec"
-	"path"
-	"strings"
 	"syscall"
 )
 
-type gitHandler struct {
-	httpClient  *http.Client
-	repoRoot    string
-	authBackend string
-}
-
-type gitService struct {
-	method     string
-	suffix     string
-	handleFunc func(gitEnv, string, string, http.ResponseWriter, *http.Request)
-	rpc        string
-}
-
-type gitEnv struct {
-	GL_ID string
-}
-
 var Version string // Set at build time in the Makefile
 
-// Routing table
-var gitServices = [...]gitService{
-	gitService{"GET", "/info/refs", handleGetInfoRefs, ""},
-	gitService{"POST", "/git-upload-pack", handlePostRPC, "git-upload-pack"},
-	gitService{"POST", "/git-receive-pack", handlePostRPC, "git-receive-pack"},
-}
-
 func main() {
 	printVersion := flag.Bool("version", false, "Print version and exit")
 	listenAddr := flag.String("listenAddr", "localhost:8181", "Listen address for HTTP server")
-	listenNetwork := flag.String("listenNetwork", "tcp", "Listen 'network' (protocol)")
+	listenNetwork := flag.String("listenNetwork", "tcp", "Listen 'network' (tcp, tcp4, tcp6, unix)")
 	listenUmask := flag.Int("listenUmask", 022, "Umask for Unix socket, default: 022")
 	authBackend := flag.String("authBackend", "http://localhost:8080", "Authentication/authorization backend")
 	flag.Usage = func() {
@@ -97,244 +69,3 @@ func main() {
 	http.Handle("/", newGitHandler(repoRoot, *authBackend))
 	log.Fatal(http.Serve(listener, nil))
 }
-
-func newGitHandler(repoRoot, authBackend string) *gitHandler {
-	return &gitHandler{&http.Client{}, repoRoot, authBackend}
-}
-
-func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	var env gitEnv
-	var g gitService
-
-	log.Print(r.Method, " ", r.URL)
-
-	// Look for a matching Git service
-	foundService := false
-	for _, g = range gitServices {
-		if r.Method == g.method && strings.HasSuffix(r.URL.Path, g.suffix) {
-			foundService = true
-			break
-		}
-	}
-	if !foundService {
-		// The protocol spec in git/Documentation/technical/http-protocol.txt
-		// says we must return 403 if no matching service is found.
-		http.Error(w, "Forbidden", 403)
-		return
-	}
-
-	// Ask the auth backend if the request is allowed, and what the
-	// user ID (GL_ID) is.
-	authResponse, err := h.doAuthRequest(r)
-	if err != nil {
-		fail500(w, err)
-		return
-	}
-	defer authResponse.Body.Close()
-
-	if authResponse.StatusCode != 200 {
-		// The Git request is not allowed by the backend. Maybe the
-		// client needs to send HTTP Basic credentials.  Forward the
-		// response from the auth backend to our client. This includes
-		// the 'WWW-Authentication' header that acts as a hint that
-		// Basic auth credentials are needed.
-		for k, v := range authResponse.Header {
-			w.Header()[k] = v
-		}
-		w.WriteHeader(authResponse.StatusCode)
-		io.Copy(w, authResponse.Body)
-		return
-	}
-
-	// The auth backend validated the client request and told us who
-	// the user is according to them (GL_ID). We must extract this
-	// information from the auth response body.
-	dec := json.NewDecoder(authResponse.Body)
-	if err := dec.Decode(&env); err != nil {
-		fail500(w, err)
-		return
-	}
-	// Don't hog a TCP connection in CLOSE_WAIT, we can already close it now
-	authResponse.Body.Close()
-
-	// About path traversal: the Go net/http HTTP server, or
-	// rather ServeMux, makes the following promise: "ServeMux
-	// also takes care of sanitizing the URL request path, redirecting
-	// any request containing . or .. elements to an equivalent
-	// .- and ..-free URL.". In other words, we may assume that
-	// r.URL.Path does not contain '/../', so there is no possibility
-	// of path traversal here.
-	repoPath := path.Join(h.repoRoot, strings.TrimSuffix(r.URL.Path, g.suffix))
-	if !looksLikeRepo(repoPath) {
-		http.Error(w, "Not Found", 404)
-		return
-	}
-
-	g.handleFunc(env, g.rpc, repoPath, w, r)
-}
-
-func looksLikeRepo(p string) bool {
-	// If /path/to/foo.git/objects exists then let's assume it is a valid Git
-	// repository.
-	if _, err := os.Stat(path.Join(p, "objects")); err != nil {
-		log.Print(err)
-		return false
-	}
-	return true
-}
-
-func (h *gitHandler) doAuthRequest(r *http.Request) (result *http.Response, err error) {
-	url := h.authBackend + r.URL.RequestURI()
-	authReq, err := http.NewRequest(r.Method, url, nil)
-	if err != nil {
-		return nil, err
-	}
-	// Forward all headers from our client to the auth backend. This includes
-	// HTTP Basic authentication credentials (the 'Authorization' header).
-	for k, v := range r.Header {
-		authReq.Header[k] = v
-	}
-	return h.httpClient.Do(authReq)
-}
-
-func handleGetInfoRefs(env gitEnv, _ string, path string, w http.ResponseWriter, r *http.Request) {
-	rpc := r.URL.Query().Get("service")
-	if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") {
-		// The 'dumb' Git HTTP protocol is not supported
-		http.Error(w, "Not Found", 404)
-		return
-	}
-
-	// Prepare our Git subprocess
-	cmd := gitCommand(env, "git", subCommand(rpc), "--stateless-rpc", "--advertise-refs", path)
-	stdout, err := cmd.StdoutPipe()
-	if err != nil {
-		fail500(w, err)
-		return
-	}
-	defer stdout.Close()
-	if err := cmd.Start(); err != nil {
-		fail500(w, err)
-		return
-	}
-	defer cleanUpProcessGroup(cmd) // Ensure brute force subprocess clean-up
-
-	// Start writing the response
-	w.Header().Add("Content-Type", fmt.Sprintf("application/x-%s-advertisement", rpc))
-	w.Header().Add("Cache-Control", "no-cache")
-	w.WriteHeader(200) // Don't bother with HTTP 500 from this point on, just panic
-	if err := pktLine(w, fmt.Sprintf("# service=%s\n", rpc)); err != nil {
-		panic(err)
-	}
-	if err := pktFlush(w); err != nil {
-		panic(err)
-	}
-	if _, err := io.Copy(w, stdout); err != nil {
-		panic(err)
-	}
-	if err := cmd.Wait(); err != nil {
-		panic(err)
-	}
-}
-
-func handlePostRPC(env gitEnv, rpc string, path string, w http.ResponseWriter, r *http.Request) {
-	var body io.Reader
-	var err error
-
-	// The client request body may have been gzipped.
-	if r.Header.Get("Content-Encoding") == "gzip" {
-		body, err = gzip.NewReader(r.Body)
-		if err != nil {
-			fail500(w, err)
-			return
-		}
-	} else {
-		body = r.Body
-	}
-
-	// Prepare our Git subprocess
-	cmd := gitCommand(env, "git", subCommand(rpc), "--stateless-rpc", path)
-	stdout, err := cmd.StdoutPipe()
-	if err != nil {
-		fail500(w, err)
-		return
-	}
-	defer stdout.Close()
-	stdin, err := cmd.StdinPipe()
-	if err != nil {
-		fail500(w, err)
-		return
-	}
-	defer stdin.Close()
-	if err := cmd.Start(); err != nil {
-		fail500(w, err)
-		return
-	}
-	defer cleanUpProcessGroup(cmd) // Ensure brute force subprocess clean-up
-
-	// Write the client request body to Git's standard input
-	if _, err := io.Copy(stdin, body); err != nil {
-		fail500(w, err)
-		return
-	}
-	stdin.Close()
-
-	// Start writing the response
-	w.Header().Add("Content-Type", fmt.Sprintf("application/x-%s-result", rpc))
-	w.Header().Add("Cache-Control", "no-cache")
-	w.WriteHeader(200) // Don't bother with HTTP 500 from this point on, just panic
-	if _, err := io.Copy(w, stdout); err != nil {
-		panic(err)
-	}
-	if err := cmd.Wait(); err != nil {
-		panic(err)
-	}
-}
-
-func fail500(w http.ResponseWriter, err error) {
-	http.Error(w, "Internal server error", 500)
-	log.Print(err)
-}
-
-// Git subprocess helpers
-func subCommand(rpc string) string {
-	return strings.TrimPrefix(rpc, "git-")
-}
-
-func gitCommand(env gitEnv, 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("PATH=%s", os.Getenv("PATH")),
-		fmt.Sprintf("GL_ID=%s", env.GL_ID),
-	}
-	return cmd
-}
-
-func cleanUpProcessGroup(cmd *exec.Cmd) {
-	if cmd == nil {
-		return
-	}
-
-	process := cmd.Process
-	if process != nil && process.Pid > 0 {
-		// Send SIGKILL (kill -9) to the process group of cmd
-		syscall.Kill(-process.Pid, syscall.SIGKILL)
-	}
-
-	// reap our child process
-	cmd.Wait()
-}
-
-// Git HTTP line protocol functions
-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/main_test.go b/main_test.go
index 1749cafa31b0..4e64671b8514 100644
--- a/main_test.go
+++ b/main_test.go
@@ -117,7 +117,7 @@ func testAuthServer(code int, body string) *httptest.Server {
 }
 
 func startServerOrFail(t *testing.T, ts *httptest.Server) *exec.Cmd {
-	cmd := exec.Command("go", "run", "main.go", fmt.Sprintf("-authBackend=%s", ts.URL), fmt.Sprintf("-listenAddr=%s", servAddr), testRepoRoot)
+	cmd := exec.Command("go", "run", "main.go", "githandler.go", fmt.Sprintf("-authBackend=%s", ts.URL), fmt.Sprintf("-listenAddr=%s", servAddr), testRepoRoot)
 	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
-- 
GitLab