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}