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