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