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(arg string) (*identity.Identity, error) {
444 id, err := syntax.ParseAtIdentifier(arg)
445 if err != nil {
446 return nil, err
447 }
448
449 ctx := context.Background()
450 dir := identity.DefaultDirectory()
451 return dir.Lookup(ctx, *id)
452}
453
454func (h *Handle) Login(w http.ResponseWriter, r *http.Request) {
455 ctx := context.Background()
456 username := r.FormValue("username")
457 appPassword := r.FormValue("app_password")
458
459 resolved, err := resolveIdent(username)
460 if err != nil {
461 http.Error(w, "invalid `handle`", http.StatusBadRequest)
462 return
463 }
464
465 pdsUrl := resolved.PDSEndpoint()
466 client := xrpc.Client{
467 Host: pdsUrl,
468 }
469
470 atSession, err := comatproto.ServerCreateSession(ctx, &client, &comatproto.ServerCreateSession_Input{
471 Identifier: resolved.DID.String(),
472 Password: appPassword,
473 })
474
475 clientSession, _ := h.s.Get(r, "bild-session")
476 clientSession.Values["handle"] = atSession.Handle
477 clientSession.Values["did"] = atSession.Did
478 clientSession.Values["accessJwt"] = atSession.AccessJwt
479 clientSession.Values["refreshJwt"] = atSession.RefreshJwt
480 clientSession.Values["expiry"] = time.Now().Add(time.Hour).String()
481 clientSession.Values["pds"] = pdsUrl
482 clientSession.Values["authenticated"] = true
483
484 err = clientSession.Save(r, w)
485
486 if err != nil {
487 log.Printf("failed to store session for did: %s\n", atSession.Did)
488 log.Println(err)
489 return
490 }
491
492 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
493 http.Redirect(w, r, "/", 302)
494}
495
496func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
497 session, _ := h.s.Get(r, "bild-session")
498 did := session.Values["did"].(string)
499
500 switch r.Method {
501 case http.MethodGet:
502 keys, err := h.db.GetPublicKeys(did)
503 if err != nil {
504 log.Println(err)
505 http.Error(w, "invalid `did`", http.StatusBadRequest)
506 return
507 }
508
509 data := make(map[string]interface{})
510 data["keys"] = keys
511 if err := h.t.ExecuteTemplate(w, "settings/keys", data); err != nil {
512 log.Println(err)
513 return
514 }
515 case http.MethodPut:
516 key := r.FormValue("key")
517 name := r.FormValue("name")
518
519 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
520 if err != nil {
521 h.WriteOOBNotice(w, "keys", "Invalid public key. Check your formatting and try again.")
522 log.Printf("parsing public key: %s", err)
523 return
524 }
525
526 if err := h.db.AddPublicKey(did, name, key); err != nil {
527 h.WriteOOBNotice(w, "keys", "Failed to add key.")
528 log.Printf("adding public key: %s", err)
529 return
530 }
531
532 h.WriteOOBNotice(w, "keys", "Key added!")
533 return
534 }
535}
536
537func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
538 session, _ := h.s.Get(r, "bild-session")
539 did := session.Values["did"].(string)
540
541 switch r.Method {
542 case http.MethodGet:
543 if err := h.t.ExecuteTemplate(w, "repo/new", nil); err != nil {
544 log.Println(err)
545 return
546 }
547 case http.MethodPut:
548 name := r.FormValue("name")
549 description := r.FormValue("description")
550
551 err := git.InitBare(filepath.Join(h.c.Repo.ScanPath, "example.com", name))
552 if err != nil {
553 h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
554 return
555 }
556
557 err = h.db.AddRepo(did, name, description)
558 if err != nil {
559 h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
560 return
561 }
562
563 w.Header().Set("HX-Redirect", fmt.Sprintf("/@example.com/%s", name))
564 w.WriteHeader(http.StatusOK)
565 }
566}