this repo has no description
1package routes
2
3import (
4 "compress/gzip"
5 "errors"
6 "fmt"
7 "html/template"
8 "log"
9 "net/http"
10 "os"
11 "path/filepath"
12 "sort"
13 "strconv"
14 "strings"
15 "time"
16
17 comatproto "github.com/bluesky-social/indigo/api/atproto"
18 lexutil "github.com/bluesky-social/indigo/lex/util"
19 "github.com/dustin/go-humanize"
20 "github.com/go-chi/chi/v5"
21 "github.com/go-git/go-git/v5/plumbing"
22 "github.com/google/uuid"
23 "github.com/gorilla/sessions"
24 shbild "github.com/icyphox/bild/api/bild"
25 "github.com/icyphox/bild/config"
26 "github.com/icyphox/bild/db"
27 "github.com/icyphox/bild/git"
28 "github.com/icyphox/bild/routes/auth"
29 "github.com/russross/blackfriday/v2"
30 "golang.org/x/crypto/ssh"
31)
32
33type Handle struct {
34 c *config.Config
35 t *template.Template
36 s *sessions.CookieStore
37 db *db.DB
38 auth *auth.Auth
39}
40
41func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
42 user := chi.URLParam(r, "user")
43 path := filepath.Join(h.c.Repo.ScanPath, user)
44 dirs, err := os.ReadDir(path)
45 if err != nil {
46 h.Write500(w)
47 log.Printf("reading scan path: %s", err)
48 return
49 }
50
51 type info struct {
52 DisplayName, Name, Desc, Idle string
53 d time.Time
54 }
55
56 infos := []info{}
57
58 for _, dir := range dirs {
59 name := dir.Name()
60 if !dir.IsDir() || h.isIgnored(name) || h.isUnlisted(name) {
61 continue
62 }
63
64 gr, err := git.Open(path, "")
65 if err != nil {
66 log.Println(err)
67 continue
68 }
69
70 c, err := gr.LastCommit()
71 if err != nil {
72 h.Write500(w)
73 log.Println(err)
74 return
75 }
76
77 infos = append(infos, info{
78 DisplayName: getDisplayName(name),
79 Name: name,
80 Desc: getDescription(path),
81 Idle: humanize.Time(c.Author.When),
82 d: c.Author.When,
83 })
84 }
85
86 sort.Slice(infos, func(i, j int) bool {
87 return infos[j].d.Before(infos[i].d)
88 })
89
90 data := make(map[string]interface{})
91 data["meta"] = h.c.Meta
92 data["info"] = infos
93
94 if err := h.t.ExecuteTemplate(w, "index", data); err != nil {
95 log.Println(err)
96 return
97 }
98}
99
100func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
101 name := uniqueName(r)
102 if h.isIgnored(name) {
103 h.Write404(w)
104 return
105 }
106
107 name = filepath.Clean(name)
108 path := filepath.Join(h.c.Repo.ScanPath, name)
109
110 gr, err := git.Open(path, "")
111 if err != nil {
112 if errors.Is(err, plumbing.ErrReferenceNotFound) {
113 h.t.ExecuteTemplate(w, "repo/empty", nil)
114 return
115 } else {
116 h.Write404(w)
117 return
118 }
119 }
120 commits, err := gr.Commits()
121 if err != nil {
122 h.Write500(w)
123 log.Println(err)
124 return
125 }
126
127 var readmeContent template.HTML
128 for _, readme := range h.c.Repo.Readme {
129 ext := filepath.Ext(readme)
130 content, _ := gr.FileContent(readme)
131 if len(content) > 0 {
132 switch ext {
133 case ".md", ".mkd", ".markdown":
134 unsafe := blackfriday.Run(
135 []byte(content),
136 blackfriday.WithExtensions(blackfriday.CommonExtensions),
137 )
138 html := sanitize(unsafe)
139 readmeContent = template.HTML(html)
140 default:
141 safe := sanitize([]byte(content))
142 readmeContent = template.HTML(
143 fmt.Sprintf(`<pre>%s</pre>`, safe),
144 )
145 }
146 break
147 }
148 }
149
150 if readmeContent == "" {
151 log.Printf("no readme found for %s", name)
152 }
153
154 mainBranch, err := gr.FindMainBranch(h.c.Repo.MainBranch)
155 if err != nil {
156 h.Write500(w)
157 log.Println(err)
158 return
159 }
160
161 if len(commits) >= 3 {
162 commits = commits[:3]
163 }
164
165 data := make(map[string]any)
166 data["name"] = name
167 data["displayname"] = getDisplayName(name)
168 data["ref"] = mainBranch
169 data["readme"] = readmeContent
170 data["commits"] = commits
171 data["desc"] = getDescription(path)
172 data["servername"] = h.c.Server.Name
173 data["meta"] = h.c.Meta
174 data["gomod"] = isGoModule(gr)
175
176 if err := h.t.ExecuteTemplate(w, "repo/repo", data); err != nil {
177 log.Println(err)
178 return
179 }
180
181 return
182}
183
184func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
185 name := uniqueName(r)
186 if h.isIgnored(name) {
187 h.Write404(w)
188 return
189 }
190 treePath := chi.URLParam(r, "*")
191 ref := chi.URLParam(r, "ref")
192
193 name = filepath.Clean(name)
194 path := filepath.Join(h.c.Repo.ScanPath, name)
195 gr, err := git.Open(path, ref)
196 if err != nil {
197 h.Write404(w)
198 return
199 }
200
201 files, err := gr.FileTree(treePath)
202 if err != nil {
203 h.Write500(w)
204 log.Println(err)
205 return
206 }
207
208 data := make(map[string]any)
209 data["name"] = name
210 data["displayname"] = getDisplayName(name)
211 data["ref"] = ref
212 data["parent"] = treePath
213 data["desc"] = getDescription(path)
214 data["dotdot"] = filepath.Dir(treePath)
215
216 h.listFiles(files, data, w)
217 return
218}
219
220func (h *Handle) 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 := uniqueName(r)
227
228 if h.isIgnored(name) {
229 h.Write404(w)
230 return
231 }
232 treePath := chi.URLParam(r, "*")
233 ref := chi.URLParam(r, "ref")
234
235 name = filepath.Clean(name)
236 path := filepath.Join(h.c.Repo.ScanPath, name)
237 gr, err := git.Open(path, ref)
238 if err != nil {
239 h.Write404(w)
240 return
241 }
242
243 contents, err := gr.FileContent(treePath)
244 if err != nil {
245 h.Write500(w)
246 return
247 }
248 data := make(map[string]any)
249 data["name"] = name
250 data["displayname"] = getDisplayName(name)
251 data["ref"] = ref
252 data["desc"] = getDescription(path)
253 data["path"] = treePath
254
255 safe := sanitize([]byte(contents))
256
257 if raw {
258 h.showRaw(string(safe), w)
259 } else {
260 if h.c.Meta.SyntaxHighlight == "" {
261 h.showFile(string(safe), data, w)
262 } else {
263 h.showFileWithHighlight(treePath, string(safe), data, w)
264 }
265 }
266}
267
268func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
269 name := uniqueName(r)
270 if h.isIgnored(name) {
271 h.Write404(w)
272 return
273 }
274
275 file := chi.URLParam(r, "file")
276
277 // TODO: extend this to add more files compression (e.g.: xz)
278 if !strings.HasSuffix(file, ".tar.gz") {
279 h.Write404(w)
280 return
281 }
282
283 ref := strings.TrimSuffix(file, ".tar.gz")
284
285 // This allows the browser to use a proper name for the file when
286 // downloading
287 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
288 setContentDisposition(w, filename)
289 setGZipMIME(w)
290
291 path := filepath.Join(h.c.Repo.ScanPath, name)
292 gr, err := git.Open(path, ref)
293 if err != nil {
294 h.Write404(w)
295 return
296 }
297
298 gw := gzip.NewWriter(w)
299 defer gw.Close()
300
301 prefix := fmt.Sprintf("%s-%s", name, ref)
302 err = gr.WriteTar(gw, prefix)
303 if err != nil {
304 // once we start writing to the body we can't report error anymore
305 // so we are only left with printing the error.
306 log.Println(err)
307 return
308 }
309
310 err = gw.Flush()
311 if err != nil {
312 // once we start writing to the body we can't report error anymore
313 // so we are only left with printing the error.
314 log.Println(err)
315 return
316 }
317}
318
319func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
320 name := uniqueName(r)
321 if h.isIgnored(name) {
322 h.Write404(w)
323 return
324 }
325 ref := chi.URLParam(r, "ref")
326
327 path := filepath.Join(h.c.Repo.ScanPath, name)
328 gr, err := git.Open(path, ref)
329 if err != nil {
330 h.Write404(w)
331 return
332 }
333
334 commits, err := gr.Commits()
335 if err != nil {
336 h.Write500(w)
337 log.Println(err)
338 return
339 }
340
341 data := make(map[string]interface{})
342 data["commits"] = commits
343 data["meta"] = h.c.Meta
344 data["name"] = name
345 data["displayname"] = getDisplayName(name)
346 data["ref"] = ref
347 data["desc"] = getDescription(path)
348 data["log"] = true
349
350 if err := h.t.ExecuteTemplate(w, "repo/log", data); err != nil {
351 log.Println(err)
352 return
353 }
354}
355
356func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
357 name := uniqueName(r)
358 if h.isIgnored(name) {
359 h.Write404(w)
360 return
361 }
362 ref := chi.URLParam(r, "ref")
363
364 path := filepath.Join(h.c.Repo.ScanPath, name)
365 gr, err := git.Open(path, ref)
366 if err != nil {
367 h.Write404(w)
368 return
369 }
370
371 diff, err := gr.Diff()
372 if err != nil {
373 h.Write500(w)
374 log.Println(err)
375 return
376 }
377
378 data := make(map[string]interface{})
379
380 data["commit"] = diff.Commit
381 data["stat"] = diff.Stat
382 data["diff"] = diff.Diff
383 data["meta"] = h.c.Meta
384 data["name"] = name
385 data["displayname"] = getDisplayName(name)
386 data["ref"] = ref
387 data["desc"] = getDescription(path)
388
389 if err := h.t.ExecuteTemplate(w, "repo/commit", data); err != nil {
390 log.Println(err)
391 return
392 }
393}
394
395func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) {
396 name := chi.URLParam(r, "name")
397 if h.isIgnored(name) {
398 h.Write404(w)
399 return
400 }
401
402 path := filepath.Join(h.c.Repo.ScanPath, name)
403 gr, err := git.Open(path, "")
404 if err != nil {
405 h.Write404(w)
406 return
407 }
408
409 tags, err := gr.Tags()
410 if err != nil {
411 // Non-fatal, we *should* have at least one branch to show.
412 log.Println(err)
413 }
414
415 branches, err := gr.Branches()
416 if err != nil {
417 log.Println(err)
418 h.Write500(w)
419 return
420 }
421
422 data := make(map[string]interface{})
423
424 data["meta"] = h.c.Meta
425 data["name"] = name
426 data["displayname"] = getDisplayName(name)
427 data["branches"] = branches
428 data["tags"] = tags
429 data["desc"] = getDescription(path)
430
431 if err := h.t.ExecuteTemplate(w, "repo/refs", data); err != nil {
432 log.Println(err)
433 return
434 }
435}
436
437func (h *Handle) ServeStatic(w http.ResponseWriter, r *http.Request) {
438 f := chi.URLParam(r, "file")
439 f = filepath.Clean(filepath.Join(h.c.Dirs.Static, f))
440
441 http.ServeFile(w, r, f)
442}
443
444func (h *Handle) Login(w http.ResponseWriter, r *http.Request) {
445 switch r.Method {
446 case http.MethodGet:
447 if err := h.t.ExecuteTemplate(w, "user/login", nil); err != nil {
448 log.Println(err)
449 return
450 }
451 case http.MethodPost:
452 username := r.FormValue("username")
453 appPassword := r.FormValue("app_password")
454
455 atSession, err := h.auth.CreateInitialSession(w, r, username, appPassword)
456 if err != nil {
457 h.WriteOOBNotice(w, "login", "Invalid username or app password.")
458 log.Printf("creating initial session: %s", err)
459 return
460 }
461
462 err = h.auth.StoreSession(r, w, &atSession, nil)
463 if err != nil {
464 h.WriteOOBNotice(w, "login", "Failed to store session.")
465 log.Printf("storing session: %s", err)
466 return
467 }
468
469 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
470 http.Redirect(w, r, "/", http.StatusPermanentRedirect)
471 return
472 }
473}
474
475func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
476 session, _ := h.s.Get(r, "bild-session")
477 did := session.Values["did"].(string)
478
479 switch r.Method {
480 case http.MethodGet:
481 keys, err := h.db.GetPublicKeys(did)
482 if err != nil {
483 h.WriteOOBNotice(w, "keys", "Failed to list keys. Try again later.")
484 log.Println(err)
485 return
486 }
487
488 data := make(map[string]interface{})
489 data["keys"] = keys
490 if err := h.t.ExecuteTemplate(w, "settings/keys", data); err != nil {
491 log.Println(err)
492 return
493 }
494 case http.MethodPut:
495 key := r.FormValue("key")
496 name := r.FormValue("name")
497 client, _ := h.auth.AuthorizedClient(r)
498
499 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
500 if err != nil {
501 h.WriteOOBNotice(w, "keys", "Invalid public key. Check your formatting and try again.")
502 log.Printf("parsing public key: %s", err)
503 return
504 }
505
506 if err := h.db.AddPublicKey(did, name, key); err != nil {
507 h.WriteOOBNotice(w, "keys", "Failed to add key.")
508 log.Printf("adding public key: %s", err)
509 return
510 }
511
512 // store in pds too
513 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
514 Collection: "sh.bild.publicKey",
515 Repo: did,
516 Rkey: uuid.New().String(),
517 Record: &lexutil.LexiconTypeDecoder{Val: &shbild.PublicKey{
518 Created: time.Now().String(),
519 Key: key,
520 Name: name,
521 }},
522 })
523
524 // invalid record
525 if err != nil {
526 h.WriteOOBNotice(w, "keys", "Invalid inputs. Check your formatting and try again.")
527 log.Printf("failed to create record: %s", err)
528 return
529 }
530
531 log.Println("created atproto record: ", resp.Uri)
532
533 h.WriteOOBNotice(w, "keys", "Key added!")
534 return
535 }
536}
537
538func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
539 session, _ := h.s.Get(r, "bild-session")
540 did := session.Values["did"].(string)
541 handle := session.Values["handle"].(string)
542
543 switch r.Method {
544 case http.MethodGet:
545 if err := h.t.ExecuteTemplate(w, "repo/new", nil); err != nil {
546 log.Println(err)
547 return
548 }
549 case http.MethodPut:
550 name := r.FormValue("name")
551 description := r.FormValue("description")
552
553 repoPath := filepath.Join(h.c.Repo.ScanPath, handle, name)
554 err := git.InitBare(repoPath)
555 if err != nil {
556 h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
557 return
558 }
559
560 // For use by repoguard
561 didPath := filepath.Join(repoPath, "did")
562 err = os.WriteFile(didPath, []byte(did), 0644)
563 if err != nil {
564 h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
565 return
566 }
567
568 err = h.db.AddRepo(did, name, description)
569 if err != nil {
570 h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
571 return
572 }
573
574 w.Header().Set("HX-Redirect", fmt.Sprintf("/@example.com/%s", name))
575 w.WriteHeader(http.StatusOK)
576 }
577}