Commit ae9a94ea authored by Jacob Vosmaer's avatar Jacob Vosmaer

Get repo path from auth backend

This is a breaking API change. Instead of parsing repo path from the request
in gitlab-git-http-server, we now expect the auth backend to tell us the full
path to the repository. This change is needed to handle API download requests.
parent eeb0410d
# gitlab-git-http-server # gitlab-git-http-server
gitlab-git-http-server was designed to unload Git HTTP traffic from gitlab-git-http-server was designed to unload Git HTTP traffic from
the GitLab Rails app (Unicorn) to a separate daemon. All authentication the GitLab Rails app (Unicorn) to a separate daemon. It also serves
and authorization logic is still handled by the GitLab Rails app. 'git archive' downloads for GitLab. All authentication and
authorization logic is still handled by the GitLab Rails app.
Architecture: Git client -> NGINX -> gitlab-git-http-server (makes Architecture: Git client -> NGINX -> gitlab-git-http-server (makes
auth request to GitLab Rails app) -> git-upload-pack auth request to GitLab Rails app) -> git-upload-pack
...@@ -10,7 +11,7 @@ auth request to GitLab Rails app) -> git-upload-pack ...@@ -10,7 +11,7 @@ auth request to GitLab Rails app) -> git-upload-pack
## Usage ## Usage
``` ```
gitlab-git-http-server [OPTIONS] REPO_ROOT gitlab-git-http-server [OPTIONS]
Options: Options:
-authBackend string -authBackend string
...@@ -27,11 +28,13 @@ Options: ...@@ -27,11 +28,13 @@ Options:
Print version and exit Print version and exit
``` ```
gitlab-git-http-server allows Git HTTP clients to push and pull to and from Git gitlab-git-http-server allows Git HTTP clients to push and pull to
repositories under REPO_ROOT. Each incoming request is first replayed (with an and from Git repositories. Each incoming request is first replayed
empty request body) to an external authentication/authorization HTTP server: (with an empty request body) to an external authentication/authorization
the 'auth backend'. The auth backend is expected to be a GitLab Unicorn HTTP server: the 'auth backend'. The auth backend is expected to
process. be a GitLab Unicorn process. The 'auth response' is a JSON message
which tells gitlab-git-http-server the path of the Git repository
to read from/write to.
gitlab-git-http-server can listen on either a TCP or a Unix domain socket. It gitlab-git-http-server can listen on either a TCP or a Unix domain socket. It
can also open a second listening TCP listening socket with the Go can also open a second listening TCP listening socket with the Go
...@@ -63,14 +66,19 @@ You can try out the Git server without authentication as follows: ...@@ -63,14 +66,19 @@ You can try out the Git server without authentication as follows:
``` ```
# Start a fake auth backend that allows everything/everybody # Start a fake auth backend that allows everything/everybody
go run support/say-yes.go & make test/data/test.git
go run support/fake-auth-backend.go ~+/test/data/test.git &
# Start gitlab-git-http-server # Start gitlab-git-http-server
go build && ./gitlab-git-http-server /path/to/git-repos make
./gitlab-git-http-server
``` ```
Now if you have a Git repository in `/path/to/git-repos/my-repo.git`, Now you can try things like:
you can push to and pull from it at the URL
`http://localhost:8181/my-repo.git`. ```
git clone http://localhost:8181/test.git
curl -JO http://localhost:8181/test/repository/archive.zip
```
## Example request flow ## Example request flow
......
...@@ -22,7 +22,6 @@ import ( ...@@ -22,7 +22,6 @@ import (
type gitHandler struct { type gitHandler struct {
httpClient *http.Client httpClient *http.Client
repoRoot string
authBackend string authBackend string
} }
...@@ -35,6 +34,7 @@ type gitService struct { ...@@ -35,6 +34,7 @@ type gitService struct {
type gitEnv struct { type gitEnv struct {
GL_ID string GL_ID string
RepoPath string
ArchivePath string ArchivePath string
} }
...@@ -49,8 +49,8 @@ var gitServices = [...]gitService{ ...@@ -49,8 +49,8 @@ var gitServices = [...]gitService{
gitService{"GET", "/repository/archive.tar.bz2", handleGetArchive, "tar.bz2"}, gitService{"GET", "/repository/archive.tar.bz2", handleGetArchive, "tar.bz2"},
} }
func newGitHandler(repoRoot, authBackend string) *gitHandler { func newGitHandler(authBackend string) *gitHandler {
return &gitHandler{&http.Client{}, repoRoot, authBackend} return &gitHandler{&http.Client{}, authBackend}
} }
func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
...@@ -108,14 +108,11 @@ func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ...@@ -108,14 +108,11 @@ func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Don't hog a TCP connection in CLOSE_WAIT, we can already close it now // Don't hog a TCP connection in CLOSE_WAIT, we can already close it now
authResponse.Body.Close() authResponse.Body.Close()
// About path traversal: the Go net/http HTTP server, or repoPath := env.RepoPath
// rather ServeMux, makes the following promise: "ServeMux if !looksLikeRepo(repoPath) {
// also takes care of sanitizing the URL request path, redirecting http.Error(w, "Not Found", 404)
// any request containing . or .. elements to an equivalent return
// .- 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))
g.handleFunc(env, g.rpc, repoPath, w, r) g.handleFunc(env, g.rpc, repoPath, w, r)
} }
...@@ -145,11 +142,6 @@ func (h *gitHandler) doAuthRequest(r *http.Request) (result *http.Response, err ...@@ -145,11 +142,6 @@ func (h *gitHandler) doAuthRequest(r *http.Request) (result *http.Response, err
} }
func handleGetInfoRefs(env gitEnv, _ string, repoPath string, w http.ResponseWriter, r *http.Request) { func handleGetInfoRefs(env gitEnv, _ string, repoPath string, w http.ResponseWriter, r *http.Request) {
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
rpc := r.URL.Query().Get("service") rpc := r.URL.Query().Get("service")
if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") { if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") {
// The 'dumb' Git HTTP protocol is not supported // The 'dumb' Git HTTP protocol is not supported
...@@ -193,18 +185,12 @@ func handleGetInfoRefs(env gitEnv, _ string, repoPath string, w http.ResponseWri ...@@ -193,18 +185,12 @@ func handleGetInfoRefs(env gitEnv, _ string, repoPath string, w http.ResponseWri
} }
} }
func handleGetArchive(env gitEnv, format string, almostPath string, w http.ResponseWriter, r *http.Request) { func handleGetArchive(env gitEnv, format string, repoPath string, w http.ResponseWriter, r *http.Request) {
ref := r.URL.Query().Get("ref") ref := r.URL.Query().Get("ref")
if ref == "" { if ref == "" {
ref = "HEAD" ref = "HEAD"
} }
repoPath := almostPath + ".git"
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
var compressCmd *exec.Cmd var compressCmd *exec.Cmd
var archiveFormat string var archiveFormat string
switch format { switch format {
...@@ -287,11 +273,6 @@ func handlePostRPC(env gitEnv, rpc string, repoPath string, w http.ResponseWrite ...@@ -287,11 +273,6 @@ func handlePostRPC(env gitEnv, rpc string, repoPath string, w http.ResponseWrite
var body io.Reader var body io.Reader
var err error var err error
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
// The client request body may have been gzipped. // The client request body may have been gzipped.
if r.Header.Get("Content-Encoding") == "gzip" { if r.Header.Get("Content-Encoding") == "gzip" {
body, err = gzip.NewReader(r.Body) body, err = gzip.NewReader(r.Body)
......
...@@ -36,22 +36,18 @@ func main() { ...@@ -36,22 +36,18 @@ func main() {
pprofListenAddr := flag.String("pprofListenAddr", "", "pprof listening address, e.g. 'localhost:6060'") pprofListenAddr := flag.String("pprofListenAddr", "", "pprof listening address, e.g. 'localhost:6060'")
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\n %s [OPTIONS] REPO_ROOT\n\nOptions:\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\n %s [OPTIONS]\n\nOptions:\n", os.Args[0])
flag.PrintDefaults() flag.PrintDefaults()
} }
flag.Parse() flag.Parse()
version := fmt.Sprintf("gitlab-git-http-server %s", Version)
if *printVersion { if *printVersion {
fmt.Printf("gitlab-git-http-server %s\n", Version) fmt.Println(version)
os.Exit(0) os.Exit(0)
} }
repoRoot := flag.Arg(0) log.Printf("Starting %s", version)
if repoRoot == "" {
flag.Usage()
os.Exit(1)
}
log.Printf("repoRoot: %s", repoRoot)
// Good housekeeping for Unix sockets: unlink before binding // Good housekeeping for Unix sockets: unlink before binding
if *listenNetwork == "unix" { if *listenNetwork == "unix" {
...@@ -81,6 +77,6 @@ func main() { ...@@ -81,6 +77,6 @@ func main() {
// Because net/http/pprof installs itself in the DefaultServeMux // Because net/http/pprof installs itself in the DefaultServeMux
// we create a fresh one for the Git server. // we create a fresh one for the Git server.
serveMux := http.NewServeMux() serveMux := http.NewServeMux()
serveMux.Handle("/", newGitHandler(repoRoot, *authBackend)) serveMux.Handle("/", newGitHandler(*authBackend))
log.Fatal(http.Serve(listener, serveMux)) log.Fatal(http.Serve(listener, serveMux))
} }
...@@ -31,7 +31,7 @@ func TestAllowedClone(t *testing.T) { ...@@ -31,7 +31,7 @@ func TestAllowedClone(t *testing.T) {
} }
// Prepare test server and backend // Prepare test server and backend
ts := testAuthServer(200, `{"GL_ID":"user-123"}`) ts := testAuthServer(200, gitOkBody(t))
defer ts.Close() defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts)) defer cleanUpProcessGroup(startServerOrFail(t, ts))
...@@ -69,7 +69,7 @@ func TestAllowedPush(t *testing.T) { ...@@ -69,7 +69,7 @@ func TestAllowedPush(t *testing.T) {
preparePushRepo(t) preparePushRepo(t)
// Prepare the test server and backend // Prepare the test server and backend
ts := testAuthServer(200, `{"GL_ID":"user-123"}`) ts := testAuthServer(200, gitOkBody(t))
defer ts.Close() defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts)) defer cleanUpProcessGroup(startServerOrFail(t, ts))
...@@ -102,7 +102,7 @@ func TestAllowedDownloadZip(t *testing.T) { ...@@ -102,7 +102,7 @@ func TestAllowedDownloadZip(t *testing.T) {
// Prepare test server and backend // Prepare test server and backend
archiveName := "foobar.zip" archiveName := "foobar.zip"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName)) ts := testAuthServer(200, archiveOkBody(t, archiveName))
defer ts.Close() defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts)) defer cleanUpProcessGroup(startServerOrFail(t, ts))
...@@ -120,7 +120,7 @@ func TestAllowedDownloadTar(t *testing.T) { ...@@ -120,7 +120,7 @@ func TestAllowedDownloadTar(t *testing.T) {
// Prepare test server and backend // Prepare test server and backend
archiveName := "foobar.tar" archiveName := "foobar.tar"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName)) ts := testAuthServer(200, archiveOkBody(t, archiveName))
defer ts.Close() defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts)) defer cleanUpProcessGroup(startServerOrFail(t, ts))
...@@ -138,7 +138,7 @@ func TestAllowedDownloadTarGz(t *testing.T) { ...@@ -138,7 +138,7 @@ func TestAllowedDownloadTarGz(t *testing.T) {
// Prepare test server and backend // Prepare test server and backend
archiveName := "foobar.tar.gz" archiveName := "foobar.tar.gz"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName)) ts := testAuthServer(200, archiveOkBody(t, archiveName))
defer ts.Close() defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts)) defer cleanUpProcessGroup(startServerOrFail(t, ts))
...@@ -156,7 +156,7 @@ func TestAllowedDownloadTarBz2(t *testing.T) { ...@@ -156,7 +156,7 @@ func TestAllowedDownloadTarBz2(t *testing.T) {
// Prepare test server and backend // Prepare test server and backend
archiveName := "foobar.tar.bz2" archiveName := "foobar.tar.bz2"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName)) ts := testAuthServer(200, archiveOkBody(t, archiveName))
defer ts.Close() defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts)) defer cleanUpProcessGroup(startServerOrFail(t, ts))
...@@ -199,7 +199,7 @@ func testAuthServer(code int, body string) *httptest.Server { ...@@ -199,7 +199,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", "githandler.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))
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
...@@ -236,3 +236,19 @@ func runOrFail(t *testing.T, cmd *exec.Cmd) { ...@@ -236,3 +236,19 @@ func runOrFail(t *testing.T, cmd *exec.Cmd) {
t.Fatal(err) t.Fatal(err)
} }
} }
func gitOkBody(t *testing.T) string {
return fmt.Sprintf(`{"GL_ID":"user-123","RepoPath":"%s"}`, repoPath(t))
}
func archiveOkBody(t *testing.T, archiveName string) string {
return fmt.Sprintf(`{"RepoPath":"%s","ArchivePath":"/tmp/%s"}`, repoPath(t), archiveName)
}
func repoPath(t *testing.T) string {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
return path.Join(cwd, testRepoRoot, testRepo)
}
...@@ -4,11 +4,17 @@ import ( ...@@ -4,11 +4,17 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os"
) )
func main() { func main() {
if len(os.Args) == 1 {
fmt.Fprintf(os.Stderr, "Usage: %s /path/to/test-repo.git\n", os.Args[0])
os.Exit(1)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"GL_ID":""}`) fmt.Fprintf(w, `{"RepoPath":"%s","ArchivePath":"%s"}`, os.Args[1], r.URL.Path)
}) })
log.Fatal(http.ListenAndServe("localhost:8080", nil)) log.Fatal(http.ListenAndServe("localhost:8080", nil))
......
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