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