Files
2026-06-08 13:34:53 -05:00

155 lines
3.7 KiB
Go

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)
}