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/filepath" 16 "strconv" 17 "strings" 18 19 securejoin "github.com/cyphar/filepath-securejoin" 20 "github.com/gliderlabs/ssh" 21 "github.com/go-chi/chi/v5" 22 gogit "github.com/go-git/go-git/v5" 23 "github.com/go-git/go-git/v5/plumbing" 24 "github.com/go-git/go-git/v5/plumbing/object" 25 "tangled.sh/tangled.sh/core/knotserver/db" 26 "tangled.sh/tangled.sh/core/knotserver/git" 27 "tangled.sh/tangled.sh/core/types" 28) 29 30func (h *Handle) Index(w http.ResponseWriter, r *http.Request) { 31 w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 32} 33 34func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) { 35 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 36 l := h.l.With("path", path, "handler", "RepoIndex") 37 ref := chi.URLParam(r, "ref") 38 ref, _ = url.PathUnescape(ref) 39 40 gr, err := git.Open(path, ref) 41 if err != nil { 42 log.Println(err) 43 if errors.Is(err, plumbing.ErrReferenceNotFound) { 44 resp := types.RepoIndexResponse{ 45 IsEmpty: true, 46 } 47 writeJSON(w, resp) 48 return 49 } else { 50 l.Error("opening repo", "error", err.Error()) 51 notFound(w) 52 return 53 } 54 } 55 56 commits, err := gr.Commits() 57 total := len(commits) 58 if err != nil { 59 writeError(w, err.Error(), http.StatusInternalServerError) 60 l.Error("fetching commits", "error", err.Error()) 61 return 62 } 63 if len(commits) > 10 { 64 commits = commits[:10] 65 } 66 67 branches, err := gr.Branches() 68 if err != nil { 69 l.Error("getting branches", "error", err.Error()) 70 writeError(w, err.Error(), http.StatusInternalServerError) 71 return 72 } 73 74 bs := []types.Branch{} 75 for _, branch := range branches { 76 b := types.Branch{} 77 b.Hash = branch.Hash().String() 78 b.Name = branch.Name().Short() 79 bs = append(bs, b) 80 } 81 82 tags, err := gr.Tags() 83 if err != nil { 84 // Non-fatal, we *should* have at least one branch to show. 85 l.Warn("getting tags", "error", err.Error()) 86 } 87 88 rtags := []*types.TagReference{} 89 for _, tag := range tags { 90 tr := types.TagReference{ 91 Tag: tag.TagObject(), 92 } 93 94 tr.Reference = types.Reference{ 95 Name: tag.Name(), 96 Hash: tag.Hash().String(), 97 } 98 99 if tag.Message() != "" { 100 tr.Message = tag.Message() 101 } 102 103 rtags = append(rtags, &tr) 104 } 105 106 var readmeContent string 107 var readmeFile string 108 for _, readme := range h.c.Repo.Readme { 109 content, _ := gr.FileContent(readme) 110 if len(content) > 0 { 111 readmeContent = string(content) 112 readmeFile = readme 113 } 114 } 115 116 files, err := gr.FileTree("") 117 if err != nil { 118 writeError(w, err.Error(), http.StatusInternalServerError) 119 l.Error("file tree", "error", err.Error()) 120 return 121 } 122 123 if ref == "" { 124 mainBranch, err := gr.FindMainBranch() 125 if err != nil { 126 writeError(w, err.Error(), http.StatusInternalServerError) 127 l.Error("finding main branch", "error", err.Error()) 128 return 129 } 130 ref = mainBranch 131 } 132 133 resp := types.RepoIndexResponse{ 134 IsEmpty: false, 135 Ref: ref, 136 Commits: commits, 137 Description: getDescription(path), 138 Readme: readmeContent, 139 ReadmeFileName: readmeFile, 140 Files: files, 141 Branches: bs, 142 Tags: rtags, 143 TotalCommits: total, 144 } 145 146 writeJSON(w, resp) 147 return 148} 149 150func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) { 151 treePath := chi.URLParam(r, "*") 152 ref := chi.URLParam(r, "ref") 153 ref, _ = url.PathUnescape(ref) 154 155 l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath) 156 157 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 158 gr, err := git.Open(path, ref) 159 if err != nil { 160 notFound(w) 161 return 162 } 163 164 files, err := gr.FileTree(treePath) 165 if err != nil { 166 writeError(w, err.Error(), http.StatusInternalServerError) 167 l.Error("file tree", "error", err.Error()) 168 return 169 } 170 171 resp := types.RepoTreeResponse{ 172 Ref: ref, 173 Parent: treePath, 174 Description: getDescription(path), 175 DotDot: filepath.Dir(treePath), 176 Files: files, 177 } 178 179 writeJSON(w, resp) 180 return 181} 182 183func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { 184 treePath := chi.URLParam(r, "*") 185 ref := chi.URLParam(r, "ref") 186 ref, _ = url.PathUnescape(ref) 187 188 l := h.l.With("handler", "FileContent", "ref", ref, "treePath", treePath) 189 190 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 191 gr, err := git.Open(path, ref) 192 if err != nil { 193 notFound(w) 194 return 195 } 196 197 var isBinaryFile bool = false 198 contents, err := gr.FileContent(treePath) 199 if errors.Is(err, git.ErrBinaryFile) { 200 isBinaryFile = true 201 } else if errors.Is(err, object.ErrFileNotFound) { 202 notFound(w) 203 return 204 } else if err != nil { 205 writeError(w, err.Error(), http.StatusInternalServerError) 206 return 207 } 208 209 bytes := []byte(contents) 210 // safe := string(sanitize(bytes)) 211 sizeHint := len(bytes) 212 213 resp := types.RepoBlobResponse{ 214 Ref: ref, 215 Contents: string(bytes), 216 Path: treePath, 217 IsBinary: isBinaryFile, 218 SizeHint: uint64(sizeHint), 219 } 220 221 h.showFile(resp, w, l) 222} 223 224func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) { 225 name := chi.URLParam(r, "name") 226 file := chi.URLParam(r, "file") 227 228 l := h.l.With("handler", "Archive", "name", name, "file", file) 229 230 // TODO: extend this to add more files compression (e.g.: xz) 231 if !strings.HasSuffix(file, ".tar.gz") { 232 notFound(w) 233 return 234 } 235 236 ref := strings.TrimSuffix(file, ".tar.gz") 237 238 // This allows the browser to use a proper name for the file when 239 // downloading 240 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref) 241 setContentDisposition(w, filename) 242 setGZipMIME(w) 243 244 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 245 gr, err := git.Open(path, ref) 246 if err != nil { 247 notFound(w) 248 return 249 } 250 251 gw := gzip.NewWriter(w) 252 defer gw.Close() 253 254 prefix := fmt.Sprintf("%s-%s", name, ref) 255 err = gr.WriteTar(gw, prefix) 256 if err != nil { 257 // once we start writing to the body we can't report error anymore 258 // so we are only left with printing the error. 259 l.Error("writing tar file", "error", err.Error()) 260 return 261 } 262 263 err = gw.Flush() 264 if err != nil { 265 // once we start writing to the body we can't report error anymore 266 // so we are only left with printing the error. 267 l.Error("flushing?", "error", err.Error()) 268 return 269 } 270} 271 272func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 273 ref := chi.URLParam(r, "ref") 274 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 275 276 l := h.l.With("handler", "Log", "ref", ref, "path", path) 277 278 gr, err := git.Open(path, ref) 279 if err != nil { 280 notFound(w) 281 return 282 } 283 284 commits, err := gr.Commits() 285 if err != nil { 286 writeError(w, err.Error(), http.StatusInternalServerError) 287 l.Error("fetching commits", "error", err.Error()) 288 return 289 } 290 291 // Get page parameters 292 page := 1 293 pageSize := 30 294 295 if pageParam := r.URL.Query().Get("page"); pageParam != "" { 296 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 { 297 page = p 298 } 299 } 300 301 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" { 302 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 { 303 pageSize = ps 304 } 305 } 306 307 // Calculate pagination 308 start := (page - 1) * pageSize 309 end := start + pageSize 310 total := len(commits) 311 312 if start >= total { 313 commits = []*object.Commit{} 314 } else { 315 if end > total { 316 end = total 317 } 318 commits = commits[start:end] 319 } 320 321 resp := types.RepoLogResponse{ 322 Commits: commits, 323 Ref: ref, 324 Description: getDescription(path), 325 Log: true, 326 Total: total, 327 Page: page, 328 PerPage: pageSize, 329 } 330 331 writeJSON(w, resp) 332 return 333} 334 335func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) { 336 ref := chi.URLParam(r, "ref") 337 ref, _ = url.PathUnescape(ref) 338 339 l := h.l.With("handler", "Diff", "ref", ref) 340 341 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 342 gr, err := git.Open(path, ref) 343 if err != nil { 344 notFound(w) 345 return 346 } 347 348 diff, err := gr.Diff() 349 if err != nil { 350 writeError(w, err.Error(), http.StatusInternalServerError) 351 l.Error("getting diff", "error", err.Error()) 352 return 353 } 354 355 resp := types.RepoCommitResponse{ 356 Ref: ref, 357 Diff: diff, 358 } 359 360 writeJSON(w, resp) 361 return 362} 363 364func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) { 365 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 366 l := h.l.With("handler", "Refs") 367 368 gr, err := git.Open(path, "") 369 if err != nil { 370 notFound(w) 371 return 372 } 373 374 tags, err := gr.Tags() 375 if err != nil { 376 // Non-fatal, we *should* have at least one branch to show. 377 l.Warn("getting tags", "error", err.Error()) 378 } 379 380 rtags := []*types.TagReference{} 381 for _, tag := range tags { 382 tr := types.TagReference{ 383 Tag: tag.TagObject(), 384 } 385 386 tr.Reference = types.Reference{ 387 Name: tag.Name(), 388 Hash: tag.Hash().String(), 389 } 390 391 if tag.Message() != "" { 392 tr.Message = tag.Message() 393 } 394 395 rtags = append(rtags, &tr) 396 } 397 398 resp := types.RepoTagsResponse{ 399 Tags: rtags, 400 } 401 402 writeJSON(w, resp) 403 return 404} 405 406func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) { 407 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 408 l := h.l.With("handler", "Branches") 409 410 gr, err := git.Open(path, "") 411 if err != nil { 412 notFound(w) 413 return 414 } 415 416 branches, err := gr.Branches() 417 if err != nil { 418 l.Error("getting branches", "error", err.Error()) 419 writeError(w, err.Error(), http.StatusInternalServerError) 420 return 421 } 422 423 bs := []types.Branch{} 424 for _, branch := range branches { 425 b := types.Branch{} 426 b.Hash = branch.Hash().String() 427 b.Name = branch.Name().Short() 428 bs = append(bs, b) 429 } 430 431 resp := types.RepoBranchesResponse{ 432 Branches: bs, 433 } 434 435 writeJSON(w, resp) 436 return 437} 438 439func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 440 l := h.l.With("handler", "Keys") 441 442 switch r.Method { 443 case http.MethodGet: 444 keys, err := h.db.GetAllPublicKeys() 445 if err != nil { 446 writeError(w, err.Error(), http.StatusInternalServerError) 447 l.Error("getting public keys", "error", err.Error()) 448 return 449 } 450 451 data := make([]map[string]any, 0) 452 for _, key := range keys { 453 j := key.JSON() 454 data = append(data, j) 455 } 456 writeJSON(w, data) 457 return 458 459 case http.MethodPut: 460 pk := db.PublicKey{} 461 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil { 462 writeError(w, "invalid request body", http.StatusBadRequest) 463 return 464 } 465 466 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key)) 467 if err != nil { 468 writeError(w, "invalid pubkey", http.StatusBadRequest) 469 } 470 471 if err := h.db.AddPublicKey(pk); err != nil { 472 writeError(w, err.Error(), http.StatusInternalServerError) 473 l.Error("adding public key", "error", err.Error()) 474 return 475 } 476 477 w.WriteHeader(http.StatusNoContent) 478 return 479 } 480} 481 482func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) { 483 l := h.l.With("handler", "NewRepo") 484 485 data := struct { 486 Did string `json:"did"` 487 Name string `json:"name"` 488 DefaultBranch string `json:"default_branch,omitempty"` 489 }{} 490 491 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 492 writeError(w, "invalid request body", http.StatusBadRequest) 493 return 494 } 495 496 if data.DefaultBranch == "" { 497 data.DefaultBranch = h.c.Repo.MainBranch 498 } 499 500 did := data.Did 501 name := data.Name 502 defaultBranch := data.DefaultBranch 503 504 relativeRepoPath := filepath.Join(did, name) 505 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 506 err := git.InitBare(repoPath, defaultBranch) 507 if err != nil { 508 l.Error("initializing bare repo", "error", err.Error()) 509 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 510 writeError(w, "That repo already exists!", http.StatusConflict) 511 return 512 } else { 513 writeError(w, err.Error(), http.StatusInternalServerError) 514 return 515 } 516 } 517 518 // add perms for this user to access the repo 519 err = h.e.AddRepo(did, ThisServer, relativeRepoPath) 520 if err != nil { 521 l.Error("adding repo permissions", "error", err.Error()) 522 writeError(w, err.Error(), http.StatusInternalServerError) 523 return 524 } 525 526 w.WriteHeader(http.StatusNoContent) 527} 528 529func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 530 l := h.l.With("handler", "RemoveRepo") 531 532 data := struct { 533 Did string `json:"did"` 534 Name string `json:"name"` 535 }{} 536 537 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 538 writeError(w, "invalid request body", http.StatusBadRequest) 539 return 540 } 541 542 did := data.Did 543 name := data.Name 544 545 if did == "" || name == "" { 546 l.Error("invalid request body, empty did or name") 547 w.WriteHeader(http.StatusBadRequest) 548 return 549 } 550 551 relativeRepoPath := filepath.Join(did, name) 552 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 553 err := os.RemoveAll(repoPath) 554 if err != nil { 555 l.Error("removing repo", "error", err.Error()) 556 writeError(w, err.Error(), http.StatusInternalServerError) 557 return 558 } 559 560 w.WriteHeader(http.StatusNoContent) 561 562} 563func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) { 564 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 565 566 data := types.MergeRequest{} 567 568 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 569 writeError(w, err.Error(), http.StatusBadRequest) 570 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err) 571 return 572 } 573 574 mo := &git.MergeOptions{ 575 AuthorName: data.AuthorName, 576 AuthorEmail: data.AuthorEmail, 577 CommitBody: data.CommitBody, 578 CommitMessage: data.CommitMessage, 579 } 580 581 patch := data.Patch 582 branch := data.Branch 583 gr, err := git.Open(path, branch) 584 if err != nil { 585 notFound(w) 586 return 587 } 588 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil { 589 var mergeErr *git.ErrMerge 590 if errors.As(err, &mergeErr) { 591 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 592 for i, conflict := range mergeErr.Conflicts { 593 conflicts[i] = types.ConflictInfo{ 594 Filename: conflict.Filename, 595 Reason: conflict.Reason, 596 } 597 } 598 response := types.MergeCheckResponse{ 599 IsConflicted: true, 600 Conflicts: conflicts, 601 Message: mergeErr.Message, 602 } 603 writeConflict(w, response) 604 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr) 605 } else { 606 writeError(w, err.Error(), http.StatusBadRequest) 607 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error()) 608 } 609 return 610 } 611 612 w.WriteHeader(http.StatusOK) 613} 614 615func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) { 616 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 617 618 var data struct { 619 Patch string `json:"patch"` 620 Branch string `json:"branch"` 621 } 622 623 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 624 writeError(w, err.Error(), http.StatusBadRequest) 625 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err) 626 return 627 } 628 629 patch := data.Patch 630 branch := data.Branch 631 gr, err := git.Open(path, branch) 632 if err != nil { 633 notFound(w) 634 return 635 } 636 637 err = gr.MergeCheck([]byte(patch), branch) 638 if err == nil { 639 response := types.MergeCheckResponse{ 640 IsConflicted: false, 641 } 642 writeJSON(w, response) 643 return 644 } 645 646 var mergeErr *git.ErrMerge 647 if errors.As(err, &mergeErr) { 648 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 649 for i, conflict := range mergeErr.Conflicts { 650 conflicts[i] = types.ConflictInfo{ 651 Filename: conflict.Filename, 652 Reason: conflict.Reason, 653 } 654 } 655 response := types.MergeCheckResponse{ 656 IsConflicted: true, 657 Conflicts: conflicts, 658 Message: mergeErr.Message, 659 } 660 writeConflict(w, response) 661 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error()) 662 return 663 } 664 writeError(w, err.Error(), http.StatusInternalServerError) 665 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error()) 666} 667 668func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { 669 l := h.l.With("handler", "AddMember") 670 671 data := struct { 672 Did string `json:"did"` 673 }{} 674 675 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 676 writeError(w, "invalid request body", http.StatusBadRequest) 677 return 678 } 679 680 did := data.Did 681 682 if err := h.db.AddDid(did); err != nil { 683 l.Error("adding did", "error", err.Error()) 684 writeError(w, err.Error(), http.StatusInternalServerError) 685 return 686 } 687 h.jc.AddDid(did) 688 689 if err := h.e.AddMember(ThisServer, did); err != nil { 690 l.Error("adding member", "error", err.Error()) 691 writeError(w, err.Error(), http.StatusInternalServerError) 692 return 693 } 694 695 if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 696 l.Error("fetching and adding keys", "error", err.Error()) 697 writeError(w, err.Error(), http.StatusInternalServerError) 698 return 699 } 700 701 w.WriteHeader(http.StatusNoContent) 702} 703 704func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 705 l := h.l.With("handler", "AddRepoCollaborator") 706 707 data := struct { 708 Did string `json:"did"` 709 }{} 710 711 ownerDid := chi.URLParam(r, "did") 712 repo := chi.URLParam(r, "name") 713 714 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 715 writeError(w, "invalid request body", http.StatusBadRequest) 716 return 717 } 718 719 if err := h.db.AddDid(data.Did); err != nil { 720 l.Error("adding did", "error", err.Error()) 721 writeError(w, err.Error(), http.StatusInternalServerError) 722 return 723 } 724 h.jc.AddDid(data.Did) 725 726 repoName, _ := securejoin.SecureJoin(ownerDid, repo) 727 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil { 728 l.Error("adding repo collaborator", "error", err.Error()) 729 writeError(w, err.Error(), http.StatusInternalServerError) 730 return 731 } 732 733 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 734 l.Error("fetching and adding keys", "error", err.Error()) 735 writeError(w, err.Error(), http.StatusInternalServerError) 736 return 737 } 738 739 w.WriteHeader(http.StatusNoContent) 740} 741 742func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { 743 l := h.l.With("handler", "Init") 744 745 if h.knotInitialized { 746 writeError(w, "knot already initialized", http.StatusConflict) 747 return 748 } 749 750 data := struct { 751 Did string `json:"did"` 752 }{} 753 754 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 755 l.Error("failed to decode request body", "error", err.Error()) 756 writeError(w, "invalid request body", http.StatusBadRequest) 757 return 758 } 759 760 if data.Did == "" { 761 l.Error("empty DID in request", "did", data.Did) 762 writeError(w, "did is empty", http.StatusBadRequest) 763 return 764 } 765 766 if err := h.db.AddDid(data.Did); err != nil { 767 l.Error("failed to add DID", "error", err.Error()) 768 writeError(w, err.Error(), http.StatusInternalServerError) 769 return 770 } 771 h.jc.AddDid(data.Did) 772 773 if err := h.e.AddOwner(ThisServer, data.Did); err != nil { 774 l.Error("adding owner", "error", err.Error()) 775 writeError(w, err.Error(), http.StatusInternalServerError) 776 return 777 } 778 779 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 780 l.Error("fetching and adding keys", "error", err.Error()) 781 writeError(w, err.Error(), http.StatusInternalServerError) 782 return 783 } 784 785 close(h.init) 786 787 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret)) 788 mac.Write([]byte("ok")) 789 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil))) 790 791 w.WriteHeader(http.StatusNoContent) 792} 793 794func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 795 w.Write([]byte("ok")) 796}