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