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