routes/routes.go (view raw)
1package routes
2
3import (
4 "compress/gzip"
5 "fmt"
6 "html/template"
7 "log"
8 "net/http"
9 "os"
10 "path/filepath"
11 "sort"
12 "strconv"
13 "strings"
14 "time"
15
16 "git.icyphox.sh/legit/config"
17 "git.icyphox.sh/legit/git"
18 securejoin "github.com/cyphar/filepath-securejoin"
19 "github.com/dustin/go-humanize"
20 "github.com/microcosm-cc/bluemonday"
21 "github.com/russross/blackfriday/v2"
22)
23
24type deps struct {
25 c *config.Config
26}
27
28func (d *deps) Index(w http.ResponseWriter, r *http.Request) {
29 dirs, err := os.ReadDir(d.c.Repo.ScanPath)
30 if err != nil {
31 d.Write500(w)
32 log.Printf("reading scan path: %s", err)
33 return
34 }
35
36 type info struct {
37 DisplayName, Name, Desc, Idle string
38 d time.Time
39 }
40
41 infos := []info{}
42
43 for _, dir := range dirs {
44 name := dir.Name()
45 if !dir.IsDir() || d.isIgnored(name) || d.isUnlisted(name) {
46 continue
47 }
48
49 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
50 if err != nil {
51 log.Printf("securejoin error: %v", err)
52 d.Write404(w)
53 return
54 }
55
56 gr, err := git.Open(path, "")
57 if err != nil {
58 log.Println(err)
59 continue
60 }
61
62 c, err := gr.LastCommit()
63 if err != nil {
64 d.Write500(w)
65 log.Println(err)
66 return
67 }
68
69 infos = append(infos, info{
70 DisplayName: getDisplayName(name),
71 Name: name,
72 Desc: getDescription(path),
73 Idle: humanize.Time(c.Commit().Author.When),
74 d: c.Commit().Author.When,
75 })
76 }
77
78 sort.Slice(infos, func(i, j int) bool {
79 return infos[j].d.Before(infos[i].d)
80 })
81
82 tpath := filepath.Join(d.c.Dirs.Templates, "*")
83 t := template.Must(template.ParseGlob(tpath))
84
85 data := make(map[string]interface{})
86 data["meta"] = d.c.Meta
87 data["info"] = infos
88
89 if err := t.ExecuteTemplate(w, "index", data); err != nil {
90 log.Println(err)
91 return
92 }
93}
94
95func (d *deps) RepoIndex(w http.ResponseWriter, r *http.Request) {
96 name := r.PathValue("name")
97 if d.isIgnored(name) {
98 d.Write404(w)
99 return
100 }
101 name = filepath.Clean(name)
102 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
103 if err != nil {
104 log.Printf("securejoin error: %v", err)
105 d.Write404(w)
106 return
107 }
108
109 gr, err := git.Open(path, "")
110 if err != nil {
111 d.Write404(w)
112 return
113 }
114
115 commits, err := gr.Commits()
116 if err != nil {
117 d.Write500(w)
118 log.Println(err)
119 return
120 }
121
122 var readmeContent template.HTML
123 for _, readme := range d.c.Repo.Readme {
124 ext := filepath.Ext(readme)
125 content, _ := gr.FileContent(readme)
126 if len(content) > 0 {
127 switch ext {
128 case ".md", ".mkd", ".markdown":
129 unsafe := blackfriday.Run(
130 []byte(content),
131 blackfriday.WithExtensions(blackfriday.CommonExtensions),
132 )
133 html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
134 readmeContent = template.HTML(html)
135 default:
136 safe := bluemonday.UGCPolicy().SanitizeBytes([]byte(content))
137 readmeContent = template.HTML(
138 fmt.Sprintf(`<pre>%s</pre>`, safe),
139 )
140 }
141 break
142 }
143 }
144
145 if readmeContent == "" {
146 log.Printf("no readme found for %s", name)
147 }
148
149 mainBranch, err := gr.FindMainBranch(d.c.Repo.MainBranch)
150 if err != nil {
151 d.Write500(w)
152 log.Println(err)
153 return
154 }
155
156 tpath := filepath.Join(d.c.Dirs.Templates, "*")
157 t := template.Must(template.ParseGlob(tpath))
158
159 if len(commits) >= 3 {
160 commits = commits[:3]
161 }
162
163 data := make(map[string]any)
164 data["name"] = name
165 data["displayname"] = getDisplayName(name)
166 data["ref"] = mainBranch
167 data["readme"] = readmeContent
168 data["commits"] = commits
169 data["desc"] = getDescription(path)
170 data["servername"] = d.c.Server.Name
171 data["meta"] = d.c.Meta
172 data["gomod"] = isGoModule(gr)
173
174 if err := t.ExecuteTemplate(w, "repo", data); err != nil {
175 log.Println(err)
176 return
177 }
178
179 return
180}
181
182func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request) {
183 name := r.PathValue("name")
184 if d.isIgnored(name) {
185 d.Write404(w)
186 return
187 }
188 treePath := r.PathValue("rest")
189 ref := r.PathValue("ref")
190
191 name = filepath.Clean(name)
192 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
193 if err != nil {
194 log.Printf("securejoin error: %v", err)
195 d.Write404(w)
196 return
197 }
198 gr, err := git.Open(path, ref)
199 if err != nil {
200 d.Write404(w)
201 return
202 }
203
204 files, err := gr.FileTree(treePath)
205 if err != nil {
206 d.Write500(w)
207 log.Println(err)
208 return
209 }
210
211 data := make(map[string]any)
212 data["name"] = name
213 data["displayname"] = getDisplayName(name)
214 data["ref"] = ref
215 data["parent"] = treePath
216 data["desc"] = getDescription(path)
217 data["dotdot"] = filepath.Dir(treePath)
218
219 d.listFiles(files, data, w)
220 return
221}
222
223func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {
224 var raw bool
225 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
226 raw = rawParam
227 }
228
229 name := r.PathValue("name")
230 if d.isIgnored(name) {
231 d.Write404(w)
232 return
233 }
234 treePath := r.PathValue("rest")
235 ref := r.PathValue("ref")
236
237 name = filepath.Clean(name)
238 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
239 if err != nil {
240 log.Printf("securejoin error: %v", err)
241 d.Write404(w)
242 return
243 }
244
245 gr, err := git.Open(path, ref)
246 if err != nil {
247 d.Write404(w)
248 return
249 }
250
251 contents, err := gr.FileContent(treePath)
252 if err != nil {
253 d.Write500(w)
254 return
255 }
256 data := make(map[string]any)
257 data["name"] = name
258 data["displayname"] = getDisplayName(name)
259 data["ref"] = ref
260 data["desc"] = getDescription(path)
261 data["path"] = treePath
262
263 if raw {
264 d.showRaw(contents, w)
265 } else {
266 if d.c.Meta.SyntaxHighlight == "" {
267 d.showFile(contents, data, w)
268 } else {
269 d.showFileWithHighlight(treePath, contents, data, w)
270 }
271 }
272}
273
274func (d *deps) Archive(w http.ResponseWriter, r *http.Request) {
275 name := r.PathValue("name")
276 if d.isIgnored(name) {
277 d.Write404(w)
278 return
279 }
280
281 file := r.PathValue("file")
282
283 // TODO: extend this to add more files compression (e.g.: xz)
284 if !strings.HasSuffix(file, ".tar.gz") {
285 d.Write404(w)
286 return
287 }
288
289 ref := strings.TrimSuffix(file, ".tar.gz")
290
291 // This allows the browser to use a proper name for the file when
292 // downloading
293 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
294 setContentDisposition(w, filename)
295 setGZipMIME(w)
296
297 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
298 if err != nil {
299 log.Printf("securejoin error: %v", err)
300 d.Write404(w)
301 return
302 }
303
304 gr, err := git.Open(path, ref)
305 if err != nil {
306 d.Write404(w)
307 return
308 }
309
310 gw := gzip.NewWriter(w)
311 defer gw.Close()
312
313 prefix := fmt.Sprintf("%s-%s", name, ref)
314 err = gr.WriteTar(gw, prefix)
315 if err != nil {
316 // once we start writing to the body we can't report error anymore
317 // so we are only left with printing the error.
318 log.Println(err)
319 return
320 }
321
322 err = gw.Flush()
323 if err != nil {
324 // once we start writing to the body we can't report error anymore
325 // so we are only left with printing the error.
326 log.Println(err)
327 return
328 }
329}
330
331func (d *deps) Log(w http.ResponseWriter, r *http.Request) {
332 name := r.PathValue("name")
333 if d.isIgnored(name) {
334 d.Write404(w)
335 return
336 }
337 ref := r.PathValue("ref")
338
339 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
340 if err != nil {
341 log.Printf("securejoin error: %v", err)
342 d.Write404(w)
343 return
344 }
345
346 gr, err := git.Open(path, ref)
347 if err != nil {
348 d.Write404(w)
349 return
350 }
351
352 commits, err := gr.Commits()
353 if err != nil {
354 d.Write500(w)
355 log.Println(err)
356 return
357 }
358
359 tpath := filepath.Join(d.c.Dirs.Templates, "*")
360 t := template.Must(template.ParseGlob(tpath))
361
362 data := make(map[string]interface{})
363 data["commits"] = commits
364 data["meta"] = d.c.Meta
365 data["name"] = name
366 data["displayname"] = getDisplayName(name)
367 data["ref"] = ref
368 data["desc"] = getDescription(path)
369 data["log"] = true
370
371 if err := t.ExecuteTemplate(w, "log", data); err != nil {
372 log.Println(err)
373 return
374 }
375}
376
377func (d *deps) Diff(w http.ResponseWriter, r *http.Request) {
378 name := r.PathValue("name")
379 if d.isIgnored(name) {
380 d.Write404(w)
381 return
382 }
383 ref := r.PathValue("ref")
384
385 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
386 if err != nil {
387 log.Printf("securejoin error: %v", err)
388 d.Write404(w)
389 return
390 }
391 gr, err := git.Open(path, ref)
392 if err != nil {
393 d.Write404(w)
394 return
395 }
396
397 diff, err := gr.Diff()
398 if err != nil {
399 d.Write500(w)
400 log.Println(err)
401 return
402 }
403
404 tpath := filepath.Join(d.c.Dirs.Templates, "*")
405 t := template.Must(template.ParseGlob(tpath))
406
407 data := make(map[string]interface{})
408
409 data["commit"] = diff.Commit
410 data["stat"] = diff.Stat
411 data["diff"] = diff.Diff
412 data["meta"] = d.c.Meta
413 data["name"] = name
414 data["displayname"] = getDisplayName(name)
415 data["ref"] = ref
416 data["desc"] = getDescription(path)
417
418 if err := t.ExecuteTemplate(w, "commit", data); err != nil {
419 log.Println(err)
420 return
421 }
422}
423
424func (d *deps) Refs(w http.ResponseWriter, r *http.Request) {
425 name := r.PathValue("name")
426 if d.isIgnored(name) {
427 d.Write404(w)
428 return
429 }
430
431 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
432 if err != nil {
433 log.Printf("securejoin error: %v", err)
434 d.Write404(w)
435 return
436 }
437
438 gr, err := git.Open(path, "")
439 if err != nil {
440 d.Write404(w)
441 return
442 }
443
444 tags, err := gr.Tags()
445 if err != nil {
446 // Non-fatal, we *should* have at least one branch to show.
447 log.Println(err)
448 }
449
450 branches, err := gr.Branches()
451 if err != nil {
452 log.Println(err)
453 d.Write500(w)
454 return
455 }
456
457 tpath := filepath.Join(d.c.Dirs.Templates, "*")
458 t := template.Must(template.ParseGlob(tpath))
459
460 data := make(map[string]interface{})
461
462 data["meta"] = d.c.Meta
463 data["name"] = name
464 data["displayname"] = getDisplayName(name)
465 data["branches"] = branches
466 data["tags"] = tags
467 data["desc"] = getDescription(path)
468
469 if err := t.ExecuteTemplate(w, "refs", data); err != nil {
470 log.Println(err)
471 return
472 }
473}
474
475func (d *deps) ServeStatic(w http.ResponseWriter, r *http.Request) {
476 f := r.PathValue("file")
477 f = filepath.Clean(f)
478 f, err := securejoin.SecureJoin(d.c.Dirs.Static, f)
479 if err != nil {
480 d.Write404(w)
481 return
482 }
483
484 http.ServeFile(w, r, f)
485}