all repos

mugit @ 96821ba3e784d93a9effc172c698b5444f02eccc

🐮 git server that your cow will love

mugit/internal/handlers/git.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
fix: limit upload-pack request body to 5 MB, 1 month ago
1
package handlers
2
3
import (
4
	"compress/gzip"
5
	"fmt"
6
	"io"
7
	"log/slog"
8
	"net/http"
9
10
	"olexsmir.xyz/mugit/internal/git"
11
)
12
13
func (h *handlers) infoRefsHandler(w http.ResponseWriter, r *http.Request) {
14
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
15
	if err != nil {
16
		h.gitError(w, http.StatusNotFound, "repository not found")
17
		return
18
	}
19
20
	service := r.URL.Query().Get("service")
21
	gitProtocol := r.Header.Get("Git-Protocol")
22
	switch service {
23
	case "git-upload-pack":
24
		w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
25
		w.Header().Set("Connection", "Keep-Alive")
26
		w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
27
28
		w.WriteHeader(http.StatusOK)
29
		if err := repo.InfoRefs(r.Context(), gitProtocol, w); err != nil {
30
			_ = git.PackError(w, err.Error())
31
			slog.Error("git: info/refs", "err", err)
32
			return
33
		}
34
35
	case "git-receive-pack":
36
		h.receivePackHandler(w, r)
37
38
	default:
39
		h.gitError(w, http.StatusBadRequest, "service unsupported")
40
	}
41
}
42
43
const uploadPackExpectedContentType = "application/x-git-upload-pack-request"
44
45
func (h *handlers) uploadPackHandler(w http.ResponseWriter, r *http.Request) {
46
	gitProtocol := r.Header.Get("Git-Protocol")
47
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
48
	if err != nil {
49
		h.gitError(w, http.StatusNotFound, "repository not found")
50
		return
51
	}
52
53
	contentType := r.Header.Get("Content-Type")
54
	if contentType != uploadPackExpectedContentType {
55
		h.gitError(w, http.StatusUnsupportedMediaType, "provided content type is not supported")
56
		return
57
	}
58
59
	r.Body = http.MaxBytesReader(w, r.Body, 5<<20) // 5 MB limit
60
61
	bodyReader := r.Body
62
	if r.Header.Get("Content-Encoding") == "gzip" {
63
		gzipReader, err := gzip.NewReader(r.Body)
64
		if err != nil {
65
			h.gitError(w, http.StatusInternalServerError, err.Error())
66
			slog.Error("git: failed to create gzip reader", "err", err)
67
			return
68
		}
69
		defer func() { _ = gzipReader.Close() }()
70
		bodyReader = gzipReader
71
	}
72
73
	w.Header().Set("Content-Type", "application/x-git-upload-pack-result")
74
	w.Header().Set("Connection", "Keep-Alive")
75
	w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
76
77
	w.WriteHeader(http.StatusOK)
78
	if err := repo.UploadPack(r.Context(), true, gitProtocol, bodyReader, newFlushWriter(w)); err != nil {
79
		_ = git.PackError(w, err.Error())
80
		slog.Error("git: upload-pack", "err", err)
81
		return
82
	}
83
}
84
85
func (h *handlers) receivePackHandler(w http.ResponseWriter, _ *http.Request) {
86
	h.gitError(w, http.StatusForbidden, "pushes are only supported over ssh")
87
}
88
89
func (h *handlers) archiveHandler(w http.ResponseWriter, r *http.Request) {
90
	name := r.PathValue("name")
91
	ref := h.parseRef(r.PathValue("ref"))
92
93
	repo, err := h.openPublicRepo(name, ref)
94
	if err != nil {
95
		h.write404(w, r.URL.Path, err)
96
		return
97
	}
98
99
	filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
100
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
101
	w.Header().Set("Content-Type", "application/gzip")
102
	w.WriteHeader(http.StatusOK)
103
104
	if err := repo.ArchiveTar(r.Context(), ref, w); err != nil {
105
		slog.Error("git: archive", "ref", ref, "err", err)
106
		return
107
	}
108
}
109
110
func (h *handlers) gitError(w http.ResponseWriter, code int, msg string) {
111
	w.Header().Set("content-type", "text/plain; charset=UTF-8")
112
	w.WriteHeader(code)
113
	_, _ = fmt.Fprintf(w, "%s\n", msg)
114
}
115
116
func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) {
117
	name = git.ResolveName(name)
118
	path, err := git.ResolvePath(h.c.Repo.Dir, name)
119
	if err != nil {
120
		return nil, err
121
	}
122
	return git.OpenPublic(path, ref)
123
}
124
125
type flushWriter struct {
126
	w io.Writer
127
	f http.Flusher
128
}
129
130
func newFlushWriter(w http.ResponseWriter) io.Writer {
131
	f, _ := w.(http.Flusher)
132
	return &flushWriter{w: w, f: f}
133
}
134
135
func (fw *flushWriter) Write(p []byte) (int, error) {
136
	n, err := fw.w.Write(p)
137
	if fw.f != nil {
138
		fw.f.Flush()
139
	}
140
	return n, err
141
}