/
git_server.go
269 lines (230 loc) · 7.46 KB
/
git_server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
package gitserver
import (
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
"go.uber.org/zap"
)
func init() {
caddy.RegisterModule(GitServer{})
httpcaddyfile.RegisterHandlerDirective("git_server", parseCaddyfile)
}
type GitServer struct {
// Git http protocol to use: 'dumb' or 'smart' or 'both' (default)
// Note this doesn't actually do anything currently, only the dumb protocol is implemented.
Protocol string `json:"protocol,omitempty"`
// Path to directory containing bare git repos (<repo>.git)
Root string `json:"root,omitempty"`
// Enable repo browser
Browse bool `json:"browse,omitempty"`
TemplateDir string `json:"template_dir,omitempty"`
// If IgnorePrefix is defined we strip it from the URL path
IgnorePrefix string `json:"ignore_prefix,omitempty"`
// Mirror a git repo
// Mirror bool `json:"mirror,omitempty"`
// MirrorRemotes []string
// File server module that serves static git files
// FileServerRaw json.RawMessage `json:"file_server,omitempty" caddy:"namespace=http.handlers inline_key=handler"`
FileServer *fileserver.FileServer `json:"-"`
// This is a list of relative paths to repositories in the root directory.
// If set, the IgnorePrefix is stripped
repositories []string
repositoriesLastModified time.Time
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
func (GitServer) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.git_server",
New: func() caddy.Module { return new(GitServer) },
}
}
// Unmarshal caddyfile directive into a GitServer
func (gsrv *GitServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
// Check if we have optional "browse" on the end of 'git_server' directive
args := d.RemainingArgs()
switch len(args) {
case 0:
case 1:
if args[0] != "browse" {
return d.ArgErr()
}
gsrv.Browse = true
default:
return d.ArgErr()
}
// Loop over remaining options
for d.NextBlock(0) {
switch d.Val() {
case "protocol":
if d.NextArg() {
if d.Val() == "dumb" || d.Val() == "smart" || d.Val() == "both" {
gsrv.Protocol = d.Val()
} else {
return d.ArgErr()
}
} else {
return d.ArgErr()
}
case "root":
if !d.AllArgs(&gsrv.Root) {
return d.ArgErr()
}
case "browse":
gsrv.Browse = true
case "template_dir":
if !d.AllArgs(&gsrv.TemplateDir) {
return d.ArgErr()
}
// case "mirror":
// gsrv.Mirror = true
// if d.NextArg() {
// gsrv.MirrorRemotes = append(gsrv.MirrorRemotes, d.Val())
// } else {
// return d.ArgErr()
// }
case "ignore_prefix":
if !d.AllArgs(&gsrv.IgnorePrefix) {
return d.ArgErr()
}
}
}
}
return nil
}
func (gsrv *GitServer) Provision(ctx caddy.Context) error {
// Support both protocol by default
if gsrv.Protocol == "" {
gsrv.Protocol = "both"
}
// Serve the set root by default
if gsrv.Root == "" {
gsrv.Root = "{http.vars.root}"
}
// Configure and load file_server submodule
// if gsrv.FileServerRaw == nil {
// // Configure a default file_server if one is not configured
// gsrv.FileServerRaw = []byte("{\"handler\":\"file_server\"}")
// fmt.Printf("using default file_server: %s\n", string(gsrv.FileServerRaw))
// } else {
// fmt.Printf("using file_server: %s\n", string(gsrv.FileServerRaw))
// }
// mod, err := ctx.LoadModule(gsrv, "FileServerRaw")
fileServerRaw := []byte("{\"root\":\"" + gsrv.Root + "\"}")
mod, err := ctx.LoadModuleByID("http.handlers.file_server", fileServerRaw)
if err != nil {
return fmt.Errorf("loading file_server module: %v", err)
}
gsrv.FileServer = mod.(*fileserver.FileServer)
// Setup a logger to use
gsrv.logger = ctx.Logger()
return nil
}
func (gsrv GitServer) Validate() error {
fmt.Println(gsrv)
return nil
}
// ServeHTTP implements http.MiddlewareHandler
func (gsrv *GitServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
// Get repo path on disk
repoPath, err := gsrv.getRepoPath(r)
if err == nil {
// fmt.Println("found repo", repoPath)
// Here we try to detect git clients and forward them on to a special git protocol handler.
// All requests that enter the git client handler will return a response.
if r.Header.Get("Git-Protocol") != "" || strings.HasPrefix(r.UserAgent(), "git") {
gsrv.logger.Debug("handling git client",
zap.String("git_protocol", r.Header.Get("Git-Protocol")),
zap.String("git_client", r.UserAgent()),
zap.String("req_path", r.RequestURI),
zap.String("repo_path", repoPath),
)
return gsrv.serveGitClient(repoPath, w, r, next)
}
// If browse is enabled we check if the requested repo exists and pawn it off to a browser handler.
if gsrv.Browse {
// Redirect /<repo>.git to /<repo>
requestPath := strings.TrimSuffix(r.URL.Path, "/")
if strings.HasSuffix(requestPath, ".git") {
http.Redirect(w, r, strings.TrimSuffix(requestPath, ".git"), http.StatusPermanentRedirect)
return nil
}
// Pass it on to the browse handler
gsrv.logger.Debug("handling web browser",
zap.String("repo_path", repoPath),
zap.String("req_path", r.URL.Path))
return gsrv.serveGitBrowser(repoPath, w, r, next)
}
}
// We pass on the request if it doesn't contain a git repo
return next.ServeHTTP(w, r)
}
// Parse caddyfile into middleware
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var gsrv GitServer
err := gsrv.UnmarshalCaddyfile(h.Dispenser)
return &gsrv, err
}
func (gsrv *GitServer) getRepoPath(r *http.Request) (string, error) {
// Update repository list
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
root := repl.ReplaceAll(gsrv.Root, ".")
gsrv.updateRepositories(root)
// Check if request path begins with a repo path
for _, path := range gsrv.repositories {
if strings.HasPrefix(strings.TrimPrefix(r.URL.Path, "/"), path) {
return filepath.Join(root, path) + ".git", nil
}
}
return "", fmt.Errorf("repo not found")
}
func (gsrv *GitServer) updateRepositories(root string) {
rootDir, err := os.Stat(root)
if err != nil {
fmt.Println("What? - updateRepositories()", err)
return
}
// If the root has been modified since last time, update the repository list
modTime := rootDir.ModTime()
if modTime.After(gsrv.repositoriesLastModified) {
var newRepos []string
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
fmt.Println("walk error", err)
return err
}
// Right now we determine a git repo by a directory with the '.git' suffix
if d.IsDir() && filepath.Ext(path) == ".git" {
// fmt.Println("Found repo", path)
// Strip root from path
path = strings.TrimPrefix(path, root)
// Strip '/' prefix from path
path = strings.TrimPrefix(path, "/")
// Strip .git suffix
path = strings.TrimSuffix(path, ".git")
newRepos = append(newRepos, path)
return fs.SkipDir
}
return nil
})
// Update git server
gsrv.repositories = newRepos
gsrv.repositoriesLastModified = modTime
}
}
// Interface Guards
var (
_ caddy.Provisioner = (*GitServer)(nil)
_ caddyhttp.MiddlewareHandler = (*GitServer)(nil)
_ caddyfile.Unmarshaler = (*GitServer)(nil)
)