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