this repo has no description
1package knotserver 2 3import ( 4 "compress/gzip" 5 "crypto/hmac" 6 "crypto/sha256" 7 "encoding/hex" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "html/template" 12 "net/http" 13 "path/filepath" 14 "strconv" 15 "strings" 16 17 securejoin "github.com/cyphar/filepath-securejoin" 18 "github.com/gliderlabs/ssh" 19 "github.com/go-chi/chi/v5" 20 gogit "github.com/go-git/go-git/v5" 21 "github.com/go-git/go-git/v5/plumbing" 22 "github.com/go-git/go-git/v5/plumbing/object" 23 "github.com/russross/blackfriday/v2" 24 "github.com/sotangled/tangled/knotserver/db" 25 "github.com/sotangled/tangled/knotserver/git" 26 "github.com/sotangled/tangled/types" 27) 28 29func (h *Handle) Index(w http.ResponseWriter, r *http.Request) { 30 w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 31} 32 33func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) { 34 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 35 l := h.l.With("path", path, "handler", "RepoIndex") 36 ref := chi.URLParam(r, "ref") 37 38 gr, err := git.Open(path, ref) 39 if err != nil { 40 if errors.Is(err, plumbing.ErrReferenceNotFound) { 41 resp := types.RepoIndexResponse{ 42 IsEmpty: true, 43 } 44 writeJSON(w, resp) 45 return 46 } else { 47 l.Error("opening repo", "error", err.Error()) 48 notFound(w) 49 return 50 } 51 } 52 53 commits, err := gr.Commits() 54 if err != nil { 55 writeError(w, err.Error(), http.StatusInternalServerError) 56 l.Error("fetching commits", "error", err.Error()) 57 return 58 } 59 if len(commits) > 10 { 60 commits = commits[:10] 61 } 62 63 branches, err := gr.Branches() 64 if err != nil { 65 l.Error("getting branches", "error", err.Error()) 66 writeError(w, err.Error(), http.StatusInternalServerError) 67 return 68 } 69 70 bs := []types.Branch{} 71 for _, branch := range branches { 72 b := types.Branch{} 73 b.Hash = branch.Hash().String() 74 b.Name = branch.Name().Short() 75 bs = append(bs, b) 76 } 77 78 tags, err := gr.Tags() 79 if err != nil { 80 // Non-fatal, we *should* have at least one branch to show. 81 l.Warn("getting tags", "error", err.Error()) 82 } 83 84 rtags := []*types.TagReference{} 85 for _, tag := range tags { 86 tr := types.TagReference{ 87 Tag: tag.TagObject(), 88 } 89 90 tr.Reference = types.Reference{ 91 Name: tag.Name(), 92 Hash: tag.Hash().String(), 93 } 94 95 if tag.Message() != "" { 96 tr.Message = tag.Message() 97 } 98 99 rtags = append(rtags, &tr) 100 } 101 102 var readmeContent template.HTML 103 for _, readme := range h.c.Repo.Readme { 104 ext := filepath.Ext(readme) 105 content, _ := gr.FileContent(readme) 106 if len(content) > 0 { 107 switch ext { 108 case ".md", ".mkd", ".markdown": 109 unsafe := blackfriday.Run( 110 []byte(content), 111 blackfriday.WithExtensions(blackfriday.CommonExtensions), 112 ) 113 html := sanitize(unsafe) 114 readmeContent = template.HTML(html) 115 default: 116 safe := sanitize([]byte(content)) 117 readmeContent = template.HTML( 118 fmt.Sprintf(`<pre>%s</pre>`, safe), 119 ) 120 } 121 break 122 } 123 } 124 125 if readmeContent == "" { 126 l.Warn("no readme found") 127 } 128 129 files, err := gr.FileTree("") 130 if err != nil { 131 writeError(w, err.Error(), http.StatusInternalServerError) 132 l.Error("file tree", "error", err.Error()) 133 return 134 } 135 136 if ref == "" { 137 mainBranch, err := gr.FindMainBranch(h.c.Repo.MainBranch) 138 if err != nil { 139 writeError(w, err.Error(), http.StatusInternalServerError) 140 l.Error("finding main branch", "error", err.Error()) 141 return 142 } 143 ref = mainBranch 144 } 145 146 resp := types.RepoIndexResponse{ 147 IsEmpty: false, 148 Ref: ref, 149 Commits: commits, 150 Description: getDescription(path), 151 Readme: readmeContent, 152 Files: files, 153 Branches: bs, 154 Tags: rtags, 155 } 156 157 writeJSON(w, resp) 158 return 159} 160 161func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) { 162 treePath := chi.URLParam(r, "*") 163 ref := chi.URLParam(r, "ref") 164 165 l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath) 166 167 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 168 gr, err := git.Open(path, ref) 169 if err != nil { 170 notFound(w) 171 return 172 } 173 174 files, err := gr.FileTree(treePath) 175 if err != nil { 176 writeError(w, err.Error(), http.StatusInternalServerError) 177 l.Error("file tree", "error", err.Error()) 178 return 179 } 180 181 resp := types.RepoTreeResponse{ 182 Ref: ref, 183 Parent: treePath, 184 Description: getDescription(path), 185 DotDot: filepath.Dir(treePath), 186 Files: files, 187 } 188 189 writeJSON(w, resp) 190 return 191} 192 193func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { 194 treePath := chi.URLParam(r, "*") 195 ref := chi.URLParam(r, "ref") 196 197 l := h.l.With("handler", "FileContent", "ref", ref, "treePath", treePath) 198 199 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 200 gr, err := git.Open(path, ref) 201 if err != nil { 202 notFound(w) 203 return 204 } 205 206 var isBinaryFile bool = false 207 contents, err := gr.FileContent(treePath) 208 if errors.Is(err, git.ErrBinaryFile) { 209 isBinaryFile = true 210 } else if errors.Is(err, object.ErrFileNotFound) { 211 notFound(w) 212 return 213 } else if err != nil { 214 writeError(w, err.Error(), http.StatusInternalServerError) 215 return 216 } 217 218 safe := string(sanitize([]byte(contents))) 219 220 resp := types.RepoBlobResponse{ 221 Ref: ref, 222 Contents: string(safe), 223 Path: treePath, 224 IsBinary: isBinaryFile, 225 } 226 227 h.showFile(resp, w, l) 228} 229 230func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) { 231 name := chi.URLParam(r, "name") 232 file := chi.URLParam(r, "file") 233 234 l := h.l.With("handler", "Archive", "name", name, "file", file) 235 236 // TODO: extend this to add more files compression (e.g.: xz) 237 if !strings.HasSuffix(file, ".tar.gz") { 238 notFound(w) 239 return 240 } 241 242 ref := strings.TrimSuffix(file, ".tar.gz") 243 244 // This allows the browser to use a proper name for the file when 245 // downloading 246 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref) 247 setContentDisposition(w, filename) 248 setGZipMIME(w) 249 250 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 251 gr, err := git.Open(path, ref) 252 if err != nil { 253 notFound(w) 254 return 255 } 256 257 gw := gzip.NewWriter(w) 258 defer gw.Close() 259 260 prefix := fmt.Sprintf("%s-%s", name, ref) 261 err = gr.WriteTar(gw, prefix) 262 if err != nil { 263 // once we start writing to the body we can't report error anymore 264 // so we are only left with printing the error. 265 l.Error("writing tar file", "error", err.Error()) 266 return 267 } 268 269 err = gw.Flush() 270 if err != nil { 271 // once we start writing to the body we can't report error anymore 272 // so we are only left with printing the error. 273 l.Error("flushing?", "error", err.Error()) 274 return 275 } 276} 277 278func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 279 ref := chi.URLParam(r, "ref") 280 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 281 282 l := h.l.With("handler", "Log", "ref", ref, "path", path) 283 284 gr, err := git.Open(path, ref) 285 if err != nil { 286 notFound(w) 287 return 288 } 289 290 commits, err := gr.Commits() 291 if err != nil { 292 writeError(w, err.Error(), http.StatusInternalServerError) 293 l.Error("fetching commits", "error", err.Error()) 294 return 295 } 296 297 // Get page parameters 298 page := 1 299 pageSize := 30 300 301 if pageParam := r.URL.Query().Get("page"); pageParam != "" { 302 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 { 303 page = p 304 } 305 } 306 307 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" { 308 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 { 309 pageSize = ps 310 } 311 } 312 313 // Calculate pagination 314 start := (page - 1) * pageSize 315 end := start + pageSize 316 total := len(commits) 317 318 if start >= total { 319 commits = []*object.Commit{} 320 } else { 321 if end > total { 322 end = total 323 } 324 commits = commits[start:end] 325 } 326 327 resp := types.RepoLogResponse{ 328 Commits: commits, 329 Ref: ref, 330 Description: getDescription(path), 331 Log: true, 332 Total: total, 333 Page: page, 334 PerPage: pageSize, 335 } 336 337 writeJSON(w, resp) 338 return 339} 340 341func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) { 342 ref := chi.URLParam(r, "ref") 343 344 l := h.l.With("handler", "Diff", "ref", ref) 345 346 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 347 gr, err := git.Open(path, ref) 348 if err != nil { 349 notFound(w) 350 return 351 } 352 353 diff, err := gr.Diff() 354 if err != nil { 355 writeError(w, err.Error(), http.StatusInternalServerError) 356 l.Error("getting diff", "error", err.Error()) 357 return 358 } 359 360 resp := types.RepoCommitResponse{ 361 Ref: ref, 362 Diff: diff, 363 } 364 365 writeJSON(w, resp) 366 return 367} 368 369func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) { 370 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 371 l := h.l.With("handler", "Refs") 372 373 gr, err := git.Open(path, "") 374 if err != nil { 375 notFound(w) 376 return 377 } 378 379 tags, err := gr.Tags() 380 if err != nil { 381 // Non-fatal, we *should* have at least one branch to show. 382 l.Warn("getting tags", "error", err.Error()) 383 } 384 385 rtags := []*types.TagReference{} 386 for _, tag := range tags { 387 tr := types.TagReference{ 388 Tag: tag.TagObject(), 389 } 390 391 tr.Reference = types.Reference{ 392 Name: tag.Name(), 393 Hash: tag.Hash().String(), 394 } 395 396 if tag.Message() != "" { 397 tr.Message = tag.Message() 398 } 399 400 rtags = append(rtags, &tr) 401 } 402 403 resp := types.RepoTagsResponse{ 404 Tags: rtags, 405 } 406 407 writeJSON(w, resp) 408 return 409} 410 411func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) { 412 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 413 l := h.l.With("handler", "Branches") 414 415 gr, err := git.Open(path, "") 416 if err != nil { 417 notFound(w) 418 return 419 } 420 421 branches, err := gr.Branches() 422 if err != nil { 423 l.Error("getting branches", "error", err.Error()) 424 writeError(w, err.Error(), http.StatusInternalServerError) 425 return 426 } 427 428 bs := []types.Branch{} 429 for _, branch := range branches { 430 b := types.Branch{} 431 b.Hash = branch.Hash().String() 432 b.Name = branch.Name().Short() 433 bs = append(bs, b) 434 } 435 436 resp := types.RepoBranchesResponse{ 437 Branches: bs, 438 } 439 440 writeJSON(w, resp) 441 return 442} 443 444func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 445 l := h.l.With("handler", "Keys") 446 447 switch r.Method { 448 case http.MethodGet: 449 keys, err := h.db.GetAllPublicKeys() 450 if err != nil { 451 writeError(w, err.Error(), http.StatusInternalServerError) 452 l.Error("getting public keys", "error", err.Error()) 453 return 454 } 455 456 data := make([]map[string]interface{}, 0) 457 for _, key := range keys { 458 j := key.JSON() 459 data = append(data, j) 460 } 461 writeJSON(w, data) 462 return 463 464 case http.MethodPut: 465 pk := db.PublicKey{} 466 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil { 467 writeError(w, "invalid request body", http.StatusBadRequest) 468 return 469 } 470 471 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key)) 472 if err != nil { 473 writeError(w, "invalid pubkey", http.StatusBadRequest) 474 } 475 476 if err := h.db.AddPublicKey(pk); err != nil { 477 writeError(w, err.Error(), http.StatusInternalServerError) 478 l.Error("adding public key", "error", err.Error()) 479 return 480 } 481 482 w.WriteHeader(http.StatusNoContent) 483 return 484 } 485} 486 487func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) { 488 l := h.l.With("handler", "NewRepo") 489 490 data := struct { 491 Did string `json:"did"` 492 Name string `json:"name"` 493 }{} 494 495 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 496 writeError(w, "invalid request body", http.StatusBadRequest) 497 return 498 } 499 500 did := data.Did 501 name := data.Name 502 503 relativeRepoPath := filepath.Join(did, name) 504 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 505 err := git.InitBare(repoPath) 506 if err != nil { 507 l.Error("initializing bare repo", "error", err.Error()) 508 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 509 writeError(w, "That repo already exists!", http.StatusConflict) 510 return 511 } else { 512 writeError(w, err.Error(), http.StatusInternalServerError) 513 return 514 } 515 } 516 517 // add perms for this user to access the repo 518 err = h.e.AddRepo(did, ThisServer, relativeRepoPath) 519 if err != nil { 520 l.Error("adding repo permissions", "error", err.Error()) 521 writeError(w, err.Error(), http.StatusInternalServerError) 522 return 523 } 524 525 w.WriteHeader(http.StatusNoContent) 526} 527 528func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { 529 l := h.l.With("handler", "AddMember") 530 531 data := struct { 532 Did string `json:"did"` 533 }{} 534 535 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 536 writeError(w, "invalid request body", http.StatusBadRequest) 537 return 538 } 539 540 did := data.Did 541 542 if err := h.db.AddDid(did); err != nil { 543 l.Error("adding did", "error", err.Error()) 544 writeError(w, err.Error(), http.StatusInternalServerError) 545 return 546 } 547 548 h.jc.AddDid(did) 549 if err := h.e.AddMember(ThisServer, did); err != nil { 550 l.Error("adding member", "error", err.Error()) 551 writeError(w, err.Error(), http.StatusInternalServerError) 552 return 553 } 554 555 if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 556 l.Error("fetching and adding keys", "error", err.Error()) 557 writeError(w, err.Error(), http.StatusInternalServerError) 558 return 559 } 560 561 w.WriteHeader(http.StatusNoContent) 562} 563 564func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 565 l := h.l.With("handler", "AddRepoCollaborator") 566 567 data := struct { 568 Did string `json:"did"` 569 }{} 570 571 ownerDid := chi.URLParam(r, "did") 572 repo := chi.URLParam(r, "name") 573 574 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 575 writeError(w, "invalid request body", http.StatusBadRequest) 576 return 577 } 578 579 if err := h.db.AddDid(data.Did); err != nil { 580 l.Error("adding did", "error", err.Error()) 581 writeError(w, err.Error(), http.StatusInternalServerError) 582 return 583 } 584 h.jc.AddDid(data.Did) 585 586 repoName, _ := securejoin.SecureJoin(ownerDid, repo) 587 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil { 588 l.Error("adding repo collaborator", "error", err.Error()) 589 writeError(w, err.Error(), http.StatusInternalServerError) 590 return 591 } 592 593 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 594 l.Error("fetching and adding keys", "error", err.Error()) 595 writeError(w, err.Error(), http.StatusInternalServerError) 596 return 597 } 598 599 w.WriteHeader(http.StatusNoContent) 600} 601 602func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { 603 l := h.l.With("handler", "Init") 604 605 if h.knotInitialized { 606 writeError(w, "knot already initialized", http.StatusConflict) 607 return 608 } 609 610 data := struct { 611 Did string `json:"did"` 612 }{} 613 614 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 615 l.Error("failed to decode request body", "error", err.Error()) 616 writeError(w, "invalid request body", http.StatusBadRequest) 617 return 618 } 619 620 if data.Did == "" { 621 l.Error("empty DID in request", "did", data.Did) 622 writeError(w, "did is empty", http.StatusBadRequest) 623 return 624 } 625 626 if err := h.db.AddDid(data.Did); err != nil { 627 l.Error("failed to add DID", "error", err.Error()) 628 writeError(w, err.Error(), http.StatusInternalServerError) 629 return 630 } 631 632 h.jc.UpdateDids([]string{data.Did}) 633 if err := h.e.AddOwner(ThisServer, data.Did); err != nil { 634 l.Error("adding owner", "error", err.Error()) 635 writeError(w, err.Error(), http.StatusInternalServerError) 636 return 637 } 638 639 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 640 l.Error("fetching and adding keys", "error", err.Error()) 641 writeError(w, err.Error(), http.StatusInternalServerError) 642 return 643 } 644 645 close(h.init) 646 647 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret)) 648 mac.Write([]byte("ok")) 649 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil))) 650 651 w.WriteHeader(http.StatusNoContent) 652} 653 654func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 655 w.Write([]byte("ok")) 656}