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