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