package git import ( "bufio" "bytes" "fmt" "io" "net/http" "net/url" "os" "os/exec" "strconv" "strings" ) // ServeBackend execs `git http-backend` as a CGI for req, translating it // into the environment http-backend expects: GIT_PROJECT_ROOT is // projectRoot, PATH_INFO is the request path, and remoteUser (when the // caller has authenticated someone) is passed as REMOTE_USER. service is // the single git service the caller has authorized; it becomes // QUERY_STRING, so the raw request query cannot smuggle in a second // service that re-routes the CGI. The CGI response — including any Status // header — is written to w. The returned error only reports exec/pipe // failures; a 404 from http-backend is a successful response. func ServeBackend(projectRoot, remoteUser, service string, req *http.Request, w http.ResponseWriter) error { query := "" if service != "" { query = "service=" + url.QueryEscape(service) } env := []string{ "GIT_PROJECT_ROOT=" + projectRoot, "GIT_HTTP_EXPORT_ALL=1", "GATEWAY_INTERFACE=CGI/1.1", "SERVER_PROTOCOL=" + req.Proto, "REQUEST_METHOD=" + req.Method, "PATH_INFO=" + req.URL.Path, "QUERY_STRING=" + query, "CONTENT_TYPE=" + req.Header.Get("Content-Type"), "CONTENT_LENGTH=" + req.Header.Get("Content-Length"), "LANG=C", "LC_ALL=C", "PATH=" + os.Getenv("PATH"), } if remoteUser != "" { env = append(env, "REMOTE_USER="+remoteUser) } // The post-receive hook needs to find this binary; ServeBackend is the // only caller that runs receive-pack, so it passes the path explicitly // rather than letting the hook depend on the ambient environment. if self, err := os.Executable(); err == nil { env = append(env, "SIMPLEGIT_BIN="+self) } cmd := exec.Command("git", "http-backend") cmd.Env = env var stderr bytes.Buffer cmd.Stderr = &stderr // The request body is streamed into http-backend's stdin through an // explicit pipe so we can wait for the copy to finish before writing the // response. net/http starts a background reader for the request body as // soon as the handler writes a response; that reader would race this copy // and truncate large pushes (git's receive-pack then waits forever for // the missing pack bytes). var stdinDone chan error if req.Method != http.MethodGet && req.Body != nil { pr, pw, err := os.Pipe() if err != nil { return fmt.Errorf("http-backend stdin pipe: %w", err) } cmd.Stdin = pr stdinDone = make(chan error, 1) go func() { _, copyErr := io.Copy(pw, req.Body) pw.Close() stdinDone <- copyErr }() defer pr.Close() } stdout, err := cmd.StdoutPipe() if err != nil { return fmt.Errorf("http-backend stdout pipe: %w", err) } if err := cmd.Start(); err != nil { return fmt.Errorf("start http-backend: %w", err) } if stdinDone != nil { if err := <-stdinDone; err != nil { cmd.Wait() return fmt.Errorf("feed http-backend stdin: %w", err) } } br := bufio.NewReader(stdout) status, headers, err := readCGIHeaders(br) if err != nil { cmd.Wait() return fmt.Errorf("read http-backend headers: %w: %s", err, strings.TrimSpace(stderr.String())) } for _, header := range headers { if !strings.EqualFold(header[0], "Transfer-Encoding") { w.Header().Add(header[0], header[1]) } } w.WriteHeader(status) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } if _, err := io.Copy(w, br); err != nil { cmd.Wait() return fmt.Errorf("stream http-backend body: %w", err) } if err := cmd.Wait(); err != nil { return fmt.Errorf("http-backend: %w: %s", err, strings.TrimSpace(stderr.String())) } return nil } // readCGIHeaders parses the CGI header block, pulling the optional Status // header out as the HTTP code; everything else is returned verbatim. func readCGIHeaders(r *bufio.Reader) (int, [][2]string, error) { status := http.StatusOK var headers [][2]string for { line, err := r.ReadString('\n') line = strings.TrimRight(line, "\r\n") if line == "" { if err != nil { return 0, nil, fmt.Errorf("eof before end of headers: %w", err) } return status, headers, nil } key, value, ok := strings.Cut(line, ":") if !ok { return 0, nil, fmt.Errorf("malformed header line %q", line) } value = strings.TrimSpace(value) if strings.EqualFold(key, "Status") { code, _, _ := strings.Cut(value, " ") parsed, err := strconv.Atoi(code) if err != nil { return 0, nil, fmt.Errorf("bad Status header %q", value) } status = parsed continue } headers = append(headers, [2]string{key, value}) if err != nil { return 0, nil, fmt.Errorf("eof mid-headers: %w", err) } } }