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