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
180func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request) {
181 name := r.PathValue("name")
182 if d.isIgnored(name) {
183 d.Write404(w)
184 return
185 }
186 treePath := r.PathValue("rest")
187 ref := r.PathValue("ref")
188
189 name = filepath.Clean(name)
190 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
191 if err != nil {
192 log.Printf("securejoin error: %v", err)
193 d.Write404(w)
194 return
195 }
196 gr, err := git.Open(path, ref)
197 if err != nil {
198 d.Write404(w)
199 return
200 }
201
202 files, err := gr.FileTree(treePath)
203 if err != nil {
204 d.Write500(w)
205 log.Println(err)
206 return
207 }
208
209 data := make(map[string]any)
210 data["name"] = name
211 data["displayname"] = getDisplayName(name)
212 data["ref"] = ref
213 data["parent"] = treePath
214 data["desc"] = getDescription(path)
215 data["dotdot"] = filepath.Dir(treePath)
216
217 d.listFiles(files, data, w)
218}
219
220func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {
221 var raw bool
222 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
223 raw = rawParam
224 }
225
226 name := r.PathValue("name")
227 if d.isIgnored(name) {
228 d.Write404(w)
229 return
230 }
231 treePath := r.PathValue("rest")
232 ref := r.PathValue("ref")
233
234 name = filepath.Clean(name)
235 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
236 if err != nil {
237 log.Printf("securejoin error: %v", err)
238 d.Write404(w)
239 return
240 }
241
242 gr, err := git.Open(path, ref)
243 if err != nil {
244 d.Write404(w)
245 return
246 }
247
248 contents, err := gr.FileContent(treePath)
249 if err != nil {
250 d.Write500(w)
251 return
252 }
253 data := make(map[string]any)
254 data["name"] = name
255 data["displayname"] = getDisplayName(name)
256 data["ref"] = ref
257 data["desc"] = getDescription(path)
258 data["path"] = treePath
259
260 if raw {
261 d.showRaw(contents, w)
262 } else {
263 if d.c.Meta.SyntaxHighlight == "" {
264 d.showFile(contents, data, w)
265 } else {
266 d.showFileWithHighlight(treePath, contents, data, w)
267 }
268 }
269}
270
271func (d *deps) Archive(w http.ResponseWriter, r *http.Request) {
272 name := r.PathValue("name")
273 if d.isIgnored(name) {
274 d.Write404(w)
275 return
276 }
277
278 file := r.PathValue("file")
279
280 // TODO: extend this to add more files compression (e.g.: xz)
281 if !strings.HasSuffix(file, ".tar.gz") {
282 d.Write404(w)
283 return
284 }
285
286 ref := strings.TrimSuffix(file, ".tar.gz")
287
288 // This allows the browser to use a proper name for the file when
289 // downloading
290 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
291 setContentDisposition(w, filename)
292 setGZipMIME(w)
293
294 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
295 if err != nil {
296 log.Printf("securejoin error: %v", err)
297 d.Write404(w)
298 return
299 }
300
301 gr, err := git.Open(path, ref)
302 if err != nil {
303 d.Write404(w)
304 return
305 }
306
307 gw := gzip.NewWriter(w)
308 defer func() { _ = gw.Close() }()
309
310 prefix := fmt.Sprintf("%s-%s", name, ref)
311 err = gr.WriteTar(gw, prefix)
312 if err != nil {
313 // once we start writing to the body we can't report error anymore
314 // so we are only left with printing the error.
315 log.Println(err)
316 return
317 }
318
319 err = gw.Flush()
320 if err != nil {
321 // once we start writing to the body we can't report error anymore
322 // so we are only left with printing the error.
323 log.Println(err)
324 return
325 }
326}
327
328func (d *deps) Log(w http.ResponseWriter, r *http.Request) {
329 name := r.PathValue("name")
330 if d.isIgnored(name) {
331 d.Write404(w)
332 return
333 }
334 ref := r.PathValue("ref")
335
336 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
337 if err != nil {
338 log.Printf("securejoin error: %v", err)
339 d.Write404(w)
340 return
341 }
342
343 gr, err := git.Open(path, ref)
344 if err != nil {
345 d.Write404(w)
346 return
347 }
348
349 commits, err := gr.Commits()
350 if err != nil {
351 d.Write500(w)
352 log.Println(err)
353 return
354 }
355
356 tpath := filepath.Join(d.c.Dirs.Templates, "*")
357 t := template.Must(template.ParseGlob(tpath))
358
359 data := make(map[string]interface{})
360 data["commits"] = commits
361 data["meta"] = d.c.Meta
362 data["name"] = name
363 data["displayname"] = getDisplayName(name)
364 data["ref"] = ref
365 data["desc"] = getDescription(path)
366 data["log"] = true
367
368 if err := t.ExecuteTemplate(w, "log", data); err != nil {
369 log.Println(err)
370 return
371 }
372}
373
374func (d *deps) Diff(w http.ResponseWriter, r *http.Request) {
375 name := r.PathValue("name")
376 if d.isIgnored(name) {
377 d.Write404(w)
378 return
379 }
380 ref := r.PathValue("ref")
381
382 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
383 if err != nil {
384 log.Printf("securejoin error: %v", err)
385 d.Write404(w)
386 return
387 }
388 gr, err := git.Open(path, ref)
389 if err != nil {
390 d.Write404(w)
391 return
392 }
393
394 diff, err := gr.Diff()
395 if err != nil {
396 d.Write500(w)
397 log.Println(err)
398 return
399 }
400
401 tpath := filepath.Join(d.c.Dirs.Templates, "*")
402 t := template.Must(template.ParseGlob(tpath))
403
404 data := make(map[string]interface{})
405
406 data["commit"] = diff.Commit
407 data["stat"] = diff.Stat
408 data["diff"] = diff.Diff
409 data["meta"] = d.c.Meta
410 data["name"] = name
411 data["displayname"] = getDisplayName(name)
412 data["ref"] = ref
413 data["desc"] = getDescription(path)
414
415 if err := t.ExecuteTemplate(w, "commit", data); err != nil {
416 log.Println(err)
417 return
418 }
419}
420
421func (d *deps) Refs(w http.ResponseWriter, r *http.Request) {
422 name := r.PathValue("name")
423 if d.isIgnored(name) {
424 d.Write404(w)
425 return
426 }
427
428 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
429 if err != nil {
430 log.Printf("securejoin error: %v", err)
431 d.Write404(w)
432 return
433 }
434
435 gr, err := git.Open(path, "")
436 if err != nil {
437 d.Write404(w)
438 return
439 }
440
441 tags, err := gr.Tags()
442 if err != nil {
443 // Non-fatal, we *should* have at least one branch to show.
444 log.Println(err)
445 }
446
447 branches, err := gr.Branches()
448 if err != nil {
449 log.Println(err)
450 d.Write500(w)
451 return
452 }
453
454 tpath := filepath.Join(d.c.Dirs.Templates, "*")
455 t := template.Must(template.ParseGlob(tpath))
456
457 data := make(map[string]interface{})
458
459 data["meta"] = d.c.Meta
460 data["name"] = name
461 data["displayname"] = getDisplayName(name)
462 data["branches"] = branches
463 data["tags"] = tags
464 data["desc"] = getDescription(path)
465
466 if err := t.ExecuteTemplate(w, "refs", data); err != nil {
467 log.Println(err)
468 return
469 }
470}
471
472func (d *deps) ServeStatic(w http.ResponseWriter, r *http.Request) {
473 f := r.PathValue("file")
474 f = filepath.Clean(f)
475 f, err := securejoin.SecureJoin(d.c.Dirs.Static, f)
476 if err != nil {
477 d.Write404(w)
478 return
479 }
480
481 http.ServeFile(w, r, f)
482}