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