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