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 } else { 788 lang, _ = enry.GetLanguageByContent(absPath, content) 789 if len(lang) == 0 { 790 return 791 } 792 } 793 } 794 795 v, ok := languageFileCount[lang] 796 if ok { 797 languageFileCount[lang] = v + 1 798 } else { 799 languageFileCount[lang] = 1 800 } 801 }, "") 802 if err != nil { 803 l.Error("failed to recurse file tree", "error", err.Error()) 804 writeError(w, err.Error(), http.StatusNoContent) 805 return 806 } 807 808 resp := types.RepoLanguageResponse{Languages: languageFileCount} 809 810 writeJSON(w, resp) 811 return 812} 813 814func recurseEntireTree(ctx context.Context, git *git.GitRepo, callback func(absPath string), filePath string) error { 815 files, err := git.FileTree(ctx, filePath) 816 if err != nil { 817 log.Println(err) 818 return err 819 } 820 821 for _, file := range files { 822 absPath := path.Join(filePath, file.Name) 823 if !file.IsFile { 824 return recurseEntireTree(ctx, git, callback, absPath) 825 } 826 callback(absPath) 827 } 828 829 return nil 830} 831 832func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) { 833 l := h.l.With("handler", "RepoForkSync") 834 835 data := struct { 836 Did string `json:"did"` 837 Source string `json:"source"` 838 Name string `json:"name,omitempty"` 839 }{} 840 841 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 842 writeError(w, "invalid request body", http.StatusBadRequest) 843 return 844 } 845 846 did := data.Did 847 source := data.Source 848 849 if did == "" || source == "" { 850 l.Error("invalid request body, empty did or name") 851 w.WriteHeader(http.StatusBadRequest) 852 return 853 } 854 855 var name string 856 if data.Name != "" { 857 name = data.Name 858 } else { 859 name = filepath.Base(source) 860 } 861 862 branch := chi.URLParam(r, "branch") 863 branch, _ = url.PathUnescape(branch) 864 865 relativeRepoPath := filepath.Join(did, name) 866 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 867 868 gr, err := git.PlainOpen(repoPath) 869 if err != nil { 870 log.Println(err) 871 notFound(w) 872 return 873 } 874 875 err = gr.Sync(branch) 876 if err != nil { 877 l.Error("error syncing repo fork", "error", err.Error()) 878 writeError(w, err.Error(), http.StatusInternalServerError) 879 return 880 } 881 882 w.WriteHeader(http.StatusNoContent) 883} 884 885func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) { 886 l := h.l.With("handler", "RepoFork") 887 888 data := struct { 889 Did string `json:"did"` 890 Source string `json:"source"` 891 Name string `json:"name,omitempty"` 892 }{} 893 894 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 895 writeError(w, "invalid request body", http.StatusBadRequest) 896 return 897 } 898 899 did := data.Did 900 source := data.Source 901 902 if did == "" || source == "" { 903 l.Error("invalid request body, empty did or name") 904 w.WriteHeader(http.StatusBadRequest) 905 return 906 } 907 908 var name string 909 if data.Name != "" { 910 name = data.Name 911 } else { 912 name = filepath.Base(source) 913 } 914 915 relativeRepoPath := filepath.Join(did, name) 916 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 917 918 err := git.Fork(repoPath, source) 919 if err != nil { 920 l.Error("forking repo", "error", err.Error()) 921 writeError(w, err.Error(), http.StatusInternalServerError) 922 return 923 } 924 925 // add perms for this user to access the repo 926 err = h.e.AddRepo(did, ThisServer, relativeRepoPath) 927 if err != nil { 928 l.Error("adding repo permissions", "error", err.Error()) 929 writeError(w, err.Error(), http.StatusInternalServerError) 930 return 931 } 932 933 hook.SetupRepo( 934 hook.Config( 935 hook.WithScanPath(h.c.Repo.ScanPath), 936 hook.WithInternalApi(h.c.Server.InternalListenAddr), 937 ), 938 repoPath, 939 ) 940 941 w.WriteHeader(http.StatusNoContent) 942} 943 944func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 945 l := h.l.With("handler", "RemoveRepo") 946 947 data := struct { 948 Did string `json:"did"` 949 Name string `json:"name"` 950 }{} 951 952 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 953 writeError(w, "invalid request body", http.StatusBadRequest) 954 return 955 } 956 957 did := data.Did 958 name := data.Name 959 960 if did == "" || name == "" { 961 l.Error("invalid request body, empty did or name") 962 w.WriteHeader(http.StatusBadRequest) 963 return 964 } 965 966 relativeRepoPath := filepath.Join(did, name) 967 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 968 err := os.RemoveAll(repoPath) 969 if err != nil { 970 l.Error("removing repo", "error", err.Error()) 971 writeError(w, err.Error(), http.StatusInternalServerError) 972 return 973 } 974 975 w.WriteHeader(http.StatusNoContent) 976 977} 978func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) { 979 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 980 981 data := types.MergeRequest{} 982 983 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 984 writeError(w, err.Error(), http.StatusBadRequest) 985 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err) 986 return 987 } 988 989 mo := &git.MergeOptions{ 990 AuthorName: data.AuthorName, 991 AuthorEmail: data.AuthorEmail, 992 CommitBody: data.CommitBody, 993 CommitMessage: data.CommitMessage, 994 } 995 996 patch := data.Patch 997 branch := data.Branch 998 gr, err := git.Open(path, branch) 999 if err != nil { 1000 notFound(w) 1001 return 1002 } 1003 1004 mo.FormatPatch = patchutil.IsFormatPatch(patch) 1005 1006 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil { 1007 var mergeErr *git.ErrMerge 1008 if errors.As(err, &mergeErr) { 1009 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 1010 for i, conflict := range mergeErr.Conflicts { 1011 conflicts[i] = types.ConflictInfo{ 1012 Filename: conflict.Filename, 1013 Reason: conflict.Reason, 1014 } 1015 } 1016 response := types.MergeCheckResponse{ 1017 IsConflicted: true, 1018 Conflicts: conflicts, 1019 Message: mergeErr.Message, 1020 } 1021 writeConflict(w, response) 1022 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr) 1023 } else { 1024 writeError(w, err.Error(), http.StatusBadRequest) 1025 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error()) 1026 } 1027 return 1028 } 1029 1030 w.WriteHeader(http.StatusOK) 1031} 1032 1033func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) { 1034 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1035 1036 var data struct { 1037 Patch string `json:"patch"` 1038 Branch string `json:"branch"` 1039 } 1040 1041 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1042 writeError(w, err.Error(), http.StatusBadRequest) 1043 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err) 1044 return 1045 } 1046 1047 patch := data.Patch 1048 branch := data.Branch 1049 gr, err := git.Open(path, branch) 1050 if err != nil { 1051 notFound(w) 1052 return 1053 } 1054 1055 err = gr.MergeCheck([]byte(patch), branch) 1056 if err == nil { 1057 response := types.MergeCheckResponse{ 1058 IsConflicted: false, 1059 } 1060 writeJSON(w, response) 1061 return 1062 } 1063 1064 var mergeErr *git.ErrMerge 1065 if errors.As(err, &mergeErr) { 1066 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 1067 for i, conflict := range mergeErr.Conflicts { 1068 conflicts[i] = types.ConflictInfo{ 1069 Filename: conflict.Filename, 1070 Reason: conflict.Reason, 1071 } 1072 } 1073 response := types.MergeCheckResponse{ 1074 IsConflicted: true, 1075 Conflicts: conflicts, 1076 Message: mergeErr.Message, 1077 } 1078 writeConflict(w, response) 1079 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error()) 1080 return 1081 } 1082 writeError(w, err.Error(), http.StatusInternalServerError) 1083 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error()) 1084} 1085 1086func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) { 1087 rev1 := chi.URLParam(r, "rev1") 1088 rev1, _ = url.PathUnescape(rev1) 1089 1090 rev2 := chi.URLParam(r, "rev2") 1091 rev2, _ = url.PathUnescape(rev2) 1092 1093 l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2) 1094 1095 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1096 gr, err := git.PlainOpen(path) 1097 if err != nil { 1098 notFound(w) 1099 return 1100 } 1101 1102 commit1, err := gr.ResolveRevision(rev1) 1103 if err != nil { 1104 l.Error("error resolving revision 1", "msg", err.Error()) 1105 writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest) 1106 return 1107 } 1108 1109 commit2, err := gr.ResolveRevision(rev2) 1110 if err != nil { 1111 l.Error("error resolving revision 2", "msg", err.Error()) 1112 writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest) 1113 return 1114 } 1115 1116 rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2) 1117 if err != nil { 1118 l.Error("error comparing revisions", "msg", err.Error()) 1119 writeError(w, "error comparing revisions", http.StatusBadRequest) 1120 return 1121 } 1122 1123 writeJSON(w, types.RepoFormatPatchResponse{ 1124 Rev1: commit1.Hash.String(), 1125 Rev2: commit2.Hash.String(), 1126 FormatPatch: formatPatch, 1127 Patch: rawPatch, 1128 }) 1129 return 1130} 1131 1132func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) { 1133 l := h.l.With("handler", "NewHiddenRef") 1134 1135 forkRef := chi.URLParam(r, "forkRef") 1136 forkRef, _ = url.PathUnescape(forkRef) 1137 1138 remoteRef := chi.URLParam(r, "remoteRef") 1139 remoteRef, _ = url.PathUnescape(remoteRef) 1140 1141 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1142 gr, err := git.PlainOpen(path) 1143 if err != nil { 1144 notFound(w) 1145 return 1146 } 1147 1148 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef) 1149 if err != nil { 1150 l.Error("error tracking hidden remote ref", "msg", err.Error()) 1151 writeError(w, "error tracking hidden remote ref", http.StatusBadRequest) 1152 return 1153 } 1154 1155 w.WriteHeader(http.StatusNoContent) 1156 return 1157} 1158 1159func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { 1160 l := h.l.With("handler", "AddMember") 1161 1162 data := struct { 1163 Did string `json:"did"` 1164 }{} 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 did := data.Did 1172 1173 if err := h.db.AddDid(did); err != nil { 1174 l.Error("adding did", "error", err.Error()) 1175 writeError(w, err.Error(), http.StatusInternalServerError) 1176 return 1177 } 1178 h.jc.AddDid(did) 1179 1180 if err := h.e.AddMember(ThisServer, did); err != nil { 1181 l.Error("adding member", "error", err.Error()) 1182 writeError(w, err.Error(), http.StatusInternalServerError) 1183 return 1184 } 1185 1186 if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 1187 l.Error("fetching and adding keys", "error", err.Error()) 1188 writeError(w, err.Error(), http.StatusInternalServerError) 1189 return 1190 } 1191 1192 w.WriteHeader(http.StatusNoContent) 1193} 1194 1195func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 1196 l := h.l.With("handler", "AddRepoCollaborator") 1197 1198 data := struct { 1199 Did string `json:"did"` 1200 }{} 1201 1202 ownerDid := chi.URLParam(r, "did") 1203 repo := chi.URLParam(r, "name") 1204 1205 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1206 writeError(w, "invalid request body", http.StatusBadRequest) 1207 return 1208 } 1209 1210 if err := h.db.AddDid(data.Did); err != nil { 1211 l.Error("adding did", "error", err.Error()) 1212 writeError(w, err.Error(), http.StatusInternalServerError) 1213 return 1214 } 1215 h.jc.AddDid(data.Did) 1216 1217 repoName, _ := securejoin.SecureJoin(ownerDid, repo) 1218 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil { 1219 l.Error("adding repo collaborator", "error", err.Error()) 1220 writeError(w, err.Error(), http.StatusInternalServerError) 1221 return 1222 } 1223 1224 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1225 l.Error("fetching and adding keys", "error", err.Error()) 1226 writeError(w, err.Error(), http.StatusInternalServerError) 1227 return 1228 } 1229 1230 w.WriteHeader(http.StatusNoContent) 1231} 1232 1233func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) { 1234 l := h.l.With("handler", "DefaultBranch") 1235 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1236 1237 gr, err := git.Open(path, "") 1238 if err != nil { 1239 notFound(w) 1240 return 1241 } 1242 1243 branch, err := gr.FindMainBranch() 1244 if err != nil { 1245 writeError(w, err.Error(), http.StatusInternalServerError) 1246 l.Error("getting default branch", "error", err.Error()) 1247 return 1248 } 1249 1250 writeJSON(w, types.RepoDefaultBranchResponse{ 1251 Branch: branch, 1252 }) 1253} 1254 1255func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1256 l := h.l.With("handler", "SetDefaultBranch") 1257 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1258 1259 data := struct { 1260 Branch string `json:"branch"` 1261 }{} 1262 1263 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1264 writeError(w, err.Error(), http.StatusBadRequest) 1265 return 1266 } 1267 1268 gr, err := git.PlainOpen(path) 1269 if err != nil { 1270 notFound(w) 1271 return 1272 } 1273 1274 err = gr.SetDefaultBranch(data.Branch) 1275 if err != nil { 1276 writeError(w, err.Error(), http.StatusInternalServerError) 1277 l.Error("setting default branch", "error", err.Error()) 1278 return 1279 } 1280 1281 w.WriteHeader(http.StatusNoContent) 1282} 1283 1284func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { 1285 l := h.l.With("handler", "Init") 1286 1287 if h.knotInitialized { 1288 writeError(w, "knot already initialized", http.StatusConflict) 1289 return 1290 } 1291 1292 data := struct { 1293 Did string `json:"did"` 1294 }{} 1295 1296 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1297 l.Error("failed to decode request body", "error", err.Error()) 1298 writeError(w, "invalid request body", http.StatusBadRequest) 1299 return 1300 } 1301 1302 if data.Did == "" { 1303 l.Error("empty DID in request", "did", data.Did) 1304 writeError(w, "did is empty", http.StatusBadRequest) 1305 return 1306 } 1307 1308 if err := h.db.AddDid(data.Did); err != nil { 1309 l.Error("failed to add DID", "error", err.Error()) 1310 writeError(w, err.Error(), http.StatusInternalServerError) 1311 return 1312 } 1313 h.jc.AddDid(data.Did) 1314 1315 if err := h.e.AddOwner(ThisServer, data.Did); err != nil { 1316 l.Error("adding owner", "error", err.Error()) 1317 writeError(w, err.Error(), http.StatusInternalServerError) 1318 return 1319 } 1320 1321 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1322 l.Error("fetching and adding keys", "error", err.Error()) 1323 writeError(w, err.Error(), http.StatusInternalServerError) 1324 return 1325 } 1326 1327 close(h.init) 1328 1329 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret)) 1330 mac.Write([]byte("ok")) 1331 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil))) 1332 1333 w.WriteHeader(http.StatusNoContent) 1334} 1335 1336func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1337 w.Write([]byte("ok")) 1338} 1339 1340func validateRepoName(name string) error { 1341 // check for path traversal attempts 1342 if name == "." || name == ".." || 1343 strings.Contains(name, "/") || strings.Contains(name, "\\") { 1344 return fmt.Errorf("Repository name contains invalid path characters") 1345 } 1346 1347 // check for sequences that could be used for traversal when normalized 1348 if strings.Contains(name, "./") || strings.Contains(name, "../") || 1349 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 1350 return fmt.Errorf("Repository name contains invalid path sequence") 1351 } 1352 1353 // then continue with character validation 1354 for _, char := range name { 1355 if !((char >= 'a' && char <= 'z') || 1356 (char >= 'A' && char <= 'Z') || 1357 (char >= '0' && char <= '9') || 1358 char == '-' || char == '_' || char == '.') { 1359 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 1360 } 1361 } 1362 1363 // additional check to prevent multiple sequential dots 1364 if strings.Contains(name, "..") { 1365 return fmt.Errorf("Repository name cannot contain sequential dots") 1366 } 1367 1368 // if all checks pass 1369 return nil 1370}