package app import ( "bytes" "fmt" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" ) // ---------------- Git HTTP ---------------- func (s *Server) handleGitHTTP(w http.ResponseWriter, r *http.Request) { owner, repoName, _, ok := parseGitPath(r.URL.Path) if !ok { writeError(w, http.StatusNotFound, "invalid git path") return } repo, err := s.loadRepo(owner, repoName) if err != nil { writeError(w, http.StatusNotFound, "repository not found") return } service := gitService(r) user, authed := s.userFromBasic(r) if service == "git-receive-pack" { if !authed { w.Header().Set("WWW-Authenticate", `Basic realm="gitocean"`) writeError(w, http.StatusUnauthorized, "authentication required") return } if repo.Archived { writeError(w, http.StatusForbidden, "repository is archived") return } if !s.canWriteRepo(repo, user) { writeError(w, http.StatusForbidden, "write access required") return } } else { if repo.Visibility == "private" { if !authed { w.Header().Set("WWW-Authenticate", `Basic realm="gitocean"`) writeError(w, http.StatusUnauthorized, "authentication required") return } if !s.canReadRepo(repo, user, authed) { writeError(w, http.StatusForbidden, "not allowed") return } } } s.runGitHTTPBackend(w, r, authed, user.Username) } func parseGitPath(path string) (owner, repo, rest string, ok bool) { path = strings.TrimPrefix(path, "/") idx := strings.Index(path, ".git") if idx < 0 { return "", "", "", false } repoPart := path[:idx] rest = path[idx+len(".git"):] parts := strings.Split(repoPart, "/") if len(parts) != 2 { return "", "", "", false } return parts[0], parts[1], rest, true } func gitService(r *http.Request) string { if svc := r.URL.Query().Get("service"); svc != "" { return svc } if strings.HasSuffix(r.URL.Path, "/git-receive-pack") { return "git-receive-pack" } return "git-upload-pack" } func (s *Server) runGitHTTPBackend(w http.ResponseWriter, r *http.Request, authed bool, username string) { root, _ := filepath.Abs(filepath.Join(s.storage, "repos")) env := append(os.Environ(), "GIT_PROJECT_ROOT="+root, "GIT_HTTP_EXPORT_ALL=1", "PATH_INFO="+r.URL.Path, "REQUEST_METHOD="+r.Method, "QUERY_STRING="+r.URL.RawQuery, "REMOTE_ADDR="+r.RemoteAddr, ) if ct := r.Header.Get("Content-Type"); ct != "" { env = append(env, "CONTENT_TYPE="+ct) } if r.ContentLength >= 0 { env = append(env, fmt.Sprintf("CONTENT_LENGTH=%d", r.ContentLength)) } if authed { env = append(env, "REMOTE_USER="+username, "AUTH_TYPE=Basic") } cmd := exec.Command("git", "http-backend") cmd.Env = env cmd.Stdin = r.Body var out, errBuf bytes.Buffer cmd.Stdout = &out cmd.Stderr = &errBuf if err := cmd.Run(); err != nil { writeError(w, http.StatusInternalServerError, strings.TrimSpace(errBuf.String())) return } writeCGIResponse(w, out.Bytes()) } func writeCGIResponse(w http.ResponseWriter, data []byte) { sep := []byte("\r\n\r\n") idx := bytes.Index(data, sep) if idx < 0 { sep = []byte("\n\n") idx = bytes.Index(data, sep) } if idx < 0 { _, _ = w.Write(data) return } headers := string(data[:idx]) body := data[idx+len(sep):] status := http.StatusOK for _, line := range strings.Split(headers, "\n") { line = strings.TrimRight(line, "\r") if line == "" { continue } k, v, ok := strings.Cut(line, ":") if !ok { continue } k = strings.TrimSpace(k) v = strings.TrimSpace(v) if strings.EqualFold(k, "Status") { fields := strings.Fields(v) if len(fields) > 0 { if n, err := strconv.Atoi(fields[0]); err == nil { status = n } } continue } w.Header().Add(k, v) } w.WriteHeader(status) _, _ = w.Write(body) }