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