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