forked from tangled.org/core
this repo has no description
at knot-xrpc 30 kB view raw
1package knotserver 2 3import ( 4 "compress/gzip" 5 "context" 6 "crypto/sha256" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "log" 11 "net/http" 12 "net/url" 13 "os" 14 "path/filepath" 15 "strconv" 16 "strings" 17 "sync" 18 "time" 19 20 securejoin "github.com/cyphar/filepath-securejoin" 21 "github.com/gliderlabs/ssh" 22 "github.com/go-chi/chi/v5" 23 "github.com/go-git/go-git/v5/plumbing" 24 "github.com/go-git/go-git/v5/plumbing/object" 25 "tangled.sh/tangled.sh/core/hook" 26 "tangled.sh/tangled.sh/core/knotserver/db" 27 "tangled.sh/tangled.sh/core/knotserver/git" 28 "tangled.sh/tangled.sh/core/patchutil" 29 "tangled.sh/tangled.sh/core/rbac" 30 "tangled.sh/tangled.sh/core/types" 31) 32 33func (h *Handle) Index(w http.ResponseWriter, r *http.Request) { 34 w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 35} 36 37func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) { 38 w.Header().Set("Content-Type", "application/json") 39 40 capabilities := map[string]any{ 41 "pull_requests": map[string]any{ 42 "format_patch": true, 43 "patch_submissions": true, 44 "branch_submissions": true, 45 "fork_submissions": true, 46 }, 47 } 48 49 jsonData, err := json.Marshal(capabilities) 50 if err != nil { 51 http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError) 52 return 53 } 54 55 w.Write(jsonData) 56} 57 58func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) { 59 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 60 l := h.l.With("path", path, "handler", "RepoIndex") 61 ref := chi.URLParam(r, "ref") 62 ref, _ = url.PathUnescape(ref) 63 64 gr, err := git.Open(path, ref) 65 if err != nil { 66 plain, err2 := git.PlainOpen(path) 67 if err2 != nil { 68 l.Error("opening repo", "error", err2.Error()) 69 notFound(w) 70 return 71 } 72 branches, _ := plain.Branches() 73 74 log.Println(err) 75 76 if errors.Is(err, plumbing.ErrReferenceNotFound) { 77 resp := types.RepoIndexResponse{ 78 IsEmpty: true, 79 Branches: branches, 80 } 81 writeJSON(w, resp) 82 return 83 } else { 84 l.Error("opening repo", "error", err.Error()) 85 notFound(w) 86 return 87 } 88 } 89 90 var ( 91 commits []*object.Commit 92 total int 93 branches []types.Branch 94 files []types.NiceTree 95 tags []object.Tag 96 ) 97 98 var wg sync.WaitGroup 99 errorsCh := make(chan error, 5) 100 101 wg.Add(1) 102 go func() { 103 defer wg.Done() 104 cs, err := gr.Commits(0, 60) 105 if err != nil { 106 errorsCh <- fmt.Errorf("commits: %w", err) 107 return 108 } 109 commits = cs 110 }() 111 112 wg.Add(1) 113 go func() { 114 defer wg.Done() 115 t, err := gr.TotalCommits() 116 if err != nil { 117 errorsCh <- fmt.Errorf("calculating total: %w", err) 118 return 119 } 120 total = t 121 }() 122 123 wg.Add(1) 124 go func() { 125 defer wg.Done() 126 bs, err := gr.Branches() 127 if err != nil { 128 errorsCh <- fmt.Errorf("fetching branches: %w", err) 129 return 130 } 131 branches = bs 132 }() 133 134 wg.Add(1) 135 go func() { 136 defer wg.Done() 137 ts, err := gr.Tags() 138 if err != nil { 139 errorsCh <- fmt.Errorf("fetching tags: %w", err) 140 return 141 } 142 tags = ts 143 }() 144 145 wg.Add(1) 146 go func() { 147 defer wg.Done() 148 fs, err := gr.FileTree(r.Context(), "") 149 if err != nil { 150 errorsCh <- fmt.Errorf("fetching filetree: %w", err) 151 return 152 } 153 files = fs 154 }() 155 156 wg.Wait() 157 close(errorsCh) 158 159 // show any errors 160 for err := range errorsCh { 161 l.Error("loading repo", "error", err.Error()) 162 writeError(w, err.Error(), http.StatusInternalServerError) 163 return 164 } 165 166 rtags := []*types.TagReference{} 167 for _, tag := range tags { 168 var target *object.Tag 169 if tag.Target != plumbing.ZeroHash { 170 target = &tag 171 } 172 tr := types.TagReference{ 173 Tag: target, 174 } 175 176 tr.Reference = types.Reference{ 177 Name: tag.Name, 178 Hash: tag.Hash.String(), 179 } 180 181 if tag.Message != "" { 182 tr.Message = tag.Message 183 } 184 185 rtags = append(rtags, &tr) 186 } 187 188 var readmeContent string 189 var readmeFile string 190 for _, readme := range h.c.Repo.Readme { 191 content, _ := gr.FileContent(readme) 192 if len(content) > 0 { 193 readmeContent = string(content) 194 readmeFile = readme 195 } 196 } 197 198 if ref == "" { 199 mainBranch, err := gr.FindMainBranch() 200 if err != nil { 201 writeError(w, err.Error(), http.StatusInternalServerError) 202 l.Error("finding main branch", "error", err.Error()) 203 return 204 } 205 ref = mainBranch 206 } 207 208 resp := types.RepoIndexResponse{ 209 IsEmpty: false, 210 Ref: ref, 211 Commits: commits, 212 Description: getDescription(path), 213 Readme: readmeContent, 214 ReadmeFileName: readmeFile, 215 Files: files, 216 Branches: branches, 217 Tags: rtags, 218 TotalCommits: total, 219 } 220 221 writeJSON(w, resp) 222 return 223} 224 225func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) { 226 treePath := chi.URLParam(r, "*") 227 ref := chi.URLParam(r, "ref") 228 ref, _ = url.PathUnescape(ref) 229 230 l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath) 231 232 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 233 gr, err := git.Open(path, ref) 234 if err != nil { 235 notFound(w) 236 return 237 } 238 239 files, err := gr.FileTree(r.Context(), treePath) 240 if err != nil { 241 writeError(w, err.Error(), http.StatusInternalServerError) 242 l.Error("file tree", "error", err.Error()) 243 return 244 } 245 246 resp := types.RepoTreeResponse{ 247 Ref: ref, 248 Parent: treePath, 249 Description: getDescription(path), 250 DotDot: filepath.Dir(treePath), 251 Files: files, 252 } 253 254 writeJSON(w, resp) 255 return 256} 257 258func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) { 259 treePath := chi.URLParam(r, "*") 260 ref := chi.URLParam(r, "ref") 261 ref, _ = url.PathUnescape(ref) 262 263 l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath) 264 265 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 266 gr, err := git.Open(path, ref) 267 if err != nil { 268 notFound(w) 269 return 270 } 271 272 contents, err := gr.RawContent(treePath) 273 if err != nil { 274 writeError(w, err.Error(), http.StatusBadRequest) 275 l.Error("file content", "error", err.Error()) 276 return 277 } 278 279 mimeType := http.DetectContentType(contents) 280 281 // exception for svg 282 if filepath.Ext(treePath) == ".svg" { 283 mimeType = "image/svg+xml" 284 } 285 286 // allow image, video, and text/plain files to be served directly 287 switch { 288 case strings.HasPrefix(mimeType, "image/"): 289 // allowed 290 case strings.HasPrefix(mimeType, "video/"): 291 // allowed 292 case strings.HasPrefix(mimeType, "text/plain"): 293 // allowed 294 default: 295 l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 296 writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) 297 return 298 } 299 300 w.Header().Set("Cache-Control", "public, max-age=86400") // cache for 24 hours 301 w.Header().Set("ETag", fmt.Sprintf("%x", sha256.Sum256(contents))) 302 w.Header().Set("Content-Type", mimeType) 303 w.Write(contents) 304} 305 306func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { 307 treePath := chi.URLParam(r, "*") 308 ref := chi.URLParam(r, "ref") 309 ref, _ = url.PathUnescape(ref) 310 311 l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath) 312 313 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 314 gr, err := git.Open(path, ref) 315 if err != nil { 316 notFound(w) 317 return 318 } 319 320 var isBinaryFile bool = false 321 contents, err := gr.FileContent(treePath) 322 if errors.Is(err, git.ErrBinaryFile) { 323 isBinaryFile = true 324 } else if errors.Is(err, object.ErrFileNotFound) { 325 notFound(w) 326 return 327 } else if err != nil { 328 writeError(w, err.Error(), http.StatusInternalServerError) 329 return 330 } 331 332 bytes := []byte(contents) 333 // safe := string(sanitize(bytes)) 334 sizeHint := len(bytes) 335 336 resp := types.RepoBlobResponse{ 337 Ref: ref, 338 Contents: string(bytes), 339 Path: treePath, 340 IsBinary: isBinaryFile, 341 SizeHint: uint64(sizeHint), 342 } 343 344 h.showFile(resp, w, l) 345} 346 347func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) { 348 name := chi.URLParam(r, "name") 349 file := chi.URLParam(r, "file") 350 351 l := h.l.With("handler", "Archive", "name", name, "file", file) 352 353 // TODO: extend this to add more files compression (e.g.: xz) 354 if !strings.HasSuffix(file, ".tar.gz") { 355 notFound(w) 356 return 357 } 358 359 ref := strings.TrimSuffix(file, ".tar.gz") 360 361 unescapedRef, err := url.PathUnescape(ref) 362 if err != nil { 363 notFound(w) 364 return 365 } 366 367 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-") 368 369 // This allows the browser to use a proper name for the file when 370 // downloading 371 filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename) 372 setContentDisposition(w, filename) 373 setGZipMIME(w) 374 375 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 376 gr, err := git.Open(path, unescapedRef) 377 if err != nil { 378 notFound(w) 379 return 380 } 381 382 gw := gzip.NewWriter(w) 383 defer gw.Close() 384 385 prefix := fmt.Sprintf("%s-%s", name, safeRefFilename) 386 err = gr.WriteTar(gw, prefix) 387 if err != nil { 388 // once we start writing to the body we can't report error anymore 389 // so we are only left with printing the error. 390 l.Error("writing tar file", "error", err.Error()) 391 return 392 } 393 394 err = gw.Flush() 395 if err != nil { 396 // once we start writing to the body we can't report error anymore 397 // so we are only left with printing the error. 398 l.Error("flushing?", "error", err.Error()) 399 return 400 } 401} 402 403func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 404 ref := chi.URLParam(r, "ref") 405 ref, _ = url.PathUnescape(ref) 406 407 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 408 409 l := h.l.With("handler", "Log", "ref", ref, "path", path) 410 411 gr, err := git.Open(path, ref) 412 if err != nil { 413 notFound(w) 414 return 415 } 416 417 // Get page parameters 418 page := 1 419 pageSize := 30 420 421 if pageParam := r.URL.Query().Get("page"); pageParam != "" { 422 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 { 423 page = p 424 } 425 } 426 427 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" { 428 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 { 429 pageSize = ps 430 } 431 } 432 433 // convert to offset/limit 434 offset := (page - 1) * pageSize 435 limit := pageSize 436 437 commits, err := gr.Commits(offset, limit) 438 if err != nil { 439 writeError(w, err.Error(), http.StatusInternalServerError) 440 l.Error("fetching commits", "error", err.Error()) 441 return 442 } 443 444 total := len(commits) 445 446 resp := types.RepoLogResponse{ 447 Commits: commits, 448 Ref: ref, 449 Description: getDescription(path), 450 Log: true, 451 Total: total, 452 Page: page, 453 PerPage: pageSize, 454 } 455 456 writeJSON(w, resp) 457 return 458} 459 460func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) { 461 ref := chi.URLParam(r, "ref") 462 ref, _ = url.PathUnescape(ref) 463 464 l := h.l.With("handler", "Diff", "ref", ref) 465 466 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 467 gr, err := git.Open(path, ref) 468 if err != nil { 469 notFound(w) 470 return 471 } 472 473 diff, err := gr.Diff() 474 if err != nil { 475 writeError(w, err.Error(), http.StatusInternalServerError) 476 l.Error("getting diff", "error", err.Error()) 477 return 478 } 479 480 resp := types.RepoCommitResponse{ 481 Ref: ref, 482 Diff: diff, 483 } 484 485 writeJSON(w, resp) 486 return 487} 488 489func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) { 490 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 491 l := h.l.With("handler", "Refs") 492 493 gr, err := git.Open(path, "") 494 if err != nil { 495 notFound(w) 496 return 497 } 498 499 tags, err := gr.Tags() 500 if err != nil { 501 // Non-fatal, we *should* have at least one branch to show. 502 l.Warn("getting tags", "error", err.Error()) 503 } 504 505 rtags := []*types.TagReference{} 506 for _, tag := range tags { 507 var target *object.Tag 508 if tag.Target != plumbing.ZeroHash { 509 target = &tag 510 } 511 tr := types.TagReference{ 512 Tag: target, 513 } 514 515 tr.Reference = types.Reference{ 516 Name: tag.Name, 517 Hash: tag.Hash.String(), 518 } 519 520 if tag.Message != "" { 521 tr.Message = tag.Message 522 } 523 524 rtags = append(rtags, &tr) 525 } 526 527 resp := types.RepoTagsResponse{ 528 Tags: rtags, 529 } 530 531 writeJSON(w, resp) 532 return 533} 534 535func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) { 536 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 537 538 gr, err := git.PlainOpen(path) 539 if err != nil { 540 notFound(w) 541 return 542 } 543 544 branches, _ := gr.Branches() 545 546 resp := types.RepoBranchesResponse{ 547 Branches: branches, 548 } 549 550 writeJSON(w, resp) 551 return 552} 553 554func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) { 555 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 556 branchName := chi.URLParam(r, "branch") 557 branchName, _ = url.PathUnescape(branchName) 558 559 l := h.l.With("handler", "Branch") 560 561 gr, err := git.PlainOpen(path) 562 if err != nil { 563 notFound(w) 564 return 565 } 566 567 ref, err := gr.Branch(branchName) 568 if err != nil { 569 l.Error("getting branch", "error", err.Error()) 570 writeError(w, err.Error(), http.StatusInternalServerError) 571 return 572 } 573 574 commit, err := gr.Commit(ref.Hash()) 575 if err != nil { 576 l.Error("getting commit object", "error", err.Error()) 577 writeError(w, err.Error(), http.StatusInternalServerError) 578 return 579 } 580 581 defaultBranch, err := gr.FindMainBranch() 582 isDefault := false 583 if err != nil { 584 l.Error("getting default branch", "error", err.Error()) 585 // do not quit though 586 } else if defaultBranch == branchName { 587 isDefault = true 588 } 589 590 resp := types.RepoBranchResponse{ 591 Branch: types.Branch{ 592 Reference: types.Reference{ 593 Name: ref.Name().Short(), 594 Hash: ref.Hash().String(), 595 }, 596 Commit: commit, 597 IsDefault: isDefault, 598 }, 599 } 600 601 writeJSON(w, resp) 602 return 603} 604 605func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 606 l := h.l.With("handler", "Keys") 607 608 switch r.Method { 609 case http.MethodGet: 610 keys, err := h.db.GetAllPublicKeys() 611 if err != nil { 612 writeError(w, err.Error(), http.StatusInternalServerError) 613 l.Error("getting public keys", "error", err.Error()) 614 return 615 } 616 617 data := make([]map[string]any, 0) 618 for _, key := range keys { 619 j := key.JSON() 620 data = append(data, j) 621 } 622 writeJSON(w, data) 623 return 624 625 case http.MethodPut: 626 pk := db.PublicKey{} 627 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil { 628 writeError(w, "invalid request body", http.StatusBadRequest) 629 return 630 } 631 632 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key)) 633 if err != nil { 634 writeError(w, "invalid pubkey", http.StatusBadRequest) 635 } 636 637 if err := h.db.AddPublicKey(pk); err != nil { 638 writeError(w, err.Error(), http.StatusInternalServerError) 639 l.Error("adding public key", "error", err.Error()) 640 return 641 } 642 643 w.WriteHeader(http.StatusNoContent) 644 return 645 } 646} 647 648func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) { 649 l := h.l.With("handler", "RepoForkSync") 650 651 data := struct { 652 Did string `json:"did"` 653 Source string `json:"source"` 654 Name string `json:"name,omitempty"` 655 HiddenRef string `json:"hiddenref"` 656 }{} 657 658 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 659 writeError(w, "invalid request body", http.StatusBadRequest) 660 return 661 } 662 663 did := data.Did 664 source := data.Source 665 666 if did == "" || source == "" { 667 l.Error("invalid request body, empty did or name") 668 w.WriteHeader(http.StatusBadRequest) 669 return 670 } 671 672 var name string 673 if data.Name != "" { 674 name = data.Name 675 } else { 676 name = filepath.Base(source) 677 } 678 679 branch := chi.URLParam(r, "branch") 680 branch, _ = url.PathUnescape(branch) 681 682 relativeRepoPath := filepath.Join(did, name) 683 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 684 685 gr, err := git.PlainOpen(repoPath) 686 if err != nil { 687 log.Println(err) 688 notFound(w) 689 return 690 } 691 692 forkCommit, err := gr.ResolveRevision(branch) 693 if err != nil { 694 l.Error("error resolving ref revision", "msg", err.Error()) 695 writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest) 696 return 697 } 698 699 sourceCommit, err := gr.ResolveRevision(data.HiddenRef) 700 if err != nil { 701 l.Error("error resolving hidden ref revision", "msg", err.Error()) 702 writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest) 703 return 704 } 705 706 status := types.UpToDate 707 if forkCommit.Hash.String() != sourceCommit.Hash.String() { 708 isAncestor, err := forkCommit.IsAncestor(sourceCommit) 709 if err != nil { 710 log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err) 711 return 712 } 713 714 if isAncestor { 715 status = types.FastForwardable 716 } else { 717 status = types.Conflict 718 } 719 } 720 721 w.Header().Set("Content-Type", "application/json") 722 json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status}) 723} 724 725func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) { 726 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 727 ref := chi.URLParam(r, "ref") 728 ref, _ = url.PathUnescape(ref) 729 730 l := h.l.With("handler", "RepoLanguages") 731 732 gr, err := git.Open(repoPath, ref) 733 if err != nil { 734 l.Error("opening repo", "error", err.Error()) 735 notFound(w) 736 return 737 } 738 739 ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second) 740 defer cancel() 741 742 sizes, err := gr.AnalyzeLanguages(ctx) 743 if err != nil { 744 l.Error("failed to analyze languages", "error", err.Error()) 745 writeError(w, err.Error(), http.StatusNoContent) 746 return 747 } 748 749 resp := types.RepoLanguageResponse{Languages: sizes} 750 751 writeJSON(w, resp) 752} 753 754func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) { 755 l := h.l.With("handler", "RepoForkSync") 756 757 data := struct { 758 Did string `json:"did"` 759 Source string `json:"source"` 760 Name string `json:"name,omitempty"` 761 }{} 762 763 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 764 writeError(w, "invalid request body", http.StatusBadRequest) 765 return 766 } 767 768 did := data.Did 769 source := data.Source 770 771 if did == "" || source == "" { 772 l.Error("invalid request body, empty did or name") 773 w.WriteHeader(http.StatusBadRequest) 774 return 775 } 776 777 var name string 778 if data.Name != "" { 779 name = data.Name 780 } else { 781 name = filepath.Base(source) 782 } 783 784 branch := chi.URLParam(r, "branch") 785 branch, _ = url.PathUnescape(branch) 786 787 relativeRepoPath := filepath.Join(did, name) 788 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 789 790 gr, err := git.PlainOpen(repoPath) 791 if err != nil { 792 log.Println(err) 793 notFound(w) 794 return 795 } 796 797 err = gr.Sync(branch) 798 if err != nil { 799 l.Error("error syncing repo fork", "error", err.Error()) 800 writeError(w, err.Error(), http.StatusInternalServerError) 801 return 802 } 803 804 w.WriteHeader(http.StatusNoContent) 805} 806 807func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) { 808 l := h.l.With("handler", "RepoFork") 809 810 data := struct { 811 Did string `json:"did"` 812 Source string `json:"source"` 813 Name string `json:"name,omitempty"` 814 }{} 815 816 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 817 writeError(w, "invalid request body", http.StatusBadRequest) 818 return 819 } 820 821 did := data.Did 822 source := data.Source 823 824 if did == "" || source == "" { 825 l.Error("invalid request body, empty did or name") 826 w.WriteHeader(http.StatusBadRequest) 827 return 828 } 829 830 var name string 831 if data.Name != "" { 832 name = data.Name 833 } else { 834 name = filepath.Base(source) 835 } 836 837 relativeRepoPath := filepath.Join(did, name) 838 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 839 840 err := git.Fork(repoPath, source) 841 if err != nil { 842 l.Error("forking repo", "error", err.Error()) 843 writeError(w, err.Error(), http.StatusInternalServerError) 844 return 845 } 846 847 // add perms for this user to access the repo 848 err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath) 849 if err != nil { 850 l.Error("adding repo permissions", "error", err.Error()) 851 writeError(w, err.Error(), http.StatusInternalServerError) 852 return 853 } 854 855 hook.SetupRepo( 856 hook.Config( 857 hook.WithScanPath(h.c.Repo.ScanPath), 858 hook.WithInternalApi(h.c.Server.InternalListenAddr), 859 ), 860 repoPath, 861 ) 862 863 w.WriteHeader(http.StatusNoContent) 864} 865 866func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 867 l := h.l.With("handler", "RemoveRepo") 868 869 data := struct { 870 Did string `json:"did"` 871 Name string `json:"name"` 872 }{} 873 874 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 875 writeError(w, "invalid request body", http.StatusBadRequest) 876 return 877 } 878 879 did := data.Did 880 name := data.Name 881 882 if did == "" || name == "" { 883 l.Error("invalid request body, empty did or name") 884 w.WriteHeader(http.StatusBadRequest) 885 return 886 } 887 888 relativeRepoPath := filepath.Join(did, name) 889 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 890 err := os.RemoveAll(repoPath) 891 if err != nil { 892 l.Error("removing repo", "error", err.Error()) 893 writeError(w, err.Error(), http.StatusInternalServerError) 894 return 895 } 896 897 w.WriteHeader(http.StatusNoContent) 898 899} 900func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) { 901 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 902 903 data := types.MergeRequest{} 904 905 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 906 writeError(w, err.Error(), http.StatusBadRequest) 907 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err) 908 return 909 } 910 911 mo := &git.MergeOptions{ 912 AuthorName: data.AuthorName, 913 AuthorEmail: data.AuthorEmail, 914 CommitBody: data.CommitBody, 915 CommitMessage: data.CommitMessage, 916 } 917 918 patch := data.Patch 919 branch := data.Branch 920 gr, err := git.Open(path, branch) 921 if err != nil { 922 notFound(w) 923 return 924 } 925 926 mo.FormatPatch = patchutil.IsFormatPatch(patch) 927 928 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil { 929 var mergeErr *git.ErrMerge 930 if errors.As(err, &mergeErr) { 931 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 932 for i, conflict := range mergeErr.Conflicts { 933 conflicts[i] = types.ConflictInfo{ 934 Filename: conflict.Filename, 935 Reason: conflict.Reason, 936 } 937 } 938 response := types.MergeCheckResponse{ 939 IsConflicted: true, 940 Conflicts: conflicts, 941 Message: mergeErr.Message, 942 } 943 writeConflict(w, response) 944 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr) 945 } else { 946 writeError(w, err.Error(), http.StatusBadRequest) 947 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error()) 948 } 949 return 950 } 951 952 w.WriteHeader(http.StatusOK) 953} 954 955func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) { 956 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 957 958 var data struct { 959 Patch string `json:"patch"` 960 Branch string `json:"branch"` 961 } 962 963 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 964 writeError(w, err.Error(), http.StatusBadRequest) 965 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err) 966 return 967 } 968 969 patch := data.Patch 970 branch := data.Branch 971 gr, err := git.Open(path, branch) 972 if err != nil { 973 notFound(w) 974 return 975 } 976 977 err = gr.MergeCheck([]byte(patch), branch) 978 if err == nil { 979 response := types.MergeCheckResponse{ 980 IsConflicted: false, 981 } 982 writeJSON(w, response) 983 return 984 } 985 986 var mergeErr *git.ErrMerge 987 if errors.As(err, &mergeErr) { 988 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 989 for i, conflict := range mergeErr.Conflicts { 990 conflicts[i] = types.ConflictInfo{ 991 Filename: conflict.Filename, 992 Reason: conflict.Reason, 993 } 994 } 995 response := types.MergeCheckResponse{ 996 IsConflicted: true, 997 Conflicts: conflicts, 998 Message: mergeErr.Message, 999 } 1000 writeConflict(w, response) 1001 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error()) 1002 return 1003 } 1004 writeError(w, err.Error(), http.StatusInternalServerError) 1005 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error()) 1006} 1007 1008func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) { 1009 rev1 := chi.URLParam(r, "rev1") 1010 rev1, _ = url.PathUnescape(rev1) 1011 1012 rev2 := chi.URLParam(r, "rev2") 1013 rev2, _ = url.PathUnescape(rev2) 1014 1015 l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2) 1016 1017 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1018 gr, err := git.PlainOpen(path) 1019 if err != nil { 1020 notFound(w) 1021 return 1022 } 1023 1024 commit1, err := gr.ResolveRevision(rev1) 1025 if err != nil { 1026 l.Error("error resolving revision 1", "msg", err.Error()) 1027 writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest) 1028 return 1029 } 1030 1031 commit2, err := gr.ResolveRevision(rev2) 1032 if err != nil { 1033 l.Error("error resolving revision 2", "msg", err.Error()) 1034 writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest) 1035 return 1036 } 1037 1038 rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2) 1039 if err != nil { 1040 l.Error("error comparing revisions", "msg", err.Error()) 1041 writeError(w, "error comparing revisions", http.StatusBadRequest) 1042 return 1043 } 1044 1045 writeJSON(w, types.RepoFormatPatchResponse{ 1046 Rev1: commit1.Hash.String(), 1047 Rev2: commit2.Hash.String(), 1048 FormatPatch: formatPatch, 1049 Patch: rawPatch, 1050 }) 1051 return 1052} 1053 1054func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) { 1055 l := h.l.With("handler", "NewHiddenRef") 1056 1057 forkRef := chi.URLParam(r, "forkRef") 1058 forkRef, _ = url.PathUnescape(forkRef) 1059 1060 remoteRef := chi.URLParam(r, "remoteRef") 1061 remoteRef, _ = url.PathUnescape(remoteRef) 1062 1063 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1064 gr, err := git.PlainOpen(path) 1065 if err != nil { 1066 notFound(w) 1067 return 1068 } 1069 1070 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef) 1071 if err != nil { 1072 l.Error("error tracking hidden remote ref", "msg", err.Error()) 1073 writeError(w, "error tracking hidden remote ref", http.StatusBadRequest) 1074 return 1075 } 1076 1077 w.WriteHeader(http.StatusNoContent) 1078 return 1079} 1080 1081func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { 1082 l := h.l.With("handler", "AddMember") 1083 1084 data := struct { 1085 Did string `json:"did"` 1086 }{} 1087 1088 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1089 writeError(w, "invalid request body", http.StatusBadRequest) 1090 return 1091 } 1092 1093 did := data.Did 1094 1095 if err := h.db.AddDid(did); err != nil { 1096 l.Error("adding did", "error", err.Error()) 1097 writeError(w, err.Error(), http.StatusInternalServerError) 1098 return 1099 } 1100 h.jc.AddDid(did) 1101 1102 if err := h.e.AddKnotMember(rbac.ThisServer, did); err != nil { 1103 l.Error("adding member", "error", err.Error()) 1104 writeError(w, err.Error(), http.StatusInternalServerError) 1105 return 1106 } 1107 1108 if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 1109 l.Error("fetching and adding keys", "error", err.Error()) 1110 writeError(w, err.Error(), http.StatusInternalServerError) 1111 return 1112 } 1113 1114 w.WriteHeader(http.StatusNoContent) 1115} 1116 1117func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 1118 l := h.l.With("handler", "AddRepoCollaborator") 1119 1120 data := struct { 1121 Did string `json:"did"` 1122 }{} 1123 1124 ownerDid := chi.URLParam(r, "did") 1125 repo := chi.URLParam(r, "name") 1126 1127 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1128 writeError(w, "invalid request body", http.StatusBadRequest) 1129 return 1130 } 1131 1132 if err := h.db.AddDid(data.Did); err != nil { 1133 l.Error("adding did", "error", err.Error()) 1134 writeError(w, err.Error(), http.StatusInternalServerError) 1135 return 1136 } 1137 h.jc.AddDid(data.Did) 1138 1139 repoName, _ := securejoin.SecureJoin(ownerDid, repo) 1140 if err := h.e.AddCollaborator(data.Did, rbac.ThisServer, repoName); err != nil { 1141 l.Error("adding repo collaborator", "error", err.Error()) 1142 writeError(w, err.Error(), http.StatusInternalServerError) 1143 return 1144 } 1145 1146 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1147 l.Error("fetching and adding keys", "error", err.Error()) 1148 writeError(w, err.Error(), http.StatusInternalServerError) 1149 return 1150 } 1151 1152 w.WriteHeader(http.StatusNoContent) 1153} 1154 1155func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) { 1156 l := h.l.With("handler", "DefaultBranch") 1157 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1158 1159 gr, err := git.Open(path, "") 1160 if err != nil { 1161 notFound(w) 1162 return 1163 } 1164 1165 branch, err := gr.FindMainBranch() 1166 if err != nil { 1167 writeError(w, err.Error(), http.StatusInternalServerError) 1168 l.Error("getting default branch", "error", err.Error()) 1169 return 1170 } 1171 1172 writeJSON(w, types.RepoDefaultBranchResponse{ 1173 Branch: branch, 1174 }) 1175} 1176 1177func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1178 l := h.l.With("handler", "SetDefaultBranch") 1179 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1180 1181 data := struct { 1182 Branch string `json:"branch"` 1183 }{} 1184 1185 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1186 writeError(w, err.Error(), http.StatusBadRequest) 1187 return 1188 } 1189 1190 gr, err := git.PlainOpen(path) 1191 if err != nil { 1192 notFound(w) 1193 return 1194 } 1195 1196 err = gr.SetDefaultBranch(data.Branch) 1197 if err != nil { 1198 writeError(w, err.Error(), http.StatusInternalServerError) 1199 l.Error("setting default branch", "error", err.Error()) 1200 return 1201 } 1202 1203 w.WriteHeader(http.StatusNoContent) 1204} 1205 1206func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1207 w.Write([]byte("ok")) 1208} 1209 1210func validateRepoName(name string) error { 1211 // check for path traversal attempts 1212 if name == "." || name == ".." || 1213 strings.Contains(name, "/") || strings.Contains(name, "\\") { 1214 return fmt.Errorf("Repository name contains invalid path characters") 1215 } 1216 1217 // check for sequences that could be used for traversal when normalized 1218 if strings.Contains(name, "./") || strings.Contains(name, "../") || 1219 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 1220 return fmt.Errorf("Repository name contains invalid path sequence") 1221 } 1222 1223 // then continue with character validation 1224 for _, char := range name { 1225 if !((char >= 'a' && char <= 'z') || 1226 (char >= 'A' && char <= 'Z') || 1227 (char >= '0' && char <= '9') || 1228 char == '-' || char == '_' || char == '.') { 1229 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 1230 } 1231 } 1232 1233 // additional check to prevent multiple sequential dots 1234 if strings.Contains(name, "..") { 1235 return fmt.Errorf("Repository name cannot contain sequential dots") 1236 } 1237 1238 // if all checks pass 1239 return nil 1240}