Commit 9ed11af6 authored by Jacob Vosmaer's avatar Jacob Vosmaer

Put program initialization in a separate file

parent ceb2092f
......@@ -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`,
......
/*
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
}
......@@ -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
}
......@@ -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
......
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