package server import ( "errors" "fmt" "log" "net/http" "path" "strings" "gitweb/internal/githttp" "gitweb/internal/gitrepo" ) // gitKind identifies which git smart-HTTP endpoint a request hit. type gitKind int const ( gitInfoRefs gitKind = iota gitUploadPack gitReceivePack ) // gitReq describes a matched git smart-HTTP request. type gitReq struct { kind gitKind service githttp.Service // the effective service (upload-pack or receive-pack) urlPath string // URL path with the git suffix stripped, e.g. "/u@d.com/x.git" } // gitEndpoint recognizes the three git smart-HTTP endpoints by method and path // suffix. It returns ok=false for ordinary browse requests. func gitEndpoint(r *http.Request) (gitReq, bool) { p := r.URL.Path switch { case r.Method == http.MethodGet && strings.HasSuffix(p, "/info/refs"): svc := r.URL.Query().Get("service") switch githttp.Service(svc) { case githttp.UploadPack, githttp.ReceivePack: return gitReq{gitInfoRefs, githttp.Service(svc), strings.TrimSuffix(p, "/info/refs")}, true } // info/refs without a smart service param: fall through to browse (this // also lets someone view a repo's literal info/refs file). return gitReq{}, false case r.Method == http.MethodPost && strings.HasSuffix(p, "/git-upload-pack"): return gitReq{gitUploadPack, githttp.UploadPack, strings.TrimSuffix(p, "/git-upload-pack")}, true case r.Method == http.MethodPost && strings.HasSuffix(p, "/git-receive-pack"): return gitReq{gitReceivePack, githttp.ReceivePack, strings.TrimSuffix(p, "/git-receive-pack")}, true } return gitReq{}, false } // handleGitHTTP serves a git smart-HTTP request: authenticate, resolve the repo, // authorize (read for clone/fetch, write for push), then run the protocol. func (s *Server) handleGitHTTP(w http.ResponseWriter, r *http.Request, ep gitReq) { user, clean, ok := parseUpspinPath(ep.urlPath) if !ok { s.notFound(w) return } authUser, ok := s.authenticate(w, r) if !ok { return // 401 already written } repoPath, rest, isRepo, err := gitrepo.Locate(s.store, user, clean) if err != nil { s.storeErr(w, err) return } // Endpoints live only at a repo root. if !isRepo || rest != "" { s.denyRead(w, r, authUser) return } writing := ep.service == githttp.ReceivePack if writing { // Push always requires an authenticated, authorized writer. if authUser == "" { s.challenge(w) return } can, err := s.authz.CanWrite(authUser, repoPath) if err != nil { s.serverErr(w, err) return } if !can { // The caller is known; distinguish "can't read" (hide it) from // "can read but not write" (explicit 403). readable, rerr := s.authz.CanRead(authUser, repoPath) if rerr != nil { s.serverErr(w, rerr) return } if !readable { s.notFound(w) return } http.Error(w, "403 forbidden: no write access", http.StatusForbidden) return } } else { can, err := s.authz.CanRead(authUser, repoPath) if err != nil { s.serverErr(w, err) return } if !can { s.denyRead(w, r, authUser) return } } switch ep.kind { case gitInfoRefs: storer, release, err := s.repos.GetStorer(repoPath) if err != nil { s.storeErr(w, err) return } defer release() if err := githttp.InfoRefs(w, storer, ep.service); err != nil { log.Printf("gitweb: info/refs %s: %v", repoPath, err) } case gitUploadPack: // upload-pack streams the packfile from the storer for the whole // response; the deferred release keeps the repo's on-disk backing // alive until ServeUploadPack returns. storer, release, err := s.repos.GetStorer(repoPath) if err != nil { s.storeErr(w, err) return } defer release() if err := githttp.ServeUploadPack(w, r, storer); err != nil { log.Printf("gitweb: upload-pack %s: %v", repoPath, err) } case gitReceivePack: req, err := githttp.DecodeReceivePack(r) if errors.Is(err, githttp.ErrProbe) { // git checking the endpoint before streaming a large push. if err := githttp.WriteReceivePackResult(w, nil); err != nil { log.Printf("gitweb: receive-pack probe %s: %v", repoPath, err) } return } if err != nil { log.Printf("gitweb: receive-pack decode %s: %v", repoPath, err) var tooBig *http.MaxBytesError if errors.As(err, &tooBig) { http.Error(w, fmt.Sprintf("push too large: the limit is %d MiB", tooBig.Limit>>20), http.StatusRequestEntityTooLarge) return } http.Error(w, "malformed receive-pack request: "+err.Error(), http.StatusBadRequest) return } rs, err := s.pusher.ReceivePack(r.Context(), repoPath, req) if err != nil { log.Printf("gitweb: receive-pack %s: %v", repoPath, err) if rs == nil { s.serverErr(w, err) return } // rs carries per-ref failure detail; report it to the client. } if err := githttp.WriteReceivePackResult(w, rs); err != nil { log.Printf("gitweb: receive-pack result %s: %v", repoPath, err) } } } // cleanRest cleans a URL remainder into an upspin path suffix beginning with // "/". path.Clean collapses any ".." so it cannot escape the user's tree. func cleanRest(rest string) string { return path.Clean("/" + rest) }