git@cosmocr.at/gitweb.git @ main · summary refs log
main / internal / server / githttp.go /
internal/server/githttp.go · 5.1 KiB · raw
view: SourceMarkdown ☑ force

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