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}