package httpgit
import (
"compress/gzip"
"fmt"
"io"
"log"
"net/http"
"os"
"regexp"
"strings"
"nugitea/internal/auth"
"nugitea/internal/gitcmd"
"nugitea/internal/repo"
)
type Handler struct {
Store *repo.Store
Auth auth.Authorizer
}
func New(store *repo.Store, a auth.Authorizer) *Handler {
if a == nil {
a = auth.AllowAll{}
}
return &Handler{Store: store, Auth: a}
}
var pathRE = regexp.MustCompile(`^/([A-Za-z0-9_][A-Za-z0-9_.-]*)\.git/(info/refs|git-upload-pack|git-receive-pack)$`)
var safeProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z=;,._-]+$`)
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m := pathRE.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return
}
repoName, endpoint := m[1], m[2]
if err := repo.ValidateName(repoName); err != nil {
http.NotFound(w, r)
return
}
if !h.Store.Exists(repoName) {
http.NotFound(w, r)
return
}
repoPath, err := h.Store.Path(repoName)
if err != nil {
http.NotFound(w, r)
return
}
switch endpoint {
case "info/refs":
h.infoRefs(w, r, repoName, repoPath)
case "git-upload-pack":
h.service(w, r, repoName, repoPath, "upload-pack", h.Auth.AllowPull)
case "git-receive-pack":
h.service(w, r, repoName, repoPath, "receive-pack", h.Auth.AllowPush)
}
}
func (h *Handler) infoRefs(w http.ResponseWriter, r *http.Request, repoName, repoPath string) {
service := strings.TrimPrefix(r.URL.Query().Get("service"), "git-")
var allowed func(string) bool
switch service {
case "upload-pack":
allowed = h.Auth.AllowPull
case "receive-pack":
allowed = h.Auth.AllowPush
default:
http.Error(w, "smart HTTP only: service must be git-upload-pack or git-receive-pack", http.StatusBadRequest)
return
}
if !allowed(repoName) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
var stdout strings.Builder
var stderr strings.Builder
cmd := gitcmd.New(service, "--stateless-rpc", "--advertise-refs", ".").
WithDir(repoPath).
WithStdio(nil, &stdout, &stderr)
if err := cmd.Run(r.Context()); err != nil {
log.Printf("info/refs %s %s: %v: %s", repoName, service, err, stderr.String())
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
pkt := fmt.Sprintf("# service=git-%s\n", service)
fmt.Fprintf(w, "%04x%s0000", len(pkt)+4, pkt)
io.WriteString(w, stdout.String())
}
func (h *Handler) service(w http.ResponseWriter, r *http.Request, repoName, repoPath, service string, allowed func(string) bool) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !allowed(repoName) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
body := r.Body
if r.Header.Get("Content-Encoding") == "gzip" {
gz, err := gzip.NewReader(r.Body)
if err != nil {
http.Error(w, "bad gzip body", http.StatusBadRequest)
return
}
defer gz.Close()
body = gz
}
env := buildEnv(r)
w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
var stderr strings.Builder
cmd := gitcmd.New(service, "--stateless-rpc", ".").
WithDir(repoPath).
WithEnv(env).
WithStdio(body, w, &stderr)
if err := cmd.Run(r.Context()); err != nil {
log.Printf("%s %s: %v: %s", repoName, service, err, stderr.String())
}
}
func buildEnv(r *http.Request) []string {
env := os.Environ()
if protocol := r.Header.Get("Git-Protocol"); protocol != "" && safeProtocolHeader.MatchString(protocol) {
env = append(env, "GIT_PROTOCOL="+protocol)
}
return env
}