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