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