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