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 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 719 ref := chi.URLParam(r, "ref") 720 ref, _ = url.PathUnescape(ref) 721 722 l := h.l.With("handler", "RepoLanguages") 723 724 gr, err := git.Open(path, ref) 725 if err != nil { 726 l.Error("opening repo", "error", err.Error()) 727 notFound(w) 728 return 729 } 730 731 languageFileCount := make(map[string]int) 732 733 err = recurseEntireTree(gr, func(absPath string) { 734 lang, safe := enry.GetLanguageByExtension(absPath) 735 if len(lang) == 0 || !safe { 736 content, _ := gr.FileContentN(absPath, 1024) 737 if !safe { 738 lang = enry.GetLanguage(absPath, content) 739 } else { 740 lang, _ = enry.GetLanguageByContent(absPath, content) 741 if len(lang) == 0 { 742 return 743 } 744 } 745 } 746 747 v, ok := languageFileCount[lang] 748 if ok { 749 languageFileCount[lang] = v + 1 750 } else { 751 languageFileCount[lang] = 1 752 } 753 }, "") 754 if err != nil { 755 l.Error("failed to recurse file tree", "error", err.Error()) 756 writeError(w, err.Error(), http.StatusNoContent) 757 return 758 } 759 760 resp := types.RepoLanguageResponse{Languages: languageFileCount} 761 762 writeJSON(w, resp) 763 return 764} 765 766func recurseEntireTree(git *git.GitRepo, callback func(absPath string), filePath string) error { 767 files, err := git.FileTree(filePath) 768 if err != nil { 769 log.Println(err) 770 return err 771 } 772 773 for _, file := range files { 774 absPath := path.Join(filePath, file.Name) 775 if !file.IsFile { 776 return recurseEntireTree(git, callback, absPath) 777 } 778 callback(absPath) 779 } 780 781 return nil 782} 783 784func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) { 785 l := h.l.With("handler", "RepoForkSync") 786 787 data := struct { 788 Did string `json:"did"` 789 Source string `json:"source"` 790 Name string `json:"name,omitempty"` 791 }{} 792 793 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 794 writeError(w, "invalid request body", http.StatusBadRequest) 795 return 796 } 797 798 did := data.Did 799 source := data.Source 800 801 if did == "" || source == "" { 802 l.Error("invalid request body, empty did or name") 803 w.WriteHeader(http.StatusBadRequest) 804 return 805 } 806 807 var name string 808 if data.Name != "" { 809 name = data.Name 810 } else { 811 name = filepath.Base(source) 812 } 813 814 branch := chi.URLParam(r, "branch") 815 branch, _ = url.PathUnescape(branch) 816 817 relativeRepoPath := filepath.Join(did, name) 818 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 819 820 gr, err := git.PlainOpen(repoPath) 821 if err != nil { 822 log.Println(err) 823 notFound(w) 824 return 825 } 826 827 err = gr.Sync(branch) 828 if err != nil { 829 l.Error("error syncing repo fork", "error", err.Error()) 830 writeError(w, err.Error(), http.StatusInternalServerError) 831 return 832 } 833 834 w.WriteHeader(http.StatusNoContent) 835} 836 837func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) { 838 l := h.l.With("handler", "RepoFork") 839 840 data := struct { 841 Did string `json:"did"` 842 Source string `json:"source"` 843 Name string `json:"name,omitempty"` 844 }{} 845 846 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 847 writeError(w, "invalid request body", http.StatusBadRequest) 848 return 849 } 850 851 did := data.Did 852 source := data.Source 853 854 if did == "" || source == "" { 855 l.Error("invalid request body, empty did or name") 856 w.WriteHeader(http.StatusBadRequest) 857 return 858 } 859 860 var name string 861 if data.Name != "" { 862 name = data.Name 863 } else { 864 name = filepath.Base(source) 865 } 866 867 relativeRepoPath := filepath.Join(did, name) 868 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 869 870 err := git.Fork(repoPath, source) 871 if err != nil { 872 l.Error("forking repo", "error", err.Error()) 873 writeError(w, err.Error(), http.StatusInternalServerError) 874 return 875 } 876 877 // add perms for this user to access the repo 878 err = h.e.AddRepo(did, ThisServer, relativeRepoPath) 879 if err != nil { 880 l.Error("adding repo permissions", "error", err.Error()) 881 writeError(w, err.Error(), http.StatusInternalServerError) 882 return 883 } 884 885 w.WriteHeader(http.StatusNoContent) 886} 887 888func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 889 l := h.l.With("handler", "RemoveRepo") 890 891 data := struct { 892 Did string `json:"did"` 893 Name string `json:"name"` 894 }{} 895 896 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 897 writeError(w, "invalid request body", http.StatusBadRequest) 898 return 899 } 900 901 did := data.Did 902 name := data.Name 903 904 if did == "" || name == "" { 905 l.Error("invalid request body, empty did or name") 906 w.WriteHeader(http.StatusBadRequest) 907 return 908 } 909 910 relativeRepoPath := filepath.Join(did, name) 911 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 912 err := os.RemoveAll(repoPath) 913 if err != nil { 914 l.Error("removing repo", "error", err.Error()) 915 writeError(w, err.Error(), http.StatusInternalServerError) 916 return 917 } 918 919 w.WriteHeader(http.StatusNoContent) 920 921} 922func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) { 923 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 924 925 data := types.MergeRequest{} 926 927 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 928 writeError(w, err.Error(), http.StatusBadRequest) 929 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err) 930 return 931 } 932 933 mo := &git.MergeOptions{ 934 AuthorName: data.AuthorName, 935 AuthorEmail: data.AuthorEmail, 936 CommitBody: data.CommitBody, 937 CommitMessage: data.CommitMessage, 938 } 939 940 patch := data.Patch 941 branch := data.Branch 942 gr, err := git.Open(path, branch) 943 if err != nil { 944 notFound(w) 945 return 946 } 947 948 mo.FormatPatch = patchutil.IsFormatPatch(patch) 949 950 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil { 951 var mergeErr *git.ErrMerge 952 if errors.As(err, &mergeErr) { 953 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 954 for i, conflict := range mergeErr.Conflicts { 955 conflicts[i] = types.ConflictInfo{ 956 Filename: conflict.Filename, 957 Reason: conflict.Reason, 958 } 959 } 960 response := types.MergeCheckResponse{ 961 IsConflicted: true, 962 Conflicts: conflicts, 963 Message: mergeErr.Message, 964 } 965 writeConflict(w, response) 966 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr) 967 } else { 968 writeError(w, err.Error(), http.StatusBadRequest) 969 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error()) 970 } 971 return 972 } 973 974 w.WriteHeader(http.StatusOK) 975} 976 977func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) { 978 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 979 980 var data struct { 981 Patch string `json:"patch"` 982 Branch string `json:"branch"` 983 } 984 985 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 986 writeError(w, err.Error(), http.StatusBadRequest) 987 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err) 988 return 989 } 990 991 patch := data.Patch 992 branch := data.Branch 993 gr, err := git.Open(path, branch) 994 if err != nil { 995 notFound(w) 996 return 997 } 998 999 err = gr.MergeCheck([]byte(patch), branch) 1000 if err == nil { 1001 response := types.MergeCheckResponse{ 1002 IsConflicted: false, 1003 } 1004 writeJSON(w, response) 1005 return 1006 } 1007 1008 var mergeErr *git.ErrMerge 1009 if errors.As(err, &mergeErr) { 1010 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 1011 for i, conflict := range mergeErr.Conflicts { 1012 conflicts[i] = types.ConflictInfo{ 1013 Filename: conflict.Filename, 1014 Reason: conflict.Reason, 1015 } 1016 } 1017 response := types.MergeCheckResponse{ 1018 IsConflicted: true, 1019 Conflicts: conflicts, 1020 Message: mergeErr.Message, 1021 } 1022 writeConflict(w, response) 1023 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error()) 1024 return 1025 } 1026 writeError(w, err.Error(), http.StatusInternalServerError) 1027 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error()) 1028} 1029 1030func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) { 1031 rev1 := chi.URLParam(r, "rev1") 1032 rev1, _ = url.PathUnescape(rev1) 1033 1034 rev2 := chi.URLParam(r, "rev2") 1035 rev2, _ = url.PathUnescape(rev2) 1036 1037 l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2) 1038 1039 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1040 gr, err := git.PlainOpen(path) 1041 if err != nil { 1042 notFound(w) 1043 return 1044 } 1045 1046 commit1, err := gr.ResolveRevision(rev1) 1047 if err != nil { 1048 l.Error("error resolving revision 1", "msg", err.Error()) 1049 writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest) 1050 return 1051 } 1052 1053 commit2, err := gr.ResolveRevision(rev2) 1054 if err != nil { 1055 l.Error("error resolving revision 2", "msg", err.Error()) 1056 writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest) 1057 return 1058 } 1059 1060 mergeBase, err := gr.MergeBase(commit1, commit2) 1061 if err != nil { 1062 l.Error("failed to find merge-base", "msg", err.Error()) 1063 writeError(w, "failed to calculate diff", http.StatusBadRequest) 1064 return 1065 } 1066 1067 rawPatch, formatPatch, err := gr.FormatPatch(mergeBase, commit2) 1068 if err != nil { 1069 l.Error("error comparing revisions", "msg", err.Error()) 1070 writeError(w, "error comparing revisions", http.StatusBadRequest) 1071 return 1072 } 1073 1074 writeJSON(w, types.RepoFormatPatchResponse{ 1075 Rev1: commit1.Hash.String(), 1076 Rev2: commit2.Hash.String(), 1077 FormatPatch: formatPatch, 1078 MergeBase: mergeBase.Hash.String(), 1079 Patch: rawPatch, 1080 }) 1081 return 1082} 1083 1084func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) { 1085 l := h.l.With("handler", "NewHiddenRef") 1086 1087 forkRef := chi.URLParam(r, "forkRef") 1088 forkRef, _ = url.PathUnescape(forkRef) 1089 1090 remoteRef := chi.URLParam(r, "remoteRef") 1091 remoteRef, _ = url.PathUnescape(remoteRef) 1092 1093 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1094 gr, err := git.PlainOpen(path) 1095 if err != nil { 1096 notFound(w) 1097 return 1098 } 1099 1100 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef) 1101 if err != nil { 1102 l.Error("error tracking hidden remote ref", "msg", err.Error()) 1103 writeError(w, "error tracking hidden remote ref", http.StatusBadRequest) 1104 return 1105 } 1106 1107 w.WriteHeader(http.StatusNoContent) 1108 return 1109} 1110 1111func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { 1112 l := h.l.With("handler", "AddMember") 1113 1114 data := struct { 1115 Did string `json:"did"` 1116 }{} 1117 1118 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1119 writeError(w, "invalid request body", http.StatusBadRequest) 1120 return 1121 } 1122 1123 did := data.Did 1124 1125 if err := h.db.AddDid(did); err != nil { 1126 l.Error("adding did", "error", err.Error()) 1127 writeError(w, err.Error(), http.StatusInternalServerError) 1128 return 1129 } 1130 h.jc.AddDid(did) 1131 1132 if err := h.e.AddMember(ThisServer, did); err != nil { 1133 l.Error("adding member", "error", err.Error()) 1134 writeError(w, err.Error(), http.StatusInternalServerError) 1135 return 1136 } 1137 1138 if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 1139 l.Error("fetching and adding keys", "error", err.Error()) 1140 writeError(w, err.Error(), http.StatusInternalServerError) 1141 return 1142 } 1143 1144 w.WriteHeader(http.StatusNoContent) 1145} 1146 1147func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 1148 l := h.l.With("handler", "AddRepoCollaborator") 1149 1150 data := struct { 1151 Did string `json:"did"` 1152 }{} 1153 1154 ownerDid := chi.URLParam(r, "did") 1155 repo := chi.URLParam(r, "name") 1156 1157 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1158 writeError(w, "invalid request body", http.StatusBadRequest) 1159 return 1160 } 1161 1162 if err := h.db.AddDid(data.Did); err != nil { 1163 l.Error("adding did", "error", err.Error()) 1164 writeError(w, err.Error(), http.StatusInternalServerError) 1165 return 1166 } 1167 h.jc.AddDid(data.Did) 1168 1169 repoName, _ := securejoin.SecureJoin(ownerDid, repo) 1170 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil { 1171 l.Error("adding repo collaborator", "error", err.Error()) 1172 writeError(w, err.Error(), http.StatusInternalServerError) 1173 return 1174 } 1175 1176 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1177 l.Error("fetching and adding keys", "error", err.Error()) 1178 writeError(w, err.Error(), http.StatusInternalServerError) 1179 return 1180 } 1181 1182 w.WriteHeader(http.StatusNoContent) 1183} 1184 1185func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) { 1186 l := h.l.With("handler", "DefaultBranch") 1187 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1188 1189 gr, err := git.Open(path, "") 1190 if err != nil { 1191 notFound(w) 1192 return 1193 } 1194 1195 branch, err := gr.FindMainBranch() 1196 if err != nil { 1197 writeError(w, err.Error(), http.StatusInternalServerError) 1198 l.Error("getting default branch", "error", err.Error()) 1199 return 1200 } 1201 1202 writeJSON(w, types.RepoDefaultBranchResponse{ 1203 Branch: branch, 1204 }) 1205} 1206 1207func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1208 l := h.l.With("handler", "SetDefaultBranch") 1209 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1210 1211 data := struct { 1212 Branch string `json:"branch"` 1213 }{} 1214 1215 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1216 writeError(w, err.Error(), http.StatusBadRequest) 1217 return 1218 } 1219 1220 gr, err := git.PlainOpen(path) 1221 if err != nil { 1222 notFound(w) 1223 return 1224 } 1225 1226 err = gr.SetDefaultBranch(data.Branch) 1227 if err != nil { 1228 writeError(w, err.Error(), http.StatusInternalServerError) 1229 l.Error("setting default branch", "error", err.Error()) 1230 return 1231 } 1232 1233 w.WriteHeader(http.StatusNoContent) 1234} 1235 1236func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { 1237 l := h.l.With("handler", "Init") 1238 1239 if h.knotInitialized { 1240 writeError(w, "knot already initialized", http.StatusConflict) 1241 return 1242 } 1243 1244 data := struct { 1245 Did string `json:"did"` 1246 }{} 1247 1248 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1249 l.Error("failed to decode request body", "error", err.Error()) 1250 writeError(w, "invalid request body", http.StatusBadRequest) 1251 return 1252 } 1253 1254 if data.Did == "" { 1255 l.Error("empty DID in request", "did", data.Did) 1256 writeError(w, "did is empty", http.StatusBadRequest) 1257 return 1258 } 1259 1260 if err := h.db.AddDid(data.Did); err != nil { 1261 l.Error("failed to add DID", "error", err.Error()) 1262 writeError(w, err.Error(), http.StatusInternalServerError) 1263 return 1264 } 1265 h.jc.AddDid(data.Did) 1266 1267 if err := h.e.AddOwner(ThisServer, data.Did); err != nil { 1268 l.Error("adding owner", "error", err.Error()) 1269 writeError(w, err.Error(), http.StatusInternalServerError) 1270 return 1271 } 1272 1273 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1274 l.Error("fetching and adding keys", "error", err.Error()) 1275 writeError(w, err.Error(), http.StatusInternalServerError) 1276 return 1277 } 1278 1279 close(h.init) 1280 1281 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret)) 1282 mac.Write([]byte("ok")) 1283 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil))) 1284 1285 w.WriteHeader(http.StatusNoContent) 1286} 1287 1288func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1289 w.Write([]byte("ok")) 1290} 1291 1292func validateRepoName(name string) error { 1293 // check for path traversal attempts 1294 if name == "." || name == ".." || 1295 strings.Contains(name, "/") || strings.Contains(name, "\\") { 1296 return fmt.Errorf("Repository name contains invalid path characters") 1297 } 1298 1299 // check for sequences that could be used for traversal when normalized 1300 if strings.Contains(name, "./") || strings.Contains(name, "../") || 1301 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 1302 return fmt.Errorf("Repository name contains invalid path sequence") 1303 } 1304 1305 // then continue with character validation 1306 for _, char := range name { 1307 if !((char >= 'a' && char <= 'z') || 1308 (char >= 'A' && char <= 'Z') || 1309 (char >= '0' && char <= '9') || 1310 char == '-' || char == '_' || char == '.') { 1311 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 1312 } 1313 } 1314 1315 // additional check to prevent multiple sequential dots 1316 if strings.Contains(name, "..") { 1317 return fmt.Errorf("Repository name cannot contain sequential dots") 1318 } 1319 1320 // if all checks pass 1321 return nil 1322}