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 fmt.Println(path)
110 gr, err := git.Open(path, "")
111 if err != nil {
112 if errors.Is(err, plumbing.ErrReferenceNotFound) {
113 h.t.ExecuteTemplate(w, "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", 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 fmt.Println(path)
196 gr, err := git.Open(path, ref)
197 if err != nil {
198 h.Write404(w)
199 return
200 }
201
202 files, err := gr.FileTree(treePath)
203 if err != nil {
204 h.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 h.listFiles(files, data, w)
218 return
219}
220
221func (h *Handle) FileContent(w http.ResponseWriter, r *http.Request) {
222 var raw bool
223 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
224 raw = rawParam
225 }
226
227 name := uniqueName(r)
228
229 if h.isIgnored(name) {
230 h.Write404(w)
231 return
232 }
233 treePath := chi.URLParam(r, "*")
234 ref := chi.URLParam(r, "ref")
235
236 name = filepath.Clean(name)
237 path := filepath.Join(h.c.Repo.ScanPath, name)
238 gr, err := git.Open(path, ref)
239 if err != nil {
240 h.Write404(w)
241 return
242 }
243
244 contents, err := gr.FileContent(treePath)
245 if err != nil {
246 h.Write500(w)
247 return
248 }
249 data := make(map[string]any)
250 data["name"] = name
251 data["displayname"] = getDisplayName(name)
252 data["ref"] = ref
253 data["desc"] = getDescription(path)
254 data["path"] = treePath
255
256 safe := sanitize([]byte(contents))
257
258 if raw {
259 h.showRaw(string(safe), w)
260 } else {
261 if h.c.Meta.SyntaxHighlight == "" {
262 h.showFile(string(safe), data, w)
263 } else {
264 h.showFileWithHighlight(treePath, string(safe), data, w)
265 }
266 }
267}
268
269func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
270 name := uniqueName(r)
271 if h.isIgnored(name) {
272 h.Write404(w)
273 return
274 }
275
276 file := chi.URLParam(r, "file")
277
278 // TODO: extend this to add more files compression (e.g.: xz)
279 if !strings.HasSuffix(file, ".tar.gz") {
280 h.Write404(w)
281 return
282 }
283
284 ref := strings.TrimSuffix(file, ".tar.gz")
285
286 // This allows the browser to use a proper name for the file when
287 // downloading
288 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
289 setContentDisposition(w, filename)
290 setGZipMIME(w)
291
292 path := filepath.Join(h.c.Repo.ScanPath, name)
293 gr, err := git.Open(path, ref)
294 if err != nil {
295 h.Write404(w)
296 return
297 }
298
299 gw := gzip.NewWriter(w)
300 defer gw.Close()
301
302 prefix := fmt.Sprintf("%s-%s", name, ref)
303 err = gr.WriteTar(gw, prefix)
304 if err != nil {
305 // once we start writing to the body we can't report error anymore
306 // so we are only left with printing the error.
307 log.Println(err)
308 return
309 }
310
311 err = gw.Flush()
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
320func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
321 name := uniqueName(r)
322 if h.isIgnored(name) {
323 h.Write404(w)
324 return
325 }
326 ref := chi.URLParam(r, "ref")
327
328 path := filepath.Join(h.c.Repo.ScanPath, name)
329 gr, err := git.Open(path, ref)
330 if err != nil {
331 h.Write404(w)
332 return
333 }
334
335 commits, err := gr.Commits()
336 if err != nil {
337 h.Write500(w)
338 log.Println(err)
339 return
340 }
341
342 data := make(map[string]interface{})
343 data["commits"] = commits
344 data["meta"] = h.c.Meta
345 data["name"] = name
346 data["displayname"] = getDisplayName(name)
347 data["ref"] = ref
348 data["desc"] = getDescription(path)
349 data["log"] = true
350
351 if err := h.t.ExecuteTemplate(w, "log", data); err != nil {
352 log.Println(err)
353 return
354 }
355}
356
357func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
358 name := uniqueName(r)
359 if h.isIgnored(name) {
360 h.Write404(w)
361 return
362 }
363 ref := chi.URLParam(r, "ref")
364
365 path := filepath.Join(h.c.Repo.ScanPath, name)
366 gr, err := git.Open(path, ref)
367 if err != nil {
368 h.Write404(w)
369 return
370 }
371
372 diff, err := gr.Diff()
373 if err != nil {
374 h.Write500(w)
375 log.Println(err)
376 return
377 }
378
379 data := make(map[string]interface{})
380
381 data["commit"] = diff.Commit
382 data["stat"] = diff.Stat
383 data["diff"] = diff.Diff
384 data["meta"] = h.c.Meta
385 data["name"] = name
386 data["displayname"] = getDisplayName(name)
387 data["ref"] = ref
388 data["desc"] = getDescription(path)
389
390 if err := h.t.ExecuteTemplate(w, "commit", data); err != nil {
391 log.Println(err)
392 return
393 }
394}
395
396func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) {
397 name := chi.URLParam(r, "name")
398 if h.isIgnored(name) {
399 h.Write404(w)
400 return
401 }
402
403 path := filepath.Join(h.c.Repo.ScanPath, name)
404 gr, err := git.Open(path, "")
405 if err != nil {
406 h.Write404(w)
407 return
408 }
409
410 tags, err := gr.Tags()
411 if err != nil {
412 // Non-fatal, we *should* have at least one branch to show.
413 log.Println(err)
414 }
415
416 branches, err := gr.Branches()
417 if err != nil {
418 log.Println(err)
419 h.Write500(w)
420 return
421 }
422
423 data := make(map[string]interface{})
424
425 data["meta"] = h.c.Meta
426 data["name"] = name
427 data["displayname"] = getDisplayName(name)
428 data["branches"] = branches
429 data["tags"] = tags
430 data["desc"] = getDescription(path)
431
432 if err := h.t.ExecuteTemplate(w, "refs", data); err != nil {
433 log.Println(err)
434 return
435 }
436}
437
438func (h *Handle) ServeStatic(w http.ResponseWriter, r *http.Request) {
439 f := chi.URLParam(r, "file")
440 f = filepath.Clean(filepath.Join(h.c.Dirs.Static, f))
441
442 http.ServeFile(w, r, f)
443}
444
445func resolveIdent(arg string) (*identity.Identity, error) {
446 id, err := syntax.ParseAtIdentifier(arg)
447 if err != nil {
448 return nil, err
449 }
450
451 ctx := context.Background()
452 dir := identity.DefaultDirectory()
453 return dir.Lookup(ctx, *id)
454}
455
456func (h *Handle) Login(w http.ResponseWriter, r *http.Request) {
457 ctx := context.Background()
458 username := r.FormValue("username")
459 appPassword := r.FormValue("app_password")
460
461 resolved, err := resolveIdent(username)
462 if err != nil {
463 http.Error(w, "invalid `handle`", http.StatusBadRequest)
464 return
465 }
466
467 pdsUrl := resolved.PDSEndpoint()
468 client := xrpc.Client{
469 Host: pdsUrl,
470 }
471
472 atSession, err := comatproto.ServerCreateSession(ctx, &client, &comatproto.ServerCreateSession_Input{
473 Identifier: resolved.DID.String(),
474 Password: appPassword,
475 })
476
477 clientSession, _ := h.s.Get(r, "bild-session")
478 clientSession.Values["handle"] = atSession.Handle
479 clientSession.Values["did"] = atSession.Did
480 clientSession.Values["accessJwt"] = atSession.AccessJwt
481 clientSession.Values["refreshJwt"] = atSession.RefreshJwt
482 clientSession.Values["pds"] = pdsUrl
483 clientSession.Values["authenticated"] = true
484
485 err = clientSession.Save(r, w)
486
487 if err != nil {
488 log.Printf("failed to store session for did: %s\n", atSession.Did)
489 log.Println(err)
490 return
491 }
492
493 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
494 http.Redirect(w, r, "/", 302)
495}
496
497func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
498 session, _ := h.s.Get(r, "bild-session")
499 did := session.Values["did"].(string)
500
501 switch r.Method {
502 case http.MethodGet:
503 keys, err := h.db.GetPublicKeys(did)
504 if err != nil {
505 log.Println(err)
506 http.Error(w, "invalid `did`", http.StatusBadRequest)
507 return
508 }
509
510 data := make(map[string]interface{})
511 data["keys"] = keys
512 if err := h.t.ExecuteTemplate(w, "keys", data); err != nil {
513 log.Println(err)
514 return
515 }
516 case http.MethodPut:
517 key := r.FormValue("key")
518 name := r.FormValue("name")
519
520 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
521 if err != nil {
522 h.WriteOOBNotice(w, "keys", "Invalid public key. Check your formatting and try again.")
523 log.Printf("parsing public key: %s", err)
524 return
525 }
526
527 if err := h.db.AddPublicKey(did, name, key); err != nil {
528 h.WriteOOBNotice(w, "keys", "Failed to add key.")
529 log.Printf("adding public key: %s", err)
530 return
531 }
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
542 switch r.Method {
543 case http.MethodGet:
544 if err := h.t.ExecuteTemplate(w, "new", nil); err != nil {
545 log.Println(err)
546 return
547 }
548 case http.MethodPut:
549 name := r.FormValue("name")
550 description := r.FormValue("description")
551
552 err := git.InitBare(filepath.Join(h.c.Repo.ScanPath, "example.com", name))
553 if err != nil {
554 h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
555 return
556 }
557
558 err = h.db.AddRepo(did, name, description)
559 if err != nil {
560 h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
561 return
562 }
563
564 w.Header().Set("HX-Redirect", fmt.Sprintf("/@example.com/%s", name))
565 w.WriteHeader(http.StatusOK)
566 }
567}