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) RepoForkSyncable(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 isAncestor, err := forkCommit.IsAncestor(sourceCommit) 693 if err != nil { 694 log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err) 695 return 696 } 697 698 w.Header().Set("Content-Type", "application/json") 699 json.NewEncoder(w).Encode(types.AncestorCheckResponse{IsAncestor: isAncestor}) 700} 701 702func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) { 703 l := h.l.With("handler", "RepoForkSync") 704 705 data := struct { 706 Did string `json:"did"` 707 Source string `json:"source"` 708 Name string `json:"name,omitempty"` 709 }{} 710 711 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 712 writeError(w, "invalid request body", http.StatusBadRequest) 713 return 714 } 715 716 did := data.Did 717 source := data.Source 718 719 if did == "" || source == "" { 720 l.Error("invalid request body, empty did or name") 721 w.WriteHeader(http.StatusBadRequest) 722 return 723 } 724 725 var name string 726 if data.Name != "" { 727 name = data.Name 728 } else { 729 name = filepath.Base(source) 730 } 731 732 branch := chi.URLParam(r, "branch") 733 branch, _ = url.PathUnescape(branch) 734 735 relativeRepoPath := filepath.Join(did, name) 736 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 737 738 gr, err := git.PlainOpen(repoPath) 739 if err != nil { 740 log.Println(err) 741 notFound(w) 742 return 743 } 744 745 err = gr.Sync(branch) 746 if err != nil { 747 l.Error("error syncing repo fork", "error", err.Error()) 748 writeError(w, err.Error(), http.StatusInternalServerError) 749 return 750 } 751 752 w.WriteHeader(http.StatusNoContent) 753} 754 755func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) { 756 l := h.l.With("handler", "RepoFork") 757 758 data := struct { 759 Did string `json:"did"` 760 Source string `json:"source"` 761 Name string `json:"name,omitempty"` 762 }{} 763 764 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 765 writeError(w, "invalid request body", http.StatusBadRequest) 766 return 767 } 768 769 did := data.Did 770 source := data.Source 771 772 if did == "" || source == "" { 773 l.Error("invalid request body, empty did or name") 774 w.WriteHeader(http.StatusBadRequest) 775 return 776 } 777 778 var name string 779 if data.Name != "" { 780 name = data.Name 781 } else { 782 name = filepath.Base(source) 783 } 784 785 relativeRepoPath := filepath.Join(did, name) 786 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 787 788 err := git.Fork(repoPath, source) 789 if err != nil { 790 l.Error("forking repo", "error", err.Error()) 791 writeError(w, err.Error(), http.StatusInternalServerError) 792 return 793 } 794 795 // add perms for this user to access the repo 796 err = h.e.AddRepo(did, ThisServer, relativeRepoPath) 797 if err != nil { 798 l.Error("adding repo permissions", "error", err.Error()) 799 writeError(w, err.Error(), http.StatusInternalServerError) 800 return 801 } 802 803 w.WriteHeader(http.StatusNoContent) 804} 805 806func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 807 l := h.l.With("handler", "RemoveRepo") 808 809 data := struct { 810 Did string `json:"did"` 811 Name string `json:"name"` 812 }{} 813 814 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 815 writeError(w, "invalid request body", http.StatusBadRequest) 816 return 817 } 818 819 did := data.Did 820 name := data.Name 821 822 if did == "" || name == "" { 823 l.Error("invalid request body, empty did or name") 824 w.WriteHeader(http.StatusBadRequest) 825 return 826 } 827 828 relativeRepoPath := filepath.Join(did, name) 829 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 830 err := os.RemoveAll(repoPath) 831 if err != nil { 832 l.Error("removing repo", "error", err.Error()) 833 writeError(w, err.Error(), http.StatusInternalServerError) 834 return 835 } 836 837 w.WriteHeader(http.StatusNoContent) 838 839} 840func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) { 841 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 842 843 data := types.MergeRequest{} 844 845 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 846 writeError(w, err.Error(), http.StatusBadRequest) 847 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err) 848 return 849 } 850 851 mo := &git.MergeOptions{ 852 AuthorName: data.AuthorName, 853 AuthorEmail: data.AuthorEmail, 854 CommitBody: data.CommitBody, 855 CommitMessage: data.CommitMessage, 856 } 857 858 patch := data.Patch 859 branch := data.Branch 860 gr, err := git.Open(path, branch) 861 if err != nil { 862 notFound(w) 863 return 864 } 865 866 mo.FormatPatch = patchutil.IsFormatPatch(patch) 867 868 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil { 869 var mergeErr *git.ErrMerge 870 if errors.As(err, &mergeErr) { 871 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 872 for i, conflict := range mergeErr.Conflicts { 873 conflicts[i] = types.ConflictInfo{ 874 Filename: conflict.Filename, 875 Reason: conflict.Reason, 876 } 877 } 878 response := types.MergeCheckResponse{ 879 IsConflicted: true, 880 Conflicts: conflicts, 881 Message: mergeErr.Message, 882 } 883 writeConflict(w, response) 884 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr) 885 } else { 886 writeError(w, err.Error(), http.StatusBadRequest) 887 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error()) 888 } 889 return 890 } 891 892 w.WriteHeader(http.StatusOK) 893} 894 895func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) { 896 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 897 898 var data struct { 899 Patch string `json:"patch"` 900 Branch string `json:"branch"` 901 } 902 903 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 904 writeError(w, err.Error(), http.StatusBadRequest) 905 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err) 906 return 907 } 908 909 patch := data.Patch 910 branch := data.Branch 911 gr, err := git.Open(path, branch) 912 if err != nil { 913 notFound(w) 914 return 915 } 916 917 err = gr.MergeCheck([]byte(patch), branch) 918 if err == nil { 919 response := types.MergeCheckResponse{ 920 IsConflicted: false, 921 } 922 writeJSON(w, response) 923 return 924 } 925 926 var mergeErr *git.ErrMerge 927 if errors.As(err, &mergeErr) { 928 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 929 for i, conflict := range mergeErr.Conflicts { 930 conflicts[i] = types.ConflictInfo{ 931 Filename: conflict.Filename, 932 Reason: conflict.Reason, 933 } 934 } 935 response := types.MergeCheckResponse{ 936 IsConflicted: true, 937 Conflicts: conflicts, 938 Message: mergeErr.Message, 939 } 940 writeConflict(w, response) 941 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error()) 942 return 943 } 944 writeError(w, err.Error(), http.StatusInternalServerError) 945 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error()) 946} 947 948func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) { 949 rev1 := chi.URLParam(r, "rev1") 950 rev1, _ = url.PathUnescape(rev1) 951 952 rev2 := chi.URLParam(r, "rev2") 953 rev2, _ = url.PathUnescape(rev2) 954 955 l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2) 956 957 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 958 gr, err := git.PlainOpen(path) 959 if err != nil { 960 notFound(w) 961 return 962 } 963 964 commit1, err := gr.ResolveRevision(rev1) 965 if err != nil { 966 l.Error("error resolving revision 1", "msg", err.Error()) 967 writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest) 968 return 969 } 970 971 commit2, err := gr.ResolveRevision(rev2) 972 if err != nil { 973 l.Error("error resolving revision 2", "msg", err.Error()) 974 writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest) 975 return 976 } 977 978 mergeBase, err := gr.MergeBase(commit1, commit2) 979 if err != nil { 980 l.Error("failed to find merge-base", "msg", err.Error()) 981 writeError(w, "failed to calculate diff", http.StatusBadRequest) 982 return 983 } 984 985 rawPatch, formatPatch, err := gr.FormatPatch(mergeBase, commit2) 986 if err != nil { 987 l.Error("error comparing revisions", "msg", err.Error()) 988 writeError(w, "error comparing revisions", http.StatusBadRequest) 989 return 990 } 991 992 writeJSON(w, types.RepoFormatPatchResponse{ 993 Rev1: commit1.Hash.String(), 994 Rev2: commit2.Hash.String(), 995 FormatPatch: formatPatch, 996 Patch: rawPatch, 997 }) 998 return 999} 1000 1001func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) { 1002 l := h.l.With("handler", "NewHiddenRef") 1003 1004 forkRef := chi.URLParam(r, "forkRef") 1005 forkRef, _ = url.PathUnescape(forkRef) 1006 1007 remoteRef := chi.URLParam(r, "remoteRef") 1008 remoteRef, _ = url.PathUnescape(remoteRef) 1009 1010 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1011 gr, err := git.PlainOpen(path) 1012 if err != nil { 1013 notFound(w) 1014 return 1015 } 1016 1017 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef) 1018 if err != nil { 1019 l.Error("error tracking hidden remote ref", "msg", err.Error()) 1020 writeError(w, "error tracking hidden remote ref", http.StatusBadRequest) 1021 return 1022 } 1023 1024 w.WriteHeader(http.StatusNoContent) 1025 return 1026} 1027 1028func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { 1029 l := h.l.With("handler", "AddMember") 1030 1031 data := struct { 1032 Did string `json:"did"` 1033 }{} 1034 1035 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1036 writeError(w, "invalid request body", http.StatusBadRequest) 1037 return 1038 } 1039 1040 did := data.Did 1041 1042 if err := h.db.AddDid(did); err != nil { 1043 l.Error("adding did", "error", err.Error()) 1044 writeError(w, err.Error(), http.StatusInternalServerError) 1045 return 1046 } 1047 h.jc.AddDid(did) 1048 1049 if err := h.e.AddMember(ThisServer, did); err != nil { 1050 l.Error("adding member", "error", err.Error()) 1051 writeError(w, err.Error(), http.StatusInternalServerError) 1052 return 1053 } 1054 1055 if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 1056 l.Error("fetching and adding keys", "error", err.Error()) 1057 writeError(w, err.Error(), http.StatusInternalServerError) 1058 return 1059 } 1060 1061 w.WriteHeader(http.StatusNoContent) 1062} 1063 1064func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 1065 l := h.l.With("handler", "AddRepoCollaborator") 1066 1067 data := struct { 1068 Did string `json:"did"` 1069 }{} 1070 1071 ownerDid := chi.URLParam(r, "did") 1072 repo := chi.URLParam(r, "name") 1073 1074 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1075 writeError(w, "invalid request body", http.StatusBadRequest) 1076 return 1077 } 1078 1079 if err := h.db.AddDid(data.Did); err != nil { 1080 l.Error("adding did", "error", err.Error()) 1081 writeError(w, err.Error(), http.StatusInternalServerError) 1082 return 1083 } 1084 h.jc.AddDid(data.Did) 1085 1086 repoName, _ := securejoin.SecureJoin(ownerDid, repo) 1087 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil { 1088 l.Error("adding repo collaborator", "error", err.Error()) 1089 writeError(w, err.Error(), http.StatusInternalServerError) 1090 return 1091 } 1092 1093 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1094 l.Error("fetching and adding keys", "error", err.Error()) 1095 writeError(w, err.Error(), http.StatusInternalServerError) 1096 return 1097 } 1098 1099 w.WriteHeader(http.StatusNoContent) 1100} 1101 1102func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) { 1103 l := h.l.With("handler", "DefaultBranch") 1104 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1105 1106 gr, err := git.Open(path, "") 1107 if err != nil { 1108 notFound(w) 1109 return 1110 } 1111 1112 branch, err := gr.FindMainBranch() 1113 if err != nil { 1114 writeError(w, err.Error(), http.StatusInternalServerError) 1115 l.Error("getting default branch", "error", err.Error()) 1116 return 1117 } 1118 1119 writeJSON(w, types.RepoDefaultBranchResponse{ 1120 Branch: branch, 1121 }) 1122} 1123 1124func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1125 l := h.l.With("handler", "SetDefaultBranch") 1126 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1127 1128 data := struct { 1129 Branch string `json:"branch"` 1130 }{} 1131 1132 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1133 writeError(w, err.Error(), http.StatusBadRequest) 1134 return 1135 } 1136 1137 gr, err := git.Open(path, "") 1138 if err != nil { 1139 notFound(w) 1140 return 1141 } 1142 1143 err = gr.SetDefaultBranch(data.Branch) 1144 if err != nil { 1145 writeError(w, err.Error(), http.StatusInternalServerError) 1146 l.Error("setting default branch", "error", err.Error()) 1147 return 1148 } 1149 1150 w.WriteHeader(http.StatusNoContent) 1151} 1152 1153func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { 1154 l := h.l.With("handler", "Init") 1155 1156 if h.knotInitialized { 1157 writeError(w, "knot already initialized", http.StatusConflict) 1158 return 1159 } 1160 1161 data := struct { 1162 Did string `json:"did"` 1163 }{} 1164 1165 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1166 l.Error("failed to decode request body", "error", err.Error()) 1167 writeError(w, "invalid request body", http.StatusBadRequest) 1168 return 1169 } 1170 1171 if data.Did == "" { 1172 l.Error("empty DID in request", "did", data.Did) 1173 writeError(w, "did is empty", http.StatusBadRequest) 1174 return 1175 } 1176 1177 if err := h.db.AddDid(data.Did); err != nil { 1178 l.Error("failed to add DID", "error", err.Error()) 1179 writeError(w, err.Error(), http.StatusInternalServerError) 1180 return 1181 } 1182 h.jc.AddDid(data.Did) 1183 1184 if err := h.e.AddOwner(ThisServer, data.Did); err != nil { 1185 l.Error("adding owner", "error", err.Error()) 1186 writeError(w, err.Error(), http.StatusInternalServerError) 1187 return 1188 } 1189 1190 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1191 l.Error("fetching and adding keys", "error", err.Error()) 1192 writeError(w, err.Error(), http.StatusInternalServerError) 1193 return 1194 } 1195 1196 close(h.init) 1197 1198 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret)) 1199 mac.Write([]byte("ok")) 1200 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil))) 1201 1202 w.WriteHeader(http.StatusNoContent) 1203} 1204 1205func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1206 w.Write([]byte("ok")) 1207} 1208 1209func validateRepoName(name string) error { 1210 // check for path traversal attempts 1211 if name == "." || name == ".." || 1212 strings.Contains(name, "/") || strings.Contains(name, "\\") { 1213 return fmt.Errorf("Repository name contains invalid path characters") 1214 } 1215 1216 // check for sequences that could be used for traversal when normalized 1217 if strings.Contains(name, "./") || strings.Contains(name, "../") || 1218 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 1219 return fmt.Errorf("Repository name contains invalid path sequence") 1220 } 1221 1222 // then continue with character validation 1223 for _, char := range name { 1224 if !((char >= 'a' && char <= 'z') || 1225 (char >= 'A' && char <= 'Z') || 1226 (char >= '0' && char <= '9') || 1227 char == '-' || char == '_' || char == '.') { 1228 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 1229 } 1230 } 1231 1232 // additional check to prevent multiple sequential dots 1233 if strings.Contains(name, "..") { 1234 return fmt.Errorf("Repository name cannot contain sequential dots") 1235 } 1236 1237 // if all checks pass 1238 return nil 1239}