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