package main import ( "compress/gzip" "context" "fmt" "io" "net/http" "os" "os/exec" "strings" "time" ) func isSmart(r *http.Request, rest string) bool { switch rest { case "info/refs": return r.URL.Query().Get("service") != "" case "git-upload-pack", "git-receive-pack": return r.Method == "POST" } return false } func (s *Server) basicUser(r *http.Request) (User, bool) { name, secret, ok := r.BasicAuth() if !ok { return User{}, false } if u, ok := s.store.UserByToken(name, secret); ok { return u, true } return s.store.CheckPassword(name, secret) } func (s *Server) smart(w http.ResponseWriter, r *http.Request, owner User, name, rest string) { repo, err := s.store.RepoByPath(owner.Name, name) if err != nil { var viewer *User if u, ok := s.basicUser(r); ok { viewer = &u } segment, _, _ := strings.Cut(strings.TrimPrefix(r.URL.Path, "/~"+owner.Name+"/"), "/") if !s.redirectMoved(w, r, owner.Name, segment, viewer, http.StatusPermanentRedirect) { http.Error(w, "repository not found", 404) } return } service := r.URL.Query().Get("service") if rest != "info/refs" { service = rest } if service != "git-upload-pack" && service != "git-receive-pack" { http.Error(w, "unsupported service", 403) return } needAuth := service == "git-receive-pack" || repo.Private if needAuth { ip := clientIP(r) if s.limiter.blocked(ip) { http.Error(w, "too many failed attempts", 429) return } u, ok := s.basicUser(r) if !ok || !s.store.CanWrite(repo, &u) { if _, _, given := r.BasicAuth(); given { s.limiter.fail(ip) } w.Header().Set("WWW-Authenticate", `Basic realm="`+s.host+`"`) http.Error(w, "authentication required", 401) return } } if service == "git-receive-pack" && repo.Archived { http.Error(w, "repository is archived and read only", 403) return } if service == "git-receive-pack" { if over, used, quota := overQuota(s.root, repo.Owner.Name); over { http.Error(w, quotaMessage(used, quota), 403) return } } dir := s.repoDir(repo) w.Header().Set("Cache-Control", "no-cache") ctx, cancel := context.WithTimeout(r.Context(), 10*time.Minute) defer cancel() if rest == "info/refs" { cmd := exec.CommandContext(ctx, "git", strings.TrimPrefix(service, "git-"), "--stateless-rpc", "--advertise-refs", dir) out, err := cmd.Output() if err != nil { http.Error(w, "git failed", 500) return } w.Header().Set("Content-Type", "application/x-"+service+"-advertisement") fmt.Fprintf(w, "%04x# service=%s\n0000", len(service)+15, service) w.Write(out) return } var body io.Reader = r.Body if r.Header.Get("Content-Encoding") == "gzip" { gz, err := gzip.NewReader(r.Body) if err != nil { http.Error(w, "bad gzip", 400) return } defer gz.Close() body = gz } argv := []string{strings.TrimPrefix(service, "git-"), "--stateless-rpc", dir} if service == "git-receive-pack" { argv = append(pushLimit(s.root, repo.Owner.Name), argv...) if over, used, quota := overQuota(s.root, repo.Owner.Name); quota > 0 && !over { body = io.LimitReader(body, quota-used+1<<20) } } cmd := exec.CommandContext(ctx, "git", argv...) cmd.Stdin = body cmd.Env = []string{"GIT_PROTOCOL=" + r.Header.Get("Git-Protocol"), "HOME=/nonexistent", "PATH=" + os.Getenv("PATH")} stdout, err := cmd.StdoutPipe() if err != nil { http.Error(w, "git failed", 500) return } if err := cmd.Start(); err != nil { http.Error(w, "git failed", 500) return } w.Header().Set("Content-Type", "application/x-"+service+"-result") w.WriteHeader(200) flusher, _ := w.(http.Flusher) buf := make([]byte, 32*1024) for { n, err := stdout.Read(buf) if n > 0 { w.Write(buf[:n]) if flusher != nil { flusher.Flush() } } if err != nil { break } } cmd.Wait() }