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