b93e14ae88f05c6e01d25ab41d2ecb7456d769b3 / internal/git/backend.go · 4634 bytes · raw
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)
}
}
}