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 "github.com/dustin/go-humanize" 18 "github.com/go-chi/chi/v5" 19 "github.com/go-git/go-git/v5/plumbing" 20 "github.com/gorilla/sessions" 21 "github.com/icyphox/bild/config" 22 "github.com/icyphox/bild/db" 23 "github.com/icyphox/bild/git" 24 "github.com/icyphox/bild/routes/auth" 25 "github.com/russross/blackfriday/v2" 26 "golang.org/x/crypto/ssh" 27) 28 29type Handle struct { 30 c *config.Config 31 t *template.Template 32 s *sessions.CookieStore 33 db *db.DB 34 auth *auth.Auth 35} 36 37func (h *Handle) Index(w http.ResponseWriter, r *http.Request) { 38 user := chi.URLParam(r, "user") 39 path := filepath.Join(h.c.Repo.ScanPath, user) 40 dirs, err := os.ReadDir(path) 41 if err != nil { 42 h.Write500(w) 43 log.Printf("reading scan path: %s", err) 44 return 45 } 46 47 type info struct { 48 DisplayName, Name, Desc, Idle string 49 d time.Time 50 } 51 52 infos := []info{} 53 54 for _, dir := range dirs { 55 name := dir.Name() 56 if !dir.IsDir() || h.isIgnored(name) || h.isUnlisted(name) { 57 continue 58 } 59 60 gr, err := git.Open(path, "") 61 if err != nil { 62 log.Println(err) 63 continue 64 } 65 66 c, err := gr.LastCommit() 67 if err != nil { 68 h.Write500(w) 69 log.Println(err) 70 return 71 } 72 73 infos = append(infos, info{ 74 DisplayName: getDisplayName(name), 75 Name: name, 76 Desc: getDescription(path), 77 Idle: humanize.Time(c.Author.When), 78 d: c.Author.When, 79 }) 80 } 81 82 sort.Slice(infos, func(i, j int) bool { 83 return infos[j].d.Before(infos[i].d) 84 }) 85 86 data := make(map[string]interface{}) 87 data["meta"] = h.c.Meta 88 data["info"] = infos 89 90 if err := h.t.ExecuteTemplate(w, "index", data); err != nil { 91 log.Println(err) 92 return 93 } 94} 95 96func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) { 97 name := uniqueName(r) 98 if h.isIgnored(name) { 99 h.Write404(w) 100 return 101 } 102 103 name = filepath.Clean(name) 104 path := filepath.Join(h.c.Repo.ScanPath, name) 105 106 gr, err := git.Open(path, "") 107 if err != nil { 108 if errors.Is(err, plumbing.ErrReferenceNotFound) { 109 h.t.ExecuteTemplate(w, "repo/empty", nil) 110 return 111 } else { 112 h.Write404(w) 113 return 114 } 115 } 116 commits, err := gr.Commits() 117 if err != nil { 118 h.Write500(w) 119 log.Println(err) 120 return 121 } 122 123 var readmeContent template.HTML 124 for _, readme := range h.c.Repo.Readme { 125 ext := filepath.Ext(readme) 126 content, _ := gr.FileContent(readme) 127 if len(content) > 0 { 128 switch ext { 129 case ".md", ".mkd", ".markdown": 130 unsafe := blackfriday.Run( 131 []byte(content), 132 blackfriday.WithExtensions(blackfriday.CommonExtensions), 133 ) 134 html := sanitize(unsafe) 135 readmeContent = template.HTML(html) 136 default: 137 safe := sanitize([]byte(content)) 138 readmeContent = template.HTML( 139 fmt.Sprintf(`<pre>%s</pre>`, safe), 140 ) 141 } 142 break 143 } 144 } 145 146 if readmeContent == "" { 147 log.Printf("no readme found for %s", name) 148 } 149 150 mainBranch, err := gr.FindMainBranch(h.c.Repo.MainBranch) 151 if err != nil { 152 h.Write500(w) 153 log.Println(err) 154 return 155 } 156 157 if len(commits) >= 3 { 158 commits = commits[:3] 159 } 160 161 data := make(map[string]any) 162 data["name"] = name 163 data["displayname"] = getDisplayName(name) 164 data["ref"] = mainBranch 165 data["readme"] = readmeContent 166 data["commits"] = commits 167 data["desc"] = getDescription(path) 168 data["servername"] = h.c.Server.Name 169 data["meta"] = h.c.Meta 170 data["gomod"] = isGoModule(gr) 171 172 if err := h.t.ExecuteTemplate(w, "repo/repo", data); err != nil { 173 log.Println(err) 174 return 175 } 176 177 return 178} 179 180func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) { 181 name := uniqueName(r) 182 if h.isIgnored(name) { 183 h.Write404(w) 184 return 185 } 186 treePath := chi.URLParam(r, "*") 187 ref := chi.URLParam(r, "ref") 188 189 name = filepath.Clean(name) 190 path := filepath.Join(h.c.Repo.ScanPath, name) 191 gr, err := git.Open(path, ref) 192 if err != nil { 193 h.Write404(w) 194 return 195 } 196 197 files, err := gr.FileTree(treePath) 198 if err != nil { 199 h.Write500(w) 200 log.Println(err) 201 return 202 } 203 204 data := make(map[string]any) 205 data["name"] = name 206 data["displayname"] = getDisplayName(name) 207 data["ref"] = ref 208 data["parent"] = treePath 209 data["desc"] = getDescription(path) 210 data["dotdot"] = filepath.Dir(treePath) 211 212 h.listFiles(files, data, w) 213 return 214} 215 216func (h *Handle) FileContent(w http.ResponseWriter, r *http.Request) { 217 var raw bool 218 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil { 219 raw = rawParam 220 } 221 222 name := uniqueName(r) 223 224 if h.isIgnored(name) { 225 h.Write404(w) 226 return 227 } 228 treePath := chi.URLParam(r, "*") 229 ref := chi.URLParam(r, "ref") 230 231 name = filepath.Clean(name) 232 path := filepath.Join(h.c.Repo.ScanPath, name) 233 gr, err := git.Open(path, ref) 234 if err != nil { 235 h.Write404(w) 236 return 237 } 238 239 contents, err := gr.FileContent(treePath) 240 if err != nil { 241 h.Write500(w) 242 return 243 } 244 data := make(map[string]any) 245 data["name"] = name 246 data["displayname"] = getDisplayName(name) 247 data["ref"] = ref 248 data["desc"] = getDescription(path) 249 data["path"] = treePath 250 251 safe := sanitize([]byte(contents)) 252 253 if raw { 254 h.showRaw(string(safe), w) 255 } else { 256 if h.c.Meta.SyntaxHighlight == "" { 257 h.showFile(string(safe), data, w) 258 } else { 259 h.showFileWithHighlight(treePath, string(safe), data, w) 260 } 261 } 262} 263 264func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) { 265 name := uniqueName(r) 266 if h.isIgnored(name) { 267 h.Write404(w) 268 return 269 } 270 271 file := chi.URLParam(r, "file") 272 273 // TODO: extend this to add more files compression (e.g.: xz) 274 if !strings.HasSuffix(file, ".tar.gz") { 275 h.Write404(w) 276 return 277 } 278 279 ref := strings.TrimSuffix(file, ".tar.gz") 280 281 // This allows the browser to use a proper name for the file when 282 // downloading 283 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref) 284 setContentDisposition(w, filename) 285 setGZipMIME(w) 286 287 path := filepath.Join(h.c.Repo.ScanPath, name) 288 gr, err := git.Open(path, ref) 289 if err != nil { 290 h.Write404(w) 291 return 292 } 293 294 gw := gzip.NewWriter(w) 295 defer gw.Close() 296 297 prefix := fmt.Sprintf("%s-%s", name, ref) 298 err = gr.WriteTar(gw, prefix) 299 if err != nil { 300 // once we start writing to the body we can't report error anymore 301 // so we are only left with printing the error. 302 log.Println(err) 303 return 304 } 305 306 err = gw.Flush() 307 if err != nil { 308 // once we start writing to the body we can't report error anymore 309 // so we are only left with printing the error. 310 log.Println(err) 311 return 312 } 313} 314 315func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 316 name := uniqueName(r) 317 if h.isIgnored(name) { 318 h.Write404(w) 319 return 320 } 321 ref := chi.URLParam(r, "ref") 322 323 path := filepath.Join(h.c.Repo.ScanPath, name) 324 gr, err := git.Open(path, ref) 325 if err != nil { 326 h.Write404(w) 327 return 328 } 329 330 commits, err := gr.Commits() 331 if err != nil { 332 h.Write500(w) 333 log.Println(err) 334 return 335 } 336 337 data := make(map[string]interface{}) 338 data["commits"] = commits 339 data["meta"] = h.c.Meta 340 data["name"] = name 341 data["displayname"] = getDisplayName(name) 342 data["ref"] = ref 343 data["desc"] = getDescription(path) 344 data["log"] = true 345 346 if err := h.t.ExecuteTemplate(w, "repo/log", data); err != nil { 347 log.Println(err) 348 return 349 } 350} 351 352func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) { 353 name := uniqueName(r) 354 if h.isIgnored(name) { 355 h.Write404(w) 356 return 357 } 358 ref := chi.URLParam(r, "ref") 359 360 path := filepath.Join(h.c.Repo.ScanPath, name) 361 gr, err := git.Open(path, ref) 362 if err != nil { 363 h.Write404(w) 364 return 365 } 366 367 diff, err := gr.Diff() 368 if err != nil { 369 h.Write500(w) 370 log.Println(err) 371 return 372 } 373 374 data := make(map[string]interface{}) 375 376 data["commit"] = diff.Commit 377 data["stat"] = diff.Stat 378 data["diff"] = diff.Diff 379 data["meta"] = h.c.Meta 380 data["name"] = name 381 data["displayname"] = getDisplayName(name) 382 data["ref"] = ref 383 data["desc"] = getDescription(path) 384 385 if err := h.t.ExecuteTemplate(w, "repo/commit", data); err != nil { 386 log.Println(err) 387 return 388 } 389} 390 391func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) { 392 name := chi.URLParam(r, "name") 393 if h.isIgnored(name) { 394 h.Write404(w) 395 return 396 } 397 398 path := filepath.Join(h.c.Repo.ScanPath, name) 399 gr, err := git.Open(path, "") 400 if err != nil { 401 h.Write404(w) 402 return 403 } 404 405 tags, err := gr.Tags() 406 if err != nil { 407 // Non-fatal, we *should* have at least one branch to show. 408 log.Println(err) 409 } 410 411 branches, err := gr.Branches() 412 if err != nil { 413 log.Println(err) 414 h.Write500(w) 415 return 416 } 417 418 data := make(map[string]interface{}) 419 420 data["meta"] = h.c.Meta 421 data["name"] = name 422 data["displayname"] = getDisplayName(name) 423 data["branches"] = branches 424 data["tags"] = tags 425 data["desc"] = getDescription(path) 426 427 if err := h.t.ExecuteTemplate(w, "repo/refs", data); err != nil { 428 log.Println(err) 429 return 430 } 431} 432 433func (h *Handle) ServeStatic(w http.ResponseWriter, r *http.Request) { 434 f := chi.URLParam(r, "file") 435 f = filepath.Clean(filepath.Join(h.c.Dirs.Static, f)) 436 437 http.ServeFile(w, r, f) 438} 439 440func (h *Handle) Login(w http.ResponseWriter, r *http.Request) { 441 switch r.Method { 442 case http.MethodGet: 443 if err := h.t.ExecuteTemplate(w, "user/login", nil); err != nil { 444 log.Println(err) 445 return 446 } 447 case http.MethodPost: 448 username := r.FormValue("username") 449 appPassword := r.FormValue("app_password") 450 451 atSession, err := h.auth.CreateInitialSession(w, r, username, appPassword) 452 if err != nil { 453 h.WriteOOBNotice(w, "login", "Invalid username or app password.") 454 log.Printf("creating initial session: %s", err) 455 return 456 } 457 458 err = h.auth.StoreSession(r, w, &atSession, nil) 459 if err != nil { 460 h.WriteOOBNotice(w, "login", "Failed to store session.") 461 log.Printf("storing session: %s", err) 462 return 463 } 464 465 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 466 http.Redirect(w, r, "/", http.StatusPermanentRedirect) 467 return 468 } 469} 470 471func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 472 session, _ := h.s.Get(r, "bild-session") 473 did := session.Values["did"].(string) 474 475 switch r.Method { 476 case http.MethodGet: 477 keys, err := h.db.GetPublicKeys(did) 478 if err != nil { 479 h.WriteOOBNotice(w, "keys", "Failed to list keys. Try again later.") 480 log.Println(err) 481 return 482 } 483 484 data := make(map[string]interface{}) 485 data["keys"] = keys 486 if err := h.t.ExecuteTemplate(w, "settings/keys", data); err != nil { 487 log.Println(err) 488 return 489 } 490 case http.MethodPut: 491 key := r.FormValue("key") 492 name := r.FormValue("name") 493 494 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 495 if err != nil { 496 h.WriteOOBNotice(w, "keys", "Invalid public key. Check your formatting and try again.") 497 log.Printf("parsing public key: %s", err) 498 return 499 } 500 501 if err := h.db.AddPublicKey(did, name, key); err != nil { 502 h.WriteOOBNotice(w, "keys", "Failed to add key.") 503 log.Printf("adding public key: %s", err) 504 return 505 } 506 507 h.WriteOOBNotice(w, "keys", "Key added!") 508 return 509 } 510} 511 512func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) { 513 session, _ := h.s.Get(r, "bild-session") 514 did := session.Values["did"].(string) 515 handle := session.Values["handle"].(string) 516 517 switch r.Method { 518 case http.MethodGet: 519 if err := h.t.ExecuteTemplate(w, "repo/new", nil); err != nil { 520 log.Println(err) 521 return 522 } 523 case http.MethodPut: 524 name := r.FormValue("name") 525 description := r.FormValue("description") 526 527 err := git.InitBare(filepath.Join(h.c.Repo.ScanPath, handle, name)) 528 if err != nil { 529 h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.") 530 return 531 } 532 533 err = h.db.AddRepo(did, name, description) 534 if err != nil { 535 h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.") 536 return 537 } 538 539 w.Header().Set("HX-Redirect", fmt.Sprintf("/@example.com/%s", name)) 540 w.WriteHeader(http.StatusOK) 541 } 542}