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}