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