From 3ec457b4fedd36ed443326638d4d895edb66065a Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Tue, 2 Feb 2016 15:55:47 +0100
Subject: [PATCH 01/10] First version of "git archive" via headers

---
 internal/api/api.go           |  9 ------
 internal/git/archive.go       | 31 ++++++++++++-------
 internal/git/blob.go          | 21 +++++--------
 internal/senddata/sendfile.go | 20 +++++++++----
 internal/upstream/routes.go   | 14 ---------
 main_test.go                  | 56 +++++++++++++++++++++--------------
 6 files changed, 76 insertions(+), 75 deletions(-)

diff --git a/internal/api/api.go b/internal/api/api.go
index b23ea7e..02eb01c 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -38,15 +38,6 @@ type Response struct {
 	// RepoPath is the full path on disk to the Git repository the request is
 	// about
 	RepoPath string
-	// ArchivePath is the full path where we should find/create a cached copy
-	// of a requested archive
-	ArchivePath string
-	// ArchivePrefix is used to put extracted archive contents in a
-	// subdirectory
-	ArchivePrefix string
-	// CommitId is used do prevent race conditions between the 'time of check'
-	// in the GitLab Rails app and the 'time of use' in gitlab-workhorse.
-	CommitId string
 	// StoreLFSPath is provided by the GitLab Rails application
 	// to mark where the tmp file should be placed
 	StoreLFSPath string
diff --git a/internal/git/archive.go b/internal/git/archive.go
index 854c887..c8b232b 100644
--- a/internal/git/archive.go
+++ b/internal/git/archive.go
@@ -5,7 +5,6 @@ In this file we handle 'git archive' downloads
 package git
 
 import (
-	"../api"
 	"../helper"
 	"fmt"
 	"io"
@@ -20,11 +19,20 @@ import (
 	"time"
 )
 
-func GetArchive(a *api.API) http.Handler {
-	return repoPreAuthorizeHandler(a, handleGetArchive)
-}
+const SendArchivePrefix = "git-archive:"
+
+func SendArchive(w http.ResponseWriter, r *http.Request, sendData string) {
+	var params struct {
+		RepoPath      string
+		ArchivePath   string
+		ArchivePrefix string
+		CommitId      string
+	}
+	if err := unpackSendData(&params, sendData, SendArchivePrefix); err != nil {
+		helper.Fail500(w, fmt.Errorf("SendArchive: unpack sendData: %v", err))
+		return
+	}
 
-func handleGetArchive(w http.ResponseWriter, r *http.Request, a *api.Response) {
 	var format string
 	urlPath := r.URL.Path
 	switch filepath.Base(urlPath) {
@@ -41,11 +49,11 @@ func handleGetArchive(w http.ResponseWriter, r *http.Request, a *api.Response) {
 		return
 	}
 
-	archiveFilename := path.Base(a.ArchivePath)
+	archiveFilename := path.Base(params.ArchivePath)
 
-	if cachedArchive, err := os.Open(a.ArchivePath); err == nil {
+	if cachedArchive, err := os.Open(params.ArchivePath); err == nil {
 		defer cachedArchive.Close()
-		log.Printf("Serving cached file %q", a.ArchivePath)
+		log.Printf("Serving cached file %q", params.ArchivePath)
 		setArchiveHeaders(w, format, archiveFilename)
 		// Even if somebody deleted the cachedArchive from disk since we opened
 		// the file, Unix file semantics guarantee we can still read from the
@@ -58,7 +66,7 @@ func handleGetArchive(w http.ResponseWriter, r *http.Request, a *api.Response) {
 	// safe. We create the tempfile in the same directory as the final cached
 	// archive we want to create so that we can use an atomic link(2) operation
 	// to finalize the cached archive.
-	tempFile, err := prepareArchiveTempfile(path.Dir(a.ArchivePath), archiveFilename)
+	tempFile, err := prepareArchiveTempfile(path.Dir(params.ArchivePath), archiveFilename)
 	if err != nil {
 		helper.Fail500(w, fmt.Errorf("handleGetArchive: create tempfile: %v", err))
 		return
@@ -68,7 +76,7 @@ func handleGetArchive(w http.ResponseWriter, r *http.Request, a *api.Response) {
 
 	compressCmd, archiveFormat := parseArchiveFormat(format)
 
-	archiveCmd := gitCommand("", "git", "--git-dir="+a.RepoPath, "archive", "--format="+archiveFormat, "--prefix="+a.ArchivePrefix+"/", a.CommitId)
+	archiveCmd := gitCommand("", "git", "--git-dir="+params.RepoPath, "archive", "--format="+archiveFormat, "--prefix="+params.ArchivePrefix+"/", params.CommitId)
 	archiveStdout, err := archiveCmd.StdoutPipe()
 	if err != nil {
 		helper.Fail500(w, fmt.Errorf("handleGetArchive: archive stdout: %v", err))
@@ -125,13 +133,14 @@ func handleGetArchive(w http.ResponseWriter, r *http.Request, a *api.Response) {
 		}
 	}
 
-	if err := finalizeCachedArchive(tempFile, a.ArchivePath); err != nil {
+	if err := finalizeCachedArchive(tempFile, params.ArchivePath); err != nil {
 		helper.LogError(fmt.Errorf("handleGetArchive: finalize cached archive: %v", err))
 		return
 	}
 }
 
 func setArchiveHeaders(w http.ResponseWriter, format string, archiveFilename string) {
+	w.Header().Del("Content-Length")
 	w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, archiveFilename))
 	if format == "zip" {
 		w.Header().Add("Content-Type", "application/zip")
diff --git a/internal/git/blob.go b/internal/git/blob.go
index aa1b7d6..b6e1b11 100644
--- a/internal/git/blob.go
+++ b/internal/git/blob.go
@@ -11,19 +11,15 @@ import (
 	"strings"
 )
 
-type blobParams struct {
-	RepoPath string
-	BlobId   string
-}
-
 const SendBlobPrefix = "git-blob:"
 
 func SendBlob(w http.ResponseWriter, r *http.Request, sendData string) {
-	params, err := unpackSendData(sendData)
-	if err != nil {
+	var params struct{ RepoPath, BlobId string }
+	if err := unpackSendData(&params, sendData, SendBlobPrefix); err != nil {
 		helper.Fail500(w, fmt.Errorf("SendBlob: unpack sendData: %v", err))
 		return
 	}
+
 	log.Printf("SendBlob: sending %q for %q", params.BlobId, r.URL.Path)
 
 	gitShowCmd := gitCommand("", "git", "--git-dir="+params.RepoPath, "cat-file", "blob", params.BlobId)
@@ -48,14 +44,13 @@ func SendBlob(w http.ResponseWriter, r *http.Request, sendData string) {
 	}
 }
 
-func unpackSendData(sendData string) (*blobParams, error) {
-	jsonBytes, err := base64.URLEncoding.DecodeString(strings.TrimPrefix(sendData, SendBlobPrefix))
+func unpackSendData(result interface{}, sendData string, prefix string) error {
+	jsonBytes, err := base64.URLEncoding.DecodeString(strings.TrimPrefix(sendData, prefix))
 	if err != nil {
-		return nil, err
+		return err
 	}
-	result := &blobParams{}
 	if err := json.Unmarshal([]byte(jsonBytes), result); err != nil {
-		return nil, err
+		return err
 	}
-	return result, nil
+	return nil
 }
diff --git a/internal/senddata/sendfile.go b/internal/senddata/sendfile.go
index 7d851d6..8e087fd 100644
--- a/internal/senddata/sendfile.go
+++ b/internal/senddata/sendfile.go
@@ -70,11 +70,21 @@ func (s *sendFileResponseWriter) WriteHeader(status int) {
 		sendFileFromDisk(s.rw, s.req, file)
 		return
 	}
-	if sendData := s.Header().Get(sendDataResponseHeader); strings.HasPrefix(sendData, git.SendBlobPrefix) {
-		s.Header().Del(sendDataResponseHeader)
-		s.hijacked = true
-		git.SendBlob(s.rw, s.req, sendData)
-		return
+
+	sendData := s.Header().Get(sendDataResponseHeader)
+	s.Header().Del(sendDataResponseHeader)
+	for _, handler := range []struct {
+		prefix string
+		f      func(http.ResponseWriter, *http.Request, string)
+	}{
+		{git.SendBlobPrefix, git.SendBlob},
+		{git.SendArchivePrefix, git.SendArchive},
+	} {
+		if strings.HasPrefix(sendData, handler.prefix) {
+			s.hijacked = true
+			handler.f(s.rw, s.req, sendData)
+			return
+		}
 	}
 
 	s.rw.WriteHeader(s.status)
diff --git a/internal/upstream/routes.go b/internal/upstream/routes.go
index 0fe09d4..4462a5c 100644
--- a/internal/upstream/routes.go
+++ b/internal/upstream/routes.go
@@ -50,20 +50,6 @@ func (u *Upstream) configureRoutes() {
 		route{"POST", regexp.MustCompile(gitProjectPattern + `git-receive-pack\z`), contentEncodingHandler(git.PostRPC(api))},
 		route{"PUT", regexp.MustCompile(gitProjectPattern + `gitlab-lfs/objects/([0-9a-f]{64})/([0-9]+)\z`), lfs.PutStore(api, proxy)},
 
-		// Repository Archive
-		route{"GET", regexp.MustCompile(projectPattern + `repository/archive\z`), git.GetArchive(api)},
-		route{"GET", regexp.MustCompile(projectPattern + `repository/archive.zip\z`), git.GetArchive(api)},
-		route{"GET", regexp.MustCompile(projectPattern + `repository/archive.tar\z`), git.GetArchive(api)},
-		route{"GET", regexp.MustCompile(projectPattern + `repository/archive.tar.gz\z`), git.GetArchive(api)},
-		route{"GET", regexp.MustCompile(projectPattern + `repository/archive.tar.bz2\z`), git.GetArchive(api)},
-
-		// Repository Archive API
-		route{"GET", regexp.MustCompile(projectsAPIPattern + `repository/archive\z`), git.GetArchive(api)},
-		route{"GET", regexp.MustCompile(projectsAPIPattern + `repository/archive.zip\z`), git.GetArchive(api)},
-		route{"GET", regexp.MustCompile(projectsAPIPattern + `repository/archive.tar\z`), git.GetArchive(api)},
-		route{"GET", regexp.MustCompile(projectsAPIPattern + `repository/archive.tar.gz\z`), git.GetArchive(api)},
-		route{"GET", regexp.MustCompile(projectsAPIPattern + `repository/archive.tar.bz2\z`), git.GetArchive(api)},
-
 		// CI Artifacts
 		route{"GET", regexp.MustCompile(projectPattern + `builds/[0-9]+/artifacts/file/`), contentEncodingHandler(artifacts.DownloadArtifact(api))},
 		route{"POST", regexp.MustCompile(ciAPIPattern + `v1/builds/[0-9]+/artifacts\z`), contentEncodingHandler(artifacts.UploadArtifacts(api, proxy))},
diff --git a/main_test.go b/main_test.go
index a4fed1c..19ec0b3 100644
--- a/main_test.go
+++ b/main_test.go
@@ -115,7 +115,7 @@ func TestAllowedDownloadZip(t *testing.T) {
 
 	// Prepare test server and backend
 	archiveName := "foobar.zip"
-	ts := testAuthServer(nil, 200, archiveOkBody(t, archiveName))
+	ts := archiveOKServer(t, archiveName)
 	defer ts.Close()
 	ws := startWorkhorseServer(ts.URL)
 	defer ws.Close()
@@ -134,7 +134,7 @@ func TestAllowedDownloadTar(t *testing.T) {
 
 	// Prepare test server and backend
 	archiveName := "foobar.tar"
-	ts := testAuthServer(nil, 200, archiveOkBody(t, archiveName))
+	ts := archiveOKServer(t, archiveName)
 	defer ts.Close()
 	ws := startWorkhorseServer(ts.URL)
 	defer ws.Close()
@@ -153,7 +153,7 @@ func TestAllowedDownloadTarGz(t *testing.T) {
 
 	// Prepare test server and backend
 	archiveName := "foobar.tar.gz"
-	ts := testAuthServer(nil, 200, archiveOkBody(t, archiveName))
+	ts := archiveOKServer(t, archiveName)
 	defer ts.Close()
 	ws := startWorkhorseServer(ts.URL)
 	defer ws.Close()
@@ -172,7 +172,7 @@ func TestAllowedDownloadTarBz2(t *testing.T) {
 
 	// Prepare test server and backend
 	archiveName := "foobar.tar.bz2"
-	ts := testAuthServer(nil, 200, archiveOkBody(t, archiveName))
+	ts := archiveOKServer(t, archiveName)
 	defer ts.Close()
 	ws := startWorkhorseServer(ts.URL)
 	defer ws.Close()
@@ -191,7 +191,7 @@ func TestAllowedApiDownloadZip(t *testing.T) {
 
 	// Prepare test server and backend
 	archiveName := "foobar.zip"
-	ts := testAuthServer(nil, 200, archiveOkBody(t, archiveName))
+	ts := archiveOKServer(t, archiveName)
 	defer ts.Close()
 	ws := startWorkhorseServer(ts.URL)
 	defer ws.Close()
@@ -210,7 +210,7 @@ func TestAllowedApiDownloadZipWithSlash(t *testing.T) {
 
 	// Prepare test server and backend
 	archiveName := "foobar.zip"
-	ts := testAuthServer(nil, 200, archiveOkBody(t, archiveName))
+	ts := archiveOKServer(t, archiveName)
 	defer ts.Close()
 	ws := startWorkhorseServer(ts.URL)
 	defer ws.Close()
@@ -233,7 +233,7 @@ func TestDownloadCacheHit(t *testing.T) {
 
 	// Prepare test server and backend
 	archiveName := "foobar.zip"
-	ts := testAuthServer(nil, 200, archiveOkBody(t, archiveName))
+	ts := archiveOKServer(t, archiveName)
 	defer ts.Close()
 	ws := startWorkhorseServer(ts.URL)
 	defer ws.Close()
@@ -264,7 +264,7 @@ func TestDownloadCacheCreate(t *testing.T) {
 
 	// Prepare test server and backend
 	archiveName := "foobar.zip"
-	ts := testAuthServer(nil, 200, archiveOkBody(t, archiveName))
+	ts := archiveOKServer(t, archiveName)
 	defer ts.Close()
 	ws := startWorkhorseServer(ts.URL)
 	defer ws.Close()
@@ -659,6 +659,31 @@ func testAuthServer(url *regexp.Regexp, code int, body interface{}) *httptest.Se
 	})
 }
 
+func archiveOKServer(t *testing.T, archiveName string) *httptest.Server {
+	return testhelper.TestServerWithHandler(regexp.MustCompile("."), func(w http.ResponseWriter, r *http.Request) {
+		cwd, err := os.Getwd()
+		if err != nil {
+			t.Fatal(err)
+		}
+		archivePath := path.Join(cwd, cacheDir, archiveName)
+
+		params := struct{ RepoPath, ArchivePath, CommitId, ArchivePrefix string }{
+			repoPath(t),
+			archivePath,
+			"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+			"foobar123",
+		}
+		jsonData, err := json.Marshal(params)
+		if err != nil {
+			t.Fatal(err)
+		}
+		encodedJSON := base64.StdEncoding.EncodeToString(jsonData)
+		w.Header().Set("Gitlab-Workhorse-Send-Data", "git-archive:"+encodedJSON)
+		// Prevent the Go HTTP server from setting the Content-Length to 0.
+		w.Header().Set("Transfer-Encoding", "chunked")
+	})
+}
+
 func startWorkhorseServer(authBackend string) *httptest.Server {
 	u := upstream.NewUpstream(
 		helper.URLMustParse(authBackend),
@@ -686,21 +711,6 @@ func gitOkBody(t *testing.T) interface{} {
 	}
 }
 
-func archiveOkBody(t *testing.T, archiveName string) interface{} {
-	cwd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
-	archivePath := path.Join(cwd, cacheDir, archiveName)
-
-	return &api.Response{
-		RepoPath:      repoPath(t),
-		ArchivePath:   archivePath,
-		CommitId:      "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
-		ArchivePrefix: "foobar123",
-	}
-}
-
 func repoPath(t *testing.T) string {
 	cwd, err := os.Getwd()
 	if err != nil {
-- 
GitLab


From ec168f4b14b638272f08334adf2ee94670daa0cf Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Tue, 2 Feb 2016 17:01:23 +0100
Subject: [PATCH 02/10] Use interfaces

---
 internal/git/archive.go                   |  9 ++++---
 internal/git/blob.go                      | 23 +++++-----------
 internal/{senddata => inject}/sendfile.go | 26 +++++++-----------
 internal/proxy/proxy.go                   |  4 +--
 internal/senddata/handler.go              | 32 +++++++++++++++++++++++
 5 files changed, 56 insertions(+), 38 deletions(-)
 rename internal/{senddata => inject}/sendfile.go (77%)
 create mode 100644 internal/senddata/handler.go

diff --git a/internal/git/archive.go b/internal/git/archive.go
index c8b232b..14c7540 100644
--- a/internal/git/archive.go
+++ b/internal/git/archive.go
@@ -6,6 +6,7 @@ package git
 
 import (
 	"../helper"
+	"../senddata"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -19,16 +20,18 @@ import (
 	"time"
 )
 
-const SendArchivePrefix = "git-archive:"
+type archive struct{ senddata.Prefix }
 
-func SendArchive(w http.ResponseWriter, r *http.Request, sendData string) {
+var SendArchive = &archive{"git-archive:"}
+
+func (a *archive) Handle(w http.ResponseWriter, r *http.Request, sendData string) {
 	var params struct {
 		RepoPath      string
 		ArchivePath   string
 		ArchivePrefix string
 		CommitId      string
 	}
-	if err := unpackSendData(&params, sendData, SendArchivePrefix); err != nil {
+	if err := a.Unpack(&params, sendData); err != nil {
 		helper.Fail500(w, fmt.Errorf("SendArchive: unpack sendData: %v", err))
 		return
 	}
diff --git a/internal/git/blob.go b/internal/git/blob.go
index b6e1b11..1d63a82 100644
--- a/internal/git/blob.go
+++ b/internal/git/blob.go
@@ -2,20 +2,20 @@ package git
 
 import (
 	"../helper"
-	"encoding/base64"
-	"encoding/json"
+	"../senddata"
 	"fmt"
 	"io"
 	"log"
 	"net/http"
-	"strings"
 )
 
-const SendBlobPrefix = "git-blob:"
+type blob struct{ senddata.Prefix }
 
-func SendBlob(w http.ResponseWriter, r *http.Request, sendData string) {
+var SendBlob = &blob{"git-blob:"}
+
+func (b *blob) Handle(w http.ResponseWriter, r *http.Request, sendData string) {
 	var params struct{ RepoPath, BlobId string }
-	if err := unpackSendData(&params, sendData, SendBlobPrefix); err != nil {
+	if err := b.Unpack(&params, sendData); err != nil {
 		helper.Fail500(w, fmt.Errorf("SendBlob: unpack sendData: %v", err))
 		return
 	}
@@ -43,14 +43,3 @@ func SendBlob(w http.ResponseWriter, r *http.Request, sendData string) {
 		return
 	}
 }
-
-func unpackSendData(result interface{}, sendData string, prefix string) error {
-	jsonBytes, err := base64.URLEncoding.DecodeString(strings.TrimPrefix(sendData, prefix))
-	if err != nil {
-		return err
-	}
-	if err := json.Unmarshal([]byte(jsonBytes), result); err != nil {
-		return err
-	}
-	return nil
-}
diff --git a/internal/senddata/sendfile.go b/internal/inject/sendfile.go
similarity index 77%
rename from internal/senddata/sendfile.go
rename to internal/inject/sendfile.go
index 8e087fd..df9cb0c 100644
--- a/internal/senddata/sendfile.go
+++ b/internal/inject/sendfile.go
@@ -4,20 +4,17 @@ via the X-Sendfile mechanism. All that is needed in the Rails code is the
 'send_file' method.
 */
 
-package senddata
+package inject
 
 import (
 	"../git"
 	"../helper"
+	"../senddata"
 	"log"
 	"net/http"
-	"strings"
 )
 
-const (
-	sendDataResponseHeader = "Gitlab-Workhorse-Send-Data"
-	sendFileResponseHeader = "X-Sendfile"
-)
+const sendFileResponseHeader = "X-Sendfile"
 
 type sendFileResponseWriter struct {
 	rw       http.ResponseWriter
@@ -71,18 +68,15 @@ func (s *sendFileResponseWriter) WriteHeader(status int) {
 		return
 	}
 
-	sendData := s.Header().Get(sendDataResponseHeader)
-	s.Header().Del(sendDataResponseHeader)
-	for _, handler := range []struct {
-		prefix string
-		f      func(http.ResponseWriter, *http.Request, string)
-	}{
-		{git.SendBlobPrefix, git.SendBlob},
-		{git.SendArchivePrefix, git.SendArchive},
+	header := s.Header().Get(senddata.Header)
+	s.Header().Del(senddata.Header)
+	for _, handler := range []senddata.Handler{
+		git.SendBlob,
+		git.SendArchive,
 	} {
-		if strings.HasPrefix(sendData, handler.prefix) {
+		if handler.Match(header) {
 			s.hijacked = true
-			handler.f(s.rw, s.req, sendData)
+			handler.Handle(s.rw, s.req, header)
 			return
 		}
 	}
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go
index 39422f9..e0511f5 100644
--- a/internal/proxy/proxy.go
+++ b/internal/proxy/proxy.go
@@ -3,7 +3,7 @@ package proxy
 import (
 	"../badgateway"
 	"../helper"
-	"../senddata"
+	"../inject"
 	"net/http"
 	"net/http/httputil"
 	"net/url"
@@ -34,7 +34,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	// Set Workhorse version
 	req.Header.Set("Gitlab-Workhorse", p.Version)
-	rw := senddata.NewSendFileResponseWriter(w, &req)
+	rw := inject.NewSendFileResponseWriter(w, &req)
 	defer rw.Flush()
 
 	p.reverseProxy.ServeHTTP(&rw, &req)
diff --git a/internal/senddata/handler.go b/internal/senddata/handler.go
new file mode 100644
index 0000000..d3c5080
--- /dev/null
+++ b/internal/senddata/handler.go
@@ -0,0 +1,32 @@
+package senddata
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"net/http"
+	"strings"
+)
+
+type Handler interface {
+	Match(string) bool
+	Handle(http.ResponseWriter, *http.Request, string)
+}
+
+type Prefix string
+
+const Header = "Gitlab-Workhorse-Send-Data"
+
+func (p Prefix) Match(s string) bool {
+	return strings.HasPrefix(s, string(p))
+}
+
+func (p Prefix) Unpack(result interface{}, sendData string) error {
+	jsonBytes, err := base64.URLEncoding.DecodeString(strings.TrimPrefix(sendData, string(p)))
+	if err != nil {
+		return err
+	}
+	if err := json.Unmarshal([]byte(jsonBytes), result); err != nil {
+		return err
+	}
+	return nil
+}
-- 
GitLab


From 2788c575786ac0dbfe44e0f154518d31919bcb5e Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Tue, 2 Feb 2016 17:37:10 +0100
Subject: [PATCH 03/10] Skip empty send-data header quicker

---
 internal/inject/sendfile.go | 21 +++++++++++----------
 1 file changed, 11 insertions(+), 10 deletions(-)

diff --git a/internal/inject/sendfile.go b/internal/inject/sendfile.go
index df9cb0c..65af336 100644
--- a/internal/inject/sendfile.go
+++ b/internal/inject/sendfile.go
@@ -68,16 +68,17 @@ func (s *sendFileResponseWriter) WriteHeader(status int) {
 		return
 	}
 
-	header := s.Header().Get(senddata.Header)
-	s.Header().Del(senddata.Header)
-	for _, handler := range []senddata.Handler{
-		git.SendBlob,
-		git.SendArchive,
-	} {
-		if handler.Match(header) {
-			s.hijacked = true
-			handler.Handle(s.rw, s.req, header)
-			return
+	if header := s.Header().Get(senddata.Header); header != "" {
+		s.Header().Del(senddata.Header)
+		for _, handler := range []senddata.Handler{
+			git.SendBlob,
+			git.SendArchive,
+		} {
+			if handler.Match(header) {
+				s.hijacked = true
+				handler.Handle(s.rw, s.req, header)
+				return
+			}
 		}
 	}
 
-- 
GitLab


From f819a3c5d71b2fef872c2418e2bc70945c7d3ebc Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Thu, 11 Feb 2016 16:56:06 +0100
Subject: [PATCH 04/10] Rename Handler to Injecter

---
 internal/git/archive.go                       | 2 +-
 internal/git/blob.go                          | 2 +-
 internal/inject/sendfile.go                   | 4 ++--
 internal/senddata/{handler.go => injecter.go} | 4 ++--
 4 files changed, 6 insertions(+), 6 deletions(-)
 rename internal/senddata/{handler.go => injecter.go} (87%)

diff --git a/internal/git/archive.go b/internal/git/archive.go
index 14c7540..e94f5cd 100644
--- a/internal/git/archive.go
+++ b/internal/git/archive.go
@@ -24,7 +24,7 @@ type archive struct{ senddata.Prefix }
 
 var SendArchive = &archive{"git-archive:"}
 
-func (a *archive) Handle(w http.ResponseWriter, r *http.Request, sendData string) {
+func (a *archive) Inject(w http.ResponseWriter, r *http.Request, sendData string) {
 	var params struct {
 		RepoPath      string
 		ArchivePath   string
diff --git a/internal/git/blob.go b/internal/git/blob.go
index 1d63a82..fbaac2d 100644
--- a/internal/git/blob.go
+++ b/internal/git/blob.go
@@ -13,7 +13,7 @@ type blob struct{ senddata.Prefix }
 
 var SendBlob = &blob{"git-blob:"}
 
-func (b *blob) Handle(w http.ResponseWriter, r *http.Request, sendData string) {
+func (b *blob) Inject(w http.ResponseWriter, r *http.Request, sendData string) {
 	var params struct{ RepoPath, BlobId string }
 	if err := b.Unpack(&params, sendData); err != nil {
 		helper.Fail500(w, fmt.Errorf("SendBlob: unpack sendData: %v", err))
diff --git a/internal/inject/sendfile.go b/internal/inject/sendfile.go
index 65af336..374b3f1 100644
--- a/internal/inject/sendfile.go
+++ b/internal/inject/sendfile.go
@@ -70,13 +70,13 @@ func (s *sendFileResponseWriter) WriteHeader(status int) {
 
 	if header := s.Header().Get(senddata.Header); header != "" {
 		s.Header().Del(senddata.Header)
-		for _, handler := range []senddata.Handler{
+		for _, handler := range []senddata.Injecter{
 			git.SendBlob,
 			git.SendArchive,
 		} {
 			if handler.Match(header) {
 				s.hijacked = true
-				handler.Handle(s.rw, s.req, header)
+				handler.Inject(s.rw, s.req, header)
 				return
 			}
 		}
diff --git a/internal/senddata/handler.go b/internal/senddata/injecter.go
similarity index 87%
rename from internal/senddata/handler.go
rename to internal/senddata/injecter.go
index d3c5080..0b6e635 100644
--- a/internal/senddata/handler.go
+++ b/internal/senddata/injecter.go
@@ -7,9 +7,9 @@ import (
 	"strings"
 )
 
-type Handler interface {
+type Injecter interface {
 	Match(string) bool
-	Handle(http.ResponseWriter, *http.Request, string)
+	Inject(http.ResponseWriter, *http.Request, string)
 }
 
 type Prefix string
-- 
GitLab


From 46680f427f1020156fed5291672526c492b06011 Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Thu, 11 Feb 2016 17:32:28 +0100
Subject: [PATCH 05/10] Configure sendfile and injecters in routes.go

---
 internal/lfs/lfs.go                       |  5 +-
 internal/proxy/proxy.go                   |  5 +-
 internal/senddata/injecter.go             |  2 +-
 internal/senddata/senddata.go             | 69 +++++++++++++++++++++++
 internal/{inject => sendfile}/sendfile.go | 37 ++++--------
 internal/upstream/routes.go               | 14 +++--
 6 files changed, 95 insertions(+), 37 deletions(-)
 create mode 100644 internal/senddata/senddata.go
 rename internal/{inject => sendfile}/sendfile.go (71%)

diff --git a/internal/lfs/lfs.go b/internal/lfs/lfs.go
index 6d78cb6..2718d0a 100644
--- a/internal/lfs/lfs.go
+++ b/internal/lfs/lfs.go
@@ -7,7 +7,6 @@ package lfs
 import (
 	"../api"
 	"../helper"
-	"../proxy"
 	"bytes"
 	"crypto/sha256"
 	"encoding/hex"
@@ -20,8 +19,8 @@ import (
 	"path/filepath"
 )
 
-func PutStore(a *api.API, p *proxy.Proxy) http.Handler {
-	return lfsAuthorizeHandler(a, handleStoreLfsObject(p))
+func PutStore(a *api.API, h http.Handler) http.Handler {
+	return lfsAuthorizeHandler(a, handleStoreLfsObject(h))
 }
 
 func lfsAuthorizeHandler(myAPI *api.API, handleFunc api.HandleFunc) http.Handler {
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go
index e0511f5..ec7ea66 100644
--- a/internal/proxy/proxy.go
+++ b/internal/proxy/proxy.go
@@ -3,7 +3,6 @@ package proxy
 import (
 	"../badgateway"
 	"../helper"
-	"../inject"
 	"net/http"
 	"net/http/httputil"
 	"net/url"
@@ -34,8 +33,6 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	// Set Workhorse version
 	req.Header.Set("Gitlab-Workhorse", p.Version)
-	rw := inject.NewSendFileResponseWriter(w, &req)
-	defer rw.Flush()
 
-	p.reverseProxy.ServeHTTP(&rw, &req)
+	p.reverseProxy.ServeHTTP(w, &req)
 }
diff --git a/internal/senddata/injecter.go b/internal/senddata/injecter.go
index 0b6e635..72d8039 100644
--- a/internal/senddata/injecter.go
+++ b/internal/senddata/injecter.go
@@ -14,7 +14,7 @@ type Injecter interface {
 
 type Prefix string
 
-const Header = "Gitlab-Workhorse-Send-Data"
+const HeaderKey = "Gitlab-Workhorse-Send-Data"
 
 func (p Prefix) Match(s string) bool {
 	return strings.HasPrefix(s, string(p))
diff --git a/internal/senddata/senddata.go b/internal/senddata/senddata.go
new file mode 100644
index 0000000..42f4091
--- /dev/null
+++ b/internal/senddata/senddata.go
@@ -0,0 +1,69 @@
+package senddata
+
+import (
+	"net/http"
+)
+
+type sendDataResponseWriter struct {
+	rw        http.ResponseWriter
+	status    int
+	hijacked  bool
+	req       *http.Request
+	injecters []Injecter
+}
+
+func SendData(h http.Handler, injecters ...Injecter) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		s := sendDataResponseWriter{
+			rw:        w,
+			req:       r,
+			injecters: injecters,
+		}
+		defer s.Flush()
+		h.ServeHTTP(&s, r)
+	})
+}
+
+func (s *sendDataResponseWriter) Header() http.Header {
+	return s.rw.Header()
+}
+
+func (s *sendDataResponseWriter) Write(data []byte) (n int, err error) {
+	if s.status == 0 {
+		s.WriteHeader(http.StatusOK)
+	}
+	if s.hijacked {
+		return
+	}
+	return s.rw.Write(data)
+}
+
+func (s *sendDataResponseWriter) WriteHeader(status int) {
+	if s.status != 0 {
+		return
+	}
+
+	s.status = status
+	if s.status != http.StatusOK {
+		s.rw.WriteHeader(s.status)
+		return
+	}
+
+	if header := s.Header().Get(HeaderKey); header != "" {
+		s.Header().Del(HeaderKey)
+		for _, injecter := range s.injecters {
+			if injecter.Match(header) {
+				s.hijacked = true
+				injecter.Inject(s.rw, s.req, header)
+				return
+			}
+		}
+	}
+
+	s.rw.WriteHeader(s.status)
+	return
+}
+
+func (s *sendDataResponseWriter) Flush() {
+	s.WriteHeader(http.StatusOK)
+}
diff --git a/internal/inject/sendfile.go b/internal/sendfile/sendfile.go
similarity index 71%
rename from internal/inject/sendfile.go
rename to internal/sendfile/sendfile.go
index 374b3f1..bfa54e0 100644
--- a/internal/inject/sendfile.go
+++ b/internal/sendfile/sendfile.go
@@ -4,12 +4,10 @@ via the X-Sendfile mechanism. All that is needed in the Rails code is the
 'send_file' method.
 */
 
-package inject
+package sendfile
 
 import (
-	"../git"
 	"../helper"
-	"../senddata"
 	"log"
 	"net/http"
 )
@@ -23,14 +21,17 @@ type sendFileResponseWriter struct {
 	req      *http.Request
 }
 
-func NewSendFileResponseWriter(rw http.ResponseWriter, req *http.Request) sendFileResponseWriter {
-	s := sendFileResponseWriter{
-		rw:  rw,
-		req: req,
-	}
-	// Advertise to upstream (Rails) that we support X-Sendfile
-	req.Header.Set("X-Sendfile-Type", "X-Sendfile")
-	return s
+func SendFile(h http.Handler) http.Handler {
+	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		s := &sendFileResponseWriter{
+			rw:  rw,
+			req: req,
+		}
+		// Advertise to upstream (Rails) that we support X-Sendfile
+		req.Header.Set("X-Sendfile-Type", "X-Sendfile")
+		defer s.Flush()
+		h.ServeHTTP(s, req)
+	})
 }
 
 func (s *sendFileResponseWriter) Header() http.Header {
@@ -68,20 +69,6 @@ func (s *sendFileResponseWriter) WriteHeader(status int) {
 		return
 	}
 
-	if header := s.Header().Get(senddata.Header); header != "" {
-		s.Header().Del(senddata.Header)
-		for _, handler := range []senddata.Injecter{
-			git.SendBlob,
-			git.SendArchive,
-		} {
-			if handler.Match(header) {
-				s.hijacked = true
-				handler.Inject(s.rw, s.req, header)
-				return
-			}
-		}
-	}
-
 	s.rw.WriteHeader(s.status)
 	return
 }
diff --git a/internal/upstream/routes.go b/internal/upstream/routes.go
index 4462a5c..a1cd83c 100644
--- a/internal/upstream/routes.go
+++ b/internal/upstream/routes.go
@@ -6,6 +6,8 @@ import (
 	"../git"
 	"../lfs"
 	proxypkg "../proxy"
+	"../senddata"
+	"../sendfile"
 	"../staticpages"
 	"net/http"
 	"regexp"
@@ -37,10 +39,14 @@ func (u *Upstream) configureRoutes() {
 		u.RoundTripper,
 	)
 	static := &staticpages.Static{u.DocumentRoot}
-	proxy := proxypkg.NewProxy(
-		u.Backend,
-		u.Version,
-		u.RoundTripper,
+	proxy := senddata.SendData(
+		sendfile.SendFile(proxypkg.NewProxy(
+			u.Backend,
+			u.Version,
+			u.RoundTripper,
+		)),
+		git.SendArchive,
+		git.SendBlob,
 	)
 
 	u.Routes = []route{
-- 
GitLab


From 28c9928c81d4b93ecf619dc0f4aa0ef596fad528 Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Thu, 11 Feb 2016 17:36:45 +0100
Subject: [PATCH 06/10] Declare params structs outside Inject functions

---
 internal/git/archive.go | 13 +++++++------
 internal/git/blob.go    |  3 ++-
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/internal/git/archive.go b/internal/git/archive.go
index e94f5cd..95016e9 100644
--- a/internal/git/archive.go
+++ b/internal/git/archive.go
@@ -21,16 +21,17 @@ import (
 )
 
 type archive struct{ senddata.Prefix }
+type archiveParams struct {
+	RepoPath      string
+	ArchivePath   string
+	ArchivePrefix string
+	CommitId      string
+}
 
 var SendArchive = &archive{"git-archive:"}
 
 func (a *archive) Inject(w http.ResponseWriter, r *http.Request, sendData string) {
-	var params struct {
-		RepoPath      string
-		ArchivePath   string
-		ArchivePrefix string
-		CommitId      string
-	}
+	var params archiveParams
 	if err := a.Unpack(&params, sendData); err != nil {
 		helper.Fail500(w, fmt.Errorf("SendArchive: unpack sendData: %v", err))
 		return
diff --git a/internal/git/blob.go b/internal/git/blob.go
index fbaac2d..b9247f7 100644
--- a/internal/git/blob.go
+++ b/internal/git/blob.go
@@ -10,11 +10,12 @@ import (
 )
 
 type blob struct{ senddata.Prefix }
+type blobParams struct{ RepoPath, BlobId string }
 
 var SendBlob = &blob{"git-blob:"}
 
 func (b *blob) Inject(w http.ResponseWriter, r *http.Request, sendData string) {
-	var params struct{ RepoPath, BlobId string }
+	var params blobParams
 	if err := b.Unpack(&params, sendData); err != nil {
 		helper.Fail500(w, fmt.Errorf("SendBlob: unpack sendData: %v", err))
 		return
-- 
GitLab


From 92e88955bf60b11833978feafeb62140b3573c19 Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Wed, 17 Feb 2016 12:37:04 +0100
Subject: [PATCH 07/10] Simplify WriteHeader in senddata

---
 internal/senddata/senddata.go | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/internal/senddata/senddata.go b/internal/senddata/senddata.go
index 42f4091..982c243 100644
--- a/internal/senddata/senddata.go
+++ b/internal/senddata/senddata.go
@@ -42,26 +42,27 @@ func (s *sendDataResponseWriter) WriteHeader(status int) {
 	if s.status != 0 {
 		return
 	}
-
 	s.status = status
-	if s.status != http.StatusOK {
-		s.rw.WriteHeader(s.status)
+
+	if s.status == http.StatusOK && s.tryInject() {
 		return
 	}
 
+	s.rw.WriteHeader(s.status)
+}
+
+func (s *sendDataResponseWriter) tryInject() bool {
 	if header := s.Header().Get(HeaderKey); header != "" {
 		s.Header().Del(HeaderKey)
 		for _, injecter := range s.injecters {
 			if injecter.Match(header) {
 				s.hijacked = true
 				injecter.Inject(s.rw, s.req, header)
-				return
+				return true
 			}
 		}
 	}
-
-	s.rw.WriteHeader(s.status)
-	return
+	return false
 }
 
 func (s *sendDataResponseWriter) Flush() {
-- 
GitLab


From 1a9126c9e6829e5a66f5e747f2673dcba04d1329 Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Wed, 17 Feb 2016 13:43:42 +0100
Subject: [PATCH 08/10] Add fmt task

---
 Makefile | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Makefile b/Makefile
index 197ca4b..6d65af4 100644
--- a/Makefile
+++ b/Makefile
@@ -28,6 +28,9 @@ coverage: testdata/data/group/test.git
 	go tool cover -html=test.coverage -o coverage.html
 	rm -f test.coverage
 
+fmt:
+	go fmt ./...
+
 testdata/data/group/test.git: testdata/data
 	git clone --bare https://gitlab.com/gitlab-org/gitlab-test.git $@
 
-- 
GitLab


From f9e11b15d666d6160a75afc0ea8222897f12766f Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Wed, 17 Feb 2016 13:51:37 +0100
Subject: [PATCH 09/10] Remove unnecessary step from main_test.go

---
 main_test.go | 2 --
 1 file changed, 2 deletions(-)

diff --git a/main_test.go b/main_test.go
index 7003112..d4e1eb0 100644
--- a/main_test.go
+++ b/main_test.go
@@ -677,8 +677,6 @@ func archiveOKServer(t *testing.T, archiveName string) *httptest.Server {
 		}
 		encodedJSON := base64.StdEncoding.EncodeToString(jsonData)
 		w.Header().Set("Gitlab-Workhorse-Send-Data", "git-archive:"+encodedJSON)
-		// Prevent the Go HTTP server from setting the Content-Length to 0.
-		w.Header().Set("Transfer-Encoding", "chunked")
 	})
 }
 
-- 
GitLab


From 85e6cbf861a09be89492433d40a16cf9a1106251 Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Wed, 17 Feb 2016 13:57:03 +0100
Subject: [PATCH 10/10] Use an early return in senddata.tryInject

---
 internal/senddata/senddata.go | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/internal/senddata/senddata.go b/internal/senddata/senddata.go
index 982c243..d865858 100644
--- a/internal/senddata/senddata.go
+++ b/internal/senddata/senddata.go
@@ -52,16 +52,20 @@ func (s *sendDataResponseWriter) WriteHeader(status int) {
 }
 
 func (s *sendDataResponseWriter) tryInject() bool {
-	if header := s.Header().Get(HeaderKey); header != "" {
-		s.Header().Del(HeaderKey)
-		for _, injecter := range s.injecters {
-			if injecter.Match(header) {
-				s.hijacked = true
-				injecter.Inject(s.rw, s.req, header)
-				return true
-			}
+	header := s.Header().Get(HeaderKey)
+	s.Header().Del(HeaderKey)
+	if header == "" {
+		return false
+	}
+
+	for _, injecter := range s.injecters {
+		if injecter.Match(header) {
+			s.hijacked = true
+			injecter.Inject(s.rw, s.req, header)
+			return true
 		}
 	}
+
 	return false
 }
 
-- 
GitLab