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