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 was designed to unload Git HTTP traffic from
the GitLab Rails app (Unicorn) to a separate daemon. All authentication
and authorization logic is still handled by the GitLab Rails app.
the GitLab Rails app (Unicorn) to a separate daemon. It also serves
'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
auth request to GitLab Rails app) -> git-upload-pack
......@@ -10,7 +11,7 @@ auth request to GitLab Rails app) -> git-upload-pack
## Usage
```
gitlab-git-http-server [OPTIONS] REPO_ROOT
gitlab-git-http-server [OPTIONS]
Options:
-authBackend string
......@@ -27,11 +28,13 @@ Options:
Print version and exit
```
gitlab-git-http-server allows Git HTTP clients to push and pull to and from Git
repositories under REPO_ROOT. Each incoming request is first replayed (with an
empty request body) to an external authentication/authorization HTTP server:
the 'auth backend'. The auth backend is expected to be a GitLab Unicorn
process.
gitlab-git-http-server allows Git HTTP clients to push and pull to
and from Git repositories. Each incoming request is first replayed
(with an empty request body) to an external authentication/authorization
HTTP server: the 'auth backend'. The auth backend is expected to
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
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:
```
# 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
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`,
you can push to and pull from it at the URL
`http://localhost:8181/my-repo.git`.
Now you can try things like:
```
git clone http://localhost:8181/test.git
curl -JO http://localhost:8181/test/repository/archive.zip
```
## Example request flow
......
......@@ -22,7 +22,6 @@ import (
type gitHandler struct {
httpClient *http.Client
repoRoot string
authBackend string
}
......@@ -35,6 +34,7 @@ type gitService struct {
type gitEnv struct {
GL_ID string
RepoPath string
ArchivePath string
}
......@@ -49,8 +49,8 @@ var gitServices = [...]gitService{
gitService{"GET", "/repository/archive.tar.bz2", handleGetArchive, "tar.bz2"},
}
func newGitHandler(repoRoot, authBackend string) *gitHandler {
return &gitHandler{&http.Client{}, repoRoot, authBackend}
func newGitHandler(authBackend string) *gitHandler {
return &gitHandler{&http.Client{}, authBackend}
}
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
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))
repoPath := env.RepoPath
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
g.handleFunc(env, g.rpc, repoPath, w, r)
}
......@@ -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) {
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
rpc := r.URL.Query().Get("service")
if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") {
// The 'dumb' Git HTTP protocol is not supported
......@@ -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")
if ref == "" {
ref = "HEAD"
}
repoPath := almostPath + ".git"
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
var compressCmd *exec.Cmd
var archiveFormat string
switch format {
......@@ -287,11 +273,6 @@ func handlePostRPC(env gitEnv, rpc string, repoPath string, w http.ResponseWrite
var body io.Reader
var err error
if !looksLikeRepo(repoPath) {
http.Error(w, "Not Found", 404)
return
}
// The client request body may have been gzipped.
if r.Header.Get("Content-Encoding") == "gzip" {
body, err = gzip.NewReader(r.Body)
......
......@@ -36,22 +36,18 @@ func main() {
pprofListenAddr := flag.String("pprofListenAddr", "", "pprof listening address, e.g. 'localhost:6060'")
flag.Usage = func() {
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.Parse()
version := fmt.Sprintf("gitlab-git-http-server %s", Version)
if *printVersion {
fmt.Printf("gitlab-git-http-server %s\n", Version)
fmt.Println(version)
os.Exit(0)
}
repoRoot := flag.Arg(0)
if repoRoot == "" {
flag.Usage()
os.Exit(1)
}
log.Printf("repoRoot: %s", repoRoot)
log.Printf("Starting %s", version)
// Good housekeeping for Unix sockets: unlink before binding
if *listenNetwork == "unix" {
......@@ -81,6 +77,6 @@ func main() {
// Because net/http/pprof installs itself in the DefaultServeMux
// we create a fresh one for the Git server.
serveMux := http.NewServeMux()
serveMux.Handle("/", newGitHandler(repoRoot, *authBackend))
serveMux.Handle("/", newGitHandler(*authBackend))
log.Fatal(http.Serve(listener, serveMux))
}
......@@ -31,7 +31,7 @@ func TestAllowedClone(t *testing.T) {
}
// Prepare test server and backend
ts := testAuthServer(200, `{"GL_ID":"user-123"}`)
ts := testAuthServer(200, gitOkBody(t))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
......@@ -69,7 +69,7 @@ func TestAllowedPush(t *testing.T) {
preparePushRepo(t)
// Prepare the test server and backend
ts := testAuthServer(200, `{"GL_ID":"user-123"}`)
ts := testAuthServer(200, gitOkBody(t))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
......@@ -102,7 +102,7 @@ func TestAllowedDownloadZip(t *testing.T) {
// Prepare test server and backend
archiveName := "foobar.zip"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName))
ts := testAuthServer(200, archiveOkBody(t, archiveName))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
......@@ -120,7 +120,7 @@ func TestAllowedDownloadTar(t *testing.T) {
// Prepare test server and backend
archiveName := "foobar.tar"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName))
ts := testAuthServer(200, archiveOkBody(t, archiveName))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
......@@ -138,7 +138,7 @@ func TestAllowedDownloadTarGz(t *testing.T) {
// Prepare test server and backend
archiveName := "foobar.tar.gz"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName))
ts := testAuthServer(200, archiveOkBody(t, archiveName))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
......@@ -156,7 +156,7 @@ func TestAllowedDownloadTarBz2(t *testing.T) {
// Prepare test server and backend
archiveName := "foobar.tar.bz2"
ts := testAuthServer(200, fmt.Sprintf(`{"ArchivePath":"/tmp/%s"}`, archiveName))
ts := testAuthServer(200, archiveOkBody(t, archiveName))
defer ts.Close()
defer cleanUpProcessGroup(startServerOrFail(t, ts))
......@@ -199,7 +199,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", "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.Stdout = os.Stdout
cmd.Stderr = os.Stderr
......@@ -236,3 +236,19 @@ func runOrFail(t *testing.T, cmd *exec.Cmd) {
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 (
"fmt"
"log"
"net/http"
"os"
)
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) {
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))
......
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