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