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