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