Commit 74eb3e7c authored by Jacob Vosmaer's avatar Jacob Vosmaer

Merge branch 'serve-uploads' into 'master'

Serve GitLab uploads via gitlab-workhorse / X-Sendfile

Building on Kamil's work in https://gitlab.com/gitlab-org/gitlab-workhorse/merge_requests/5

The xSendFile middleware should be reusable for other types of requests too (artifacts, LFS etc.).

See merge request !4
parents c2697091 1632cfe6
PREFIX=/usr/local PREFIX=/usr/local
VERSION=$(shell git describe)-$(shell date -u +%Y%m%d.%H%M%S) VERSION=$(shell git describe)-$(shell date -u +%Y%m%d.%H%M%S)
gitlab-workhorse: main.go upstream.go archive.go git-http.go helpers.go gitlab-workhorse: main.go upstream.go archive.go git-http.go helpers.go xsendfile.go
go build -ldflags "-X main.Version ${VERSION}" -o gitlab-workhorse go build -ldflags "-X main.Version ${VERSION}" -o gitlab-workhorse
install: gitlab-workhorse install: gitlab-workhorse
......
...@@ -239,6 +239,73 @@ func TestDownloadCacheCreate(t *testing.T) { ...@@ -239,6 +239,73 @@ func TestDownloadCacheCreate(t *testing.T) {
} }
} }
func TestAllowedXSendfileDownload(t *testing.T) {
contentFilename := "my-content"
contentPath := path.Join(cacheDir, contentFilename)
prepareDownloadDir(t)
// Prepare test server and backend
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if xSendfileType := r.Header.Get("X-Sendfile-Type"); xSendfileType != "X-Sendfile" {
t.Fatalf(`X-Sendfile-Type want "X-Sendfile" got %q`, xSendfileType)
}
w.Header().Set("X-Sendfile", contentPath)
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, contentFilename))
w.WriteHeader(200)
}))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
if err := os.MkdirAll(cacheDir, 0755); err != nil {
t.Fatal(err)
}
contentBytes := []byte{'c', 'o', 'n', 't', 'e', 'n', 't'}
if err := ioutil.WriteFile(contentPath, contentBytes, 0644); err != nil {
t.Fatal(err)
}
downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/foo/uploads/bar", servAddr))
downloadCmd.Dir = scratchDir
runOrFail(t, downloadCmd)
actual, err := ioutil.ReadFile(path.Join(scratchDir, contentFilename))
if err != nil {
t.Fatal(err)
}
if bytes.Compare(actual, contentBytes) != 0 {
t.Fatal("Unexpected file contents in download")
}
}
func TestDeniedXSendfileDownload(t *testing.T) {
contentFilename := "my-content"
prepareDownloadDir(t)
// Prepare test server and backend
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if xSendfileType := r.Header.Get("X-Sendfile-Type"); xSendfileType != "X-Sendfile" {
t.Fatalf(`X-Sendfile-Type want "X-Sendfile" got %q`, xSendfileType)
}
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, contentFilename))
w.WriteHeader(200)
fmt.Fprint(w, "Denied")
}))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/foo/uploads/bar", servAddr))
downloadCmd.Dir = scratchDir
runOrFail(t, downloadCmd)
actual, err := ioutil.ReadFile(path.Join(scratchDir, contentFilename))
if err != nil {
t.Fatal(err)
}
if bytes.Compare(actual, []byte{'D', 'e', 'n', 'i', 'e', 'd'}) != 0 {
t.Fatal("Unexpected file contents in download")
}
}
func prepareDownloadDir(t *testing.T) { func prepareDownloadDir(t *testing.T) {
if err := os.RemoveAll(scratchDir); err != nil { if err := os.RemoveAll(scratchDir); err != nil {
t.Fatal(err) t.Fatal(err)
...@@ -269,7 +336,7 @@ func testAuthServer(code int, body string) *httptest.Server { ...@@ -269,7 +336,7 @@ func testAuthServer(code int, body string) *httptest.Server {
} }
func startServerOrFail(t *testing.T, ts *httptest.Server) *exec.Cmd { func startServerOrFail(t *testing.T, ts *httptest.Server) *exec.Cmd {
cmd := exec.Command("go", "run", "main.go", "upstream.go", "archive.go", "git-http.go", "helpers.go", fmt.Sprintf("-authBackend=%s", ts.URL), fmt.Sprintf("-listenAddr=%s", servAddr)) cmd := exec.Command("go", "run", "main.go", "upstream.go", "archive.go", "git-http.go", "helpers.go", "xsendfile.go", fmt.Sprintf("-authBackend=%s", ts.URL), fmt.Sprintf("-listenAddr=%s", servAddr))
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
......
...@@ -61,6 +61,7 @@ var gitServices = [...]gitService{ ...@@ -61,6 +61,7 @@ var gitServices = [...]gitService{
gitService{"GET", regexp.MustCompile(`/repository/archive.tar\z`), repoPreAuth, handleGetArchive, "tar"}, gitService{"GET", regexp.MustCompile(`/repository/archive.tar\z`), repoPreAuth, handleGetArchive, "tar"},
gitService{"GET", regexp.MustCompile(`/repository/archive.tar.gz\z`), repoPreAuth, handleGetArchive, "tar.gz"}, gitService{"GET", regexp.MustCompile(`/repository/archive.tar.gz\z`), repoPreAuth, handleGetArchive, "tar.gz"},
gitService{"GET", regexp.MustCompile(`/repository/archive.tar.bz2\z`), repoPreAuth, handleGetArchive, "tar.bz2"}, gitService{"GET", regexp.MustCompile(`/repository/archive.tar.bz2\z`), repoPreAuth, handleGetArchive, "tar.bz2"},
gitService{"GET", regexp.MustCompile(`/uploads/`), xSendFile, nil, ""},
} }
func newUpstream(authBackend string, authTransport http.RoundTripper) *upstream { func newUpstream(authBackend string, authTransport http.RoundTripper) *upstream {
...@@ -91,27 +92,13 @@ func (u *upstream) ServeHTTP(w http.ResponseWriter, r *http.Request) { ...@@ -91,27 +92,13 @@ func (u *upstream) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
func repoPreAuth(u *upstream, w http.ResponseWriter, r *http.Request, handleFunc func(w http.ResponseWriter, r *gitRequest, rpc string), rpc string) { func repoPreAuth(u *upstream, w http.ResponseWriter, r *http.Request, handleFunc func(w http.ResponseWriter, r *gitRequest, rpc string), rpc string) {
url := u.authBackend + r.URL.RequestURI() authReq, err := u.newUpstreamRequest(r)
authReq, err := http.NewRequest(r.Method, url, nil)
if err != nil { if err != nil {
fail500(w, "doAuthRequest", err) fail500(w, "newUpstreamRequest", err)
return return
} }
// 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
}
// Also forward the Host header, which is excluded from the Header map by the http libary.
// This allows the Host header received by the backend to be consistent with other
// requests not going through gitlab-workhorse.
authReq.Host = r.Host
// Set a custom header for the request. This can be used in some
// configurations (Passenger) to solve auth request routing problems.
authReq.Header.Set("GitLab-Git-HTTP-Server", Version)
authResponse, err := u.httpClient.Do(authReq) authResponse, err := u.httpClient.Do(authReq)
if err != nil { if err != nil {
fail500(w, "doAuthRequest", err) fail500(w, "doAuthRequest", err)
return return
...@@ -174,3 +161,25 @@ func looksLikeRepo(p string) bool { ...@@ -174,3 +161,25 @@ func looksLikeRepo(p string) bool {
} }
return true return true
} }
func (u *upstream) newUpstreamRequest(r *http.Request) (*http.Request, error) {
url := u.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
}
// Also forward the Host header, which is excluded from the Header map by the http libary.
// This allows the Host header received by the backend to be consistent with other
// requests not going through gitlab-workhorse.
authReq.Host = r.Host
// Set a custom header for the request. This can be used in some
// configurations (Passenger) to solve auth request routing problems.
authReq.Header.Set("GitLab-Git-HTTP-Server", Version)
return authReq, nil
}
/*
The xSendFile middleware transparently sends static files in HTTP responses
via the X-Sendfile mechanism. All that is needed in the Rails code is the
'send_file' method.
*/
package main
import (
"io"
"log"
"net/http"
"os"
)
func xSendFile(u *upstream, w http.ResponseWriter, r *http.Request, _ func(http.ResponseWriter, *gitRequest, string), _ string) {
upRequest, err := u.newUpstreamRequest(r)
if err != nil {
fail500(w, "newUpstreamRequest", err)
return
}
upRequest.Header.Set("X-Sendfile-Type", "X-Sendfile")
upResponse, err := u.httpClient.Do(upRequest)
if err != nil {
fail500(w, "do upstream request", err)
return
}
defer upResponse.Body.Close()
// Get X-Sendfile
sendfile := upResponse.Header.Get("X-Sendfile")
upResponse.Header.Del("X-Sendfile")
// Copy headers from Rails upResponse
for k, v := range upResponse.Header {
w.Header()[k] = v
}
// Use accelerated file serving
if sendfile == "" {
// Copy request body otherwise
w.WriteHeader(upResponse.StatusCode)
// Copy body from Rails upResponse
if _, err := io.Copy(w, upResponse.Body); err != nil {
fail500(w, "Couldn't finalize X-File download request.", err)
}
return
}
log.Printf("Serving file %q", sendfile)
upResponse.Body.Close()
content, err := os.Open(sendfile)
if err != nil {
fail500(w, "open sendfile", err)
return
}
defer content.Close()
fi, err := content.Stat()
if err != nil {
fail500(w, "xSendFile get mtime", err)
return
}
http.ServeContent(w, r, "", fi.ModTime(), content)
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment