forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
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 contentHash := sha256.Sum256(contents) 287 eTag := fmt.Sprintf("\"%x\"", contentHash) 288 289 // allow image, video, and text/plain files to be served directly 290 switch { 291 case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 292 if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 293 w.WriteHeader(http.StatusNotModified) 294 return 295 } 296 w.Header().Set("ETag", eTag) 297 298 case strings.HasPrefix(mimeType, "text/plain"): 299 w.Header().Set("Cache-Control", "public, no-cache") 300 301 default: 302 l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 303 writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) 304 return 305 } 306 307 w.Header().Set("Content-Type", mimeType) 308 w.Write(contents) 309} 310 311func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { 312 treePath := chi.URLParam(r, "*") 313 ref := chi.URLParam(r, "ref") 314 ref, _ = url.PathUnescape(ref) 315 316 l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath) 317 318 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 319 gr, err := git.Open(path, ref) 320 if err != nil { 321 notFound(w) 322 return 323 } 324 325 var isBinaryFile bool = false 326 contents, err := gr.FileContent(treePath) 327 if errors.Is(err, git.ErrBinaryFile) { 328 isBinaryFile = true 329 } else if errors.Is(err, object.ErrFileNotFound) { 330 notFound(w) 331 return 332 } else if err != nil { 333 writeError(w, err.Error(), http.StatusInternalServerError) 334 return 335 } 336 337 bytes := []byte(contents) 338 // safe := string(sanitize(bytes)) 339 sizeHint := len(bytes) 340 341 resp := types.RepoBlobResponse{ 342 Ref: ref, 343 Contents: string(bytes), 344 Path: treePath, 345 IsBinary: isBinaryFile, 346 SizeHint: uint64(sizeHint), 347 } 348 349 h.showFile(resp, w, l) 350} 351 352func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) { 353 name := chi.URLParam(r, "name") 354 file := chi.URLParam(r, "file") 355 356 l := h.l.With("handler", "Archive", "name", name, "file", file) 357 358 // TODO: extend this to add more files compression (e.g.: xz) 359 if !strings.HasSuffix(file, ".tar.gz") { 360 notFound(w) 361 return 362 } 363 364 ref := strings.TrimSuffix(file, ".tar.gz") 365 366 unescapedRef, err := url.PathUnescape(ref) 367 if err != nil { 368 notFound(w) 369 return 370 } 371 372 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-") 373 374 // This allows the browser to use a proper name for the file when 375 // downloading 376 filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename) 377 setContentDisposition(w, filename) 378 setGZipMIME(w) 379 380 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 381 gr, err := git.Open(path, unescapedRef) 382 if err != nil { 383 notFound(w) 384 return 385 } 386 387 gw := gzip.NewWriter(w) 388 defer gw.Close() 389 390 prefix := fmt.Sprintf("%s-%s", name, safeRefFilename) 391 err = gr.WriteTar(gw, prefix) 392 if err != nil { 393 // once we start writing to the body we can't report error anymore 394 // so we are only left with printing the error. 395 l.Error("writing tar file", "error", err.Error()) 396 return 397 } 398 399 err = gw.Flush() 400 if err != nil { 401 // once we start writing to the body we can't report error anymore 402 // so we are only left with printing the error. 403 l.Error("flushing?", "error", err.Error()) 404 return 405 } 406} 407 408func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 409 ref := chi.URLParam(r, "ref") 410 ref, _ = url.PathUnescape(ref) 411 412 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 413 414 l := h.l.With("handler", "Log", "ref", ref, "path", path) 415 416 gr, err := git.Open(path, ref) 417 if err != nil { 418 notFound(w) 419 return 420 } 421 422 // Get page parameters 423 page := 1 424 pageSize := 30 425 426 if pageParam := r.URL.Query().Get("page"); pageParam != "" { 427 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 { 428 page = p 429 } 430 } 431 432 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" { 433 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 { 434 pageSize = ps 435 } 436 } 437 438 // convert to offset/limit 439 offset := (page - 1) * pageSize 440 limit := pageSize 441 442 commits, err := gr.Commits(offset, limit) 443 if err != nil { 444 writeError(w, err.Error(), http.StatusInternalServerError) 445 l.Error("fetching commits", "error", err.Error()) 446 return 447 } 448 449 total := len(commits) 450 451 resp := types.RepoLogResponse{ 452 Commits: commits, 453 Ref: ref, 454 Description: getDescription(path), 455 Log: true, 456 Total: total, 457 Page: page, 458 PerPage: pageSize, 459 } 460 461 writeJSON(w, resp) 462 return 463} 464 465func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) { 466 ref := chi.URLParam(r, "ref") 467 ref, _ = url.PathUnescape(ref) 468 469 l := h.l.With("handler", "Diff", "ref", ref) 470 471 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 472 gr, err := git.Open(path, ref) 473 if err != nil { 474 notFound(w) 475 return 476 } 477 478 diff, err := gr.Diff() 479 if err != nil { 480 writeError(w, err.Error(), http.StatusInternalServerError) 481 l.Error("getting diff", "error", err.Error()) 482 return 483 } 484 485 resp := types.RepoCommitResponse{ 486 Ref: ref, 487 Diff: diff, 488 } 489 490 writeJSON(w, resp) 491 return 492} 493 494func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) { 495 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 496 l := h.l.With("handler", "Refs") 497 498 gr, err := git.Open(path, "") 499 if err != nil { 500 notFound(w) 501 return 502 } 503 504 tags, err := gr.Tags() 505 if err != nil { 506 // Non-fatal, we *should* have at least one branch to show. 507 l.Warn("getting tags", "error", err.Error()) 508 } 509 510 rtags := []*types.TagReference{} 511 for _, tag := range tags { 512 var target *object.Tag 513 if tag.Target != plumbing.ZeroHash { 514 target = &tag 515 } 516 tr := types.TagReference{ 517 Tag: target, 518 } 519 520 tr.Reference = types.Reference{ 521 Name: tag.Name, 522 Hash: tag.Hash.String(), 523 } 524 525 if tag.Message != "" { 526 tr.Message = tag.Message 527 } 528 529 rtags = append(rtags, &tr) 530 } 531 532 resp := types.RepoTagsResponse{ 533 Tags: rtags, 534 } 535 536 writeJSON(w, resp) 537 return 538} 539 540func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) { 541 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 542 543 gr, err := git.PlainOpen(path) 544 if err != nil { 545 notFound(w) 546 return 547 } 548 549 branches, _ := gr.Branches() 550 551 resp := types.RepoBranchesResponse{ 552 Branches: branches, 553 } 554 555 writeJSON(w, resp) 556 return 557} 558 559func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) { 560 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 561 branchName := chi.URLParam(r, "branch") 562 branchName, _ = url.PathUnescape(branchName) 563 564 l := h.l.With("handler", "Branch") 565 566 gr, err := git.PlainOpen(path) 567 if err != nil { 568 notFound(w) 569 return 570 } 571 572 ref, err := gr.Branch(branchName) 573 if err != nil { 574 l.Error("getting branch", "error", err.Error()) 575 writeError(w, err.Error(), http.StatusInternalServerError) 576 return 577 } 578 579 commit, err := gr.Commit(ref.Hash()) 580 if err != nil { 581 l.Error("getting commit object", "error", err.Error()) 582 writeError(w, err.Error(), http.StatusInternalServerError) 583 return 584 } 585 586 defaultBranch, err := gr.FindMainBranch() 587 isDefault := false 588 if err != nil { 589 l.Error("getting default branch", "error", err.Error()) 590 // do not quit though 591 } else if defaultBranch == branchName { 592 isDefault = true 593 } 594 595 resp := types.RepoBranchResponse{ 596 Branch: types.Branch{ 597 Reference: types.Reference{ 598 Name: ref.Name().Short(), 599 Hash: ref.Hash().String(), 600 }, 601 Commit: commit, 602 IsDefault: isDefault, 603 }, 604 } 605 606 writeJSON(w, resp) 607 return 608} 609 610func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 611 l := h.l.With("handler", "Keys") 612 613 switch r.Method { 614 case http.MethodGet: 615 keys, err := h.db.GetAllPublicKeys() 616 if err != nil { 617 writeError(w, err.Error(), http.StatusInternalServerError) 618 l.Error("getting public keys", "error", err.Error()) 619 return 620 } 621 622 data := make([]map[string]any, 0) 623 for _, key := range keys { 624 j := key.JSON() 625 data = append(data, j) 626 } 627 writeJSON(w, data) 628 return 629 630 case http.MethodPut: 631 pk := db.PublicKey{} 632 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil { 633 writeError(w, "invalid request body", http.StatusBadRequest) 634 return 635 } 636 637 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key)) 638 if err != nil { 639 writeError(w, "invalid pubkey", http.StatusBadRequest) 640 } 641 642 if err := h.db.AddPublicKey(pk); err != nil { 643 writeError(w, err.Error(), http.StatusInternalServerError) 644 l.Error("adding public key", "error", err.Error()) 645 return 646 } 647 648 w.WriteHeader(http.StatusNoContent) 649 return 650 } 651} 652 653func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) { 654 l := h.l.With("handler", "RepoForkAheadBehind") 655 656 data := struct { 657 Did string `json:"did"` 658 Source string `json:"source"` 659 Name string `json:"name,omitempty"` 660 HiddenRef string `json:"hiddenref"` 661 }{} 662 663 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 664 writeError(w, "invalid request body", http.StatusBadRequest) 665 return 666 } 667 668 did := data.Did 669 source := data.Source 670 671 if did == "" || source == "" { 672 l.Error("invalid request body, empty did or name") 673 w.WriteHeader(http.StatusBadRequest) 674 return 675 } 676 677 var name string 678 if data.Name != "" { 679 name = data.Name 680 } else { 681 name = filepath.Base(source) 682 } 683 684 branch := chi.URLParam(r, "branch") 685 branch, _ = url.PathUnescape(branch) 686 687 relativeRepoPath := filepath.Join(did, name) 688 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 689 690 gr, err := git.PlainOpen(repoPath) 691 if err != nil { 692 log.Println(err) 693 notFound(w) 694 return 695 } 696 697 forkCommit, err := gr.ResolveRevision(branch) 698 if err != nil { 699 l.Error("error resolving ref revision", "msg", err.Error()) 700 writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest) 701 return 702 } 703 704 sourceCommit, err := gr.ResolveRevision(data.HiddenRef) 705 if err != nil { 706 l.Error("error resolving hidden ref revision", "msg", err.Error()) 707 writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest) 708 return 709 } 710 711 status := types.UpToDate 712 if forkCommit.Hash.String() != sourceCommit.Hash.String() { 713 isAncestor, err := forkCommit.IsAncestor(sourceCommit) 714 if err != nil { 715 log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err) 716 return 717 } 718 719 if isAncestor { 720 status = types.FastForwardable 721 } else { 722 status = types.Conflict 723 } 724 } 725 726 w.Header().Set("Content-Type", "application/json") 727 json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status}) 728} 729 730func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) { 731 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 732 ref := chi.URLParam(r, "ref") 733 ref, _ = url.PathUnescape(ref) 734 735 l := h.l.With("handler", "RepoLanguages") 736 737 gr, err := git.Open(repoPath, ref) 738 if err != nil { 739 l.Error("opening repo", "error", err.Error()) 740 notFound(w) 741 return 742 } 743 744 ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second) 745 defer cancel() 746 747 sizes, err := gr.AnalyzeLanguages(ctx) 748 if err != nil { 749 l.Error("failed to analyze languages", "error", err.Error()) 750 writeError(w, err.Error(), http.StatusNoContent) 751 return 752 } 753 754 resp := types.RepoLanguageResponse{Languages: sizes} 755 756 writeJSON(w, resp) 757} 758 759func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) { 760 l := h.l.With("handler", "RepoForkSync") 761 762 data := struct { 763 Did string `json:"did"` 764 Source string `json:"source"` 765 Name string `json:"name,omitempty"` 766 }{} 767 768 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 769 writeError(w, "invalid request body", http.StatusBadRequest) 770 return 771 } 772 773 did := data.Did 774 source := data.Source 775 776 if did == "" || source == "" { 777 l.Error("invalid request body, empty did or name") 778 w.WriteHeader(http.StatusBadRequest) 779 return 780 } 781 782 var name string 783 if data.Name != "" { 784 name = data.Name 785 } else { 786 name = filepath.Base(source) 787 } 788 789 branch := chi.URLParam(r, "*") 790 branch, _ = url.PathUnescape(branch) 791 792 relativeRepoPath := filepath.Join(did, name) 793 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 794 795 gr, err := git.Open(repoPath, branch) 796 if err != nil { 797 log.Println(err) 798 notFound(w) 799 return 800 } 801 802 err = gr.Sync() 803 if err != nil { 804 l.Error("error syncing repo fork", "error", err.Error()) 805 writeError(w, err.Error(), http.StatusInternalServerError) 806 return 807 } 808 809 w.WriteHeader(http.StatusNoContent) 810} 811 812func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) { 813 l := h.l.With("handler", "RepoFork") 814 815 data := struct { 816 Did string `json:"did"` 817 Source string `json:"source"` 818 Name string `json:"name,omitempty"` 819 }{} 820 821 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 822 writeError(w, "invalid request body", http.StatusBadRequest) 823 return 824 } 825 826 did := data.Did 827 source := data.Source 828 829 if did == "" || source == "" { 830 l.Error("invalid request body, empty did or name") 831 w.WriteHeader(http.StatusBadRequest) 832 return 833 } 834 835 var name string 836 if data.Name != "" { 837 name = data.Name 838 } else { 839 name = filepath.Base(source) 840 } 841 842 relativeRepoPath := filepath.Join(did, name) 843 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 844 845 err := git.Fork(repoPath, source) 846 if err != nil { 847 l.Error("forking repo", "error", err.Error()) 848 writeError(w, err.Error(), http.StatusInternalServerError) 849 return 850 } 851 852 // add perms for this user to access the repo 853 err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath) 854 if err != nil { 855 l.Error("adding repo permissions", "error", err.Error()) 856 writeError(w, err.Error(), http.StatusInternalServerError) 857 return 858 } 859 860 hook.SetupRepo( 861 hook.Config( 862 hook.WithScanPath(h.c.Repo.ScanPath), 863 hook.WithInternalApi(h.c.Server.InternalListenAddr), 864 ), 865 repoPath, 866 ) 867 868 w.WriteHeader(http.StatusNoContent) 869} 870 871func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 872 l := h.l.With("handler", "RemoveRepo") 873 874 data := struct { 875 Did string `json:"did"` 876 Name string `json:"name"` 877 }{} 878 879 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 880 writeError(w, "invalid request body", http.StatusBadRequest) 881 return 882 } 883 884 did := data.Did 885 name := data.Name 886 887 if did == "" || name == "" { 888 l.Error("invalid request body, empty did or name") 889 w.WriteHeader(http.StatusBadRequest) 890 return 891 } 892 893 relativeRepoPath := filepath.Join(did, name) 894 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 895 err := os.RemoveAll(repoPath) 896 if err != nil { 897 l.Error("removing repo", "error", err.Error()) 898 writeError(w, err.Error(), http.StatusInternalServerError) 899 return 900 } 901 902 w.WriteHeader(http.StatusNoContent) 903 904} 905func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) { 906 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 907 908 data := types.MergeRequest{} 909 910 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 911 writeError(w, err.Error(), http.StatusBadRequest) 912 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err) 913 return 914 } 915 916 mo := &git.MergeOptions{ 917 AuthorName: data.AuthorName, 918 AuthorEmail: data.AuthorEmail, 919 CommitBody: data.CommitBody, 920 CommitMessage: data.CommitMessage, 921 } 922 923 patch := data.Patch 924 branch := data.Branch 925 gr, err := git.Open(path, branch) 926 if err != nil { 927 notFound(w) 928 return 929 } 930 931 mo.FormatPatch = patchutil.IsFormatPatch(patch) 932 933 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil { 934 var mergeErr *git.ErrMerge 935 if errors.As(err, &mergeErr) { 936 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 937 for i, conflict := range mergeErr.Conflicts { 938 conflicts[i] = types.ConflictInfo{ 939 Filename: conflict.Filename, 940 Reason: conflict.Reason, 941 } 942 } 943 response := types.MergeCheckResponse{ 944 IsConflicted: true, 945 Conflicts: conflicts, 946 Message: mergeErr.Message, 947 } 948 writeConflict(w, response) 949 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr) 950 } else { 951 writeError(w, err.Error(), http.StatusBadRequest) 952 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error()) 953 } 954 return 955 } 956 957 w.WriteHeader(http.StatusOK) 958} 959 960func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) { 961 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 962 963 var data struct { 964 Patch string `json:"patch"` 965 Branch string `json:"branch"` 966 } 967 968 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 969 writeError(w, err.Error(), http.StatusBadRequest) 970 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err) 971 return 972 } 973 974 patch := data.Patch 975 branch := data.Branch 976 gr, err := git.Open(path, branch) 977 if err != nil { 978 notFound(w) 979 return 980 } 981 982 err = gr.MergeCheck([]byte(patch), branch) 983 if err == nil { 984 response := types.MergeCheckResponse{ 985 IsConflicted: false, 986 } 987 writeJSON(w, response) 988 return 989 } 990 991 var mergeErr *git.ErrMerge 992 if errors.As(err, &mergeErr) { 993 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 994 for i, conflict := range mergeErr.Conflicts { 995 conflicts[i] = types.ConflictInfo{ 996 Filename: conflict.Filename, 997 Reason: conflict.Reason, 998 } 999 } 1000 response := types.MergeCheckResponse{ 1001 IsConflicted: true, 1002 Conflicts: conflicts, 1003 Message: mergeErr.Message, 1004 } 1005 writeConflict(w, response) 1006 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error()) 1007 return 1008 } 1009 writeError(w, err.Error(), http.StatusInternalServerError) 1010 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error()) 1011} 1012 1013func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) { 1014 rev1 := chi.URLParam(r, "rev1") 1015 rev1, _ = url.PathUnescape(rev1) 1016 1017 rev2 := chi.URLParam(r, "rev2") 1018 rev2, _ = url.PathUnescape(rev2) 1019 1020 l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2) 1021 1022 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1023 gr, err := git.PlainOpen(path) 1024 if err != nil { 1025 notFound(w) 1026 return 1027 } 1028 1029 commit1, err := gr.ResolveRevision(rev1) 1030 if err != nil { 1031 l.Error("error resolving revision 1", "msg", err.Error()) 1032 writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest) 1033 return 1034 } 1035 1036 commit2, err := gr.ResolveRevision(rev2) 1037 if err != nil { 1038 l.Error("error resolving revision 2", "msg", err.Error()) 1039 writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest) 1040 return 1041 } 1042 1043 rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2) 1044 if err != nil { 1045 l.Error("error comparing revisions", "msg", err.Error()) 1046 writeError(w, "error comparing revisions", http.StatusBadRequest) 1047 return 1048 } 1049 1050 writeJSON(w, types.RepoFormatPatchResponse{ 1051 Rev1: commit1.Hash.String(), 1052 Rev2: commit2.Hash.String(), 1053 FormatPatch: formatPatch, 1054 Patch: rawPatch, 1055 }) 1056 return 1057} 1058 1059func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) { 1060 l := h.l.With("handler", "NewHiddenRef") 1061 1062 forkRef := chi.URLParam(r, "forkRef") 1063 forkRef, _ = url.PathUnescape(forkRef) 1064 1065 remoteRef := chi.URLParam(r, "remoteRef") 1066 remoteRef, _ = url.PathUnescape(remoteRef) 1067 1068 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1069 gr, err := git.PlainOpen(path) 1070 if err != nil { 1071 notFound(w) 1072 return 1073 } 1074 1075 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef) 1076 if err != nil { 1077 l.Error("error tracking hidden remote ref", "msg", err.Error()) 1078 writeError(w, "error tracking hidden remote ref", http.StatusBadRequest) 1079 return 1080 } 1081 1082 w.WriteHeader(http.StatusNoContent) 1083 return 1084} 1085 1086func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { 1087 l := h.l.With("handler", "AddMember") 1088 1089 data := struct { 1090 Did string `json:"did"` 1091 }{} 1092 1093 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1094 writeError(w, "invalid request body", http.StatusBadRequest) 1095 return 1096 } 1097 1098 did := data.Did 1099 1100 if err := h.db.AddDid(did); err != nil { 1101 l.Error("adding did", "error", err.Error()) 1102 writeError(w, err.Error(), http.StatusInternalServerError) 1103 return 1104 } 1105 h.jc.AddDid(did) 1106 1107 if err := h.e.AddKnotMember(rbac.ThisServer, did); err != nil { 1108 l.Error("adding member", "error", err.Error()) 1109 writeError(w, err.Error(), http.StatusInternalServerError) 1110 return 1111 } 1112 1113 if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 1114 l.Error("fetching and adding keys", "error", err.Error()) 1115 writeError(w, err.Error(), http.StatusInternalServerError) 1116 return 1117 } 1118 1119 w.WriteHeader(http.StatusNoContent) 1120} 1121 1122func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 1123 l := h.l.With("handler", "AddRepoCollaborator") 1124 1125 data := struct { 1126 Did string `json:"did"` 1127 }{} 1128 1129 ownerDid := chi.URLParam(r, "did") 1130 repo := chi.URLParam(r, "name") 1131 1132 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1133 writeError(w, "invalid request body", http.StatusBadRequest) 1134 return 1135 } 1136 1137 if err := h.db.AddDid(data.Did); err != nil { 1138 l.Error("adding did", "error", err.Error()) 1139 writeError(w, err.Error(), http.StatusInternalServerError) 1140 return 1141 } 1142 h.jc.AddDid(data.Did) 1143 1144 repoName, _ := securejoin.SecureJoin(ownerDid, repo) 1145 if err := h.e.AddCollaborator(data.Did, rbac.ThisServer, repoName); err != nil { 1146 l.Error("adding repo collaborator", "error", err.Error()) 1147 writeError(w, err.Error(), http.StatusInternalServerError) 1148 return 1149 } 1150 1151 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1152 l.Error("fetching and adding keys", "error", err.Error()) 1153 writeError(w, err.Error(), http.StatusInternalServerError) 1154 return 1155 } 1156 1157 w.WriteHeader(http.StatusNoContent) 1158} 1159 1160func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) { 1161 l := h.l.With("handler", "DefaultBranch") 1162 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1163 1164 gr, err := git.Open(path, "") 1165 if err != nil { 1166 notFound(w) 1167 return 1168 } 1169 1170 branch, err := gr.FindMainBranch() 1171 if err != nil { 1172 writeError(w, err.Error(), http.StatusInternalServerError) 1173 l.Error("getting default branch", "error", err.Error()) 1174 return 1175 } 1176 1177 writeJSON(w, types.RepoDefaultBranchResponse{ 1178 Branch: branch, 1179 }) 1180} 1181 1182func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1183 l := h.l.With("handler", "SetDefaultBranch") 1184 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1185 1186 data := struct { 1187 Branch string `json:"branch"` 1188 }{} 1189 1190 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1191 writeError(w, err.Error(), http.StatusBadRequest) 1192 return 1193 } 1194 1195 gr, err := git.PlainOpen(path) 1196 if err != nil { 1197 notFound(w) 1198 return 1199 } 1200 1201 err = gr.SetDefaultBranch(data.Branch) 1202 if err != nil { 1203 writeError(w, err.Error(), http.StatusInternalServerError) 1204 l.Error("setting default branch", "error", err.Error()) 1205 return 1206 } 1207 1208 w.WriteHeader(http.StatusNoContent) 1209} 1210 1211func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1212 w.Write([]byte("ok")) 1213} 1214 1215func validateRepoName(name string) error { 1216 // check for path traversal attempts 1217 if name == "." || name == ".." || 1218 strings.Contains(name, "/") || strings.Contains(name, "\\") { 1219 return fmt.Errorf("Repository name contains invalid path characters") 1220 } 1221 1222 // check for sequences that could be used for traversal when normalized 1223 if strings.Contains(name, "./") || strings.Contains(name, "../") || 1224 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 1225 return fmt.Errorf("Repository name contains invalid path sequence") 1226 } 1227 1228 // then continue with character validation 1229 for _, char := range name { 1230 if !((char >= 'a' && char <= 'z') || 1231 (char >= 'A' && char <= 'Z') || 1232 (char >= '0' && char <= '9') || 1233 char == '-' || char == '_' || char == '.') { 1234 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 1235 } 1236 } 1237 1238 // additional check to prevent multiple sequential dots 1239 if strings.Contains(name, "..") { 1240 return fmt.Errorf("Repository name cannot contain sequential dots") 1241 } 1242 1243 // if all checks pass 1244 return nil 1245}