all repos

mugit @ 96929ec

🐮 git server that your cow will love

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

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