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) AddMember(w http.ResponseWriter, r *http.Request) { 560 l := h.l.With("handler", "AddMember") 561 562 data := struct { 563 Did string `json:"did"` 564 }{} 565 566 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 567 writeError(w, "invalid request body", http.StatusBadRequest) 568 return 569 } 570 571 did := data.Did 572 573 if err := h.db.AddDid(did); err != nil { 574 l.Error("adding did", "error", err.Error()) 575 writeError(w, err.Error(), http.StatusInternalServerError) 576 return 577 } 578 579 h.jc.AddDid(did) 580 if err := h.e.AddMember(ThisServer, did); err != nil { 581 l.Error("adding member", "error", err.Error()) 582 writeError(w, err.Error(), http.StatusInternalServerError) 583 return 584 } 585 586 if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 587 l.Error("fetching and adding keys", "error", err.Error()) 588 writeError(w, err.Error(), http.StatusInternalServerError) 589 return 590 } 591 592 w.WriteHeader(http.StatusNoContent) 593} 594 595func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 596 l := h.l.With("handler", "AddRepoCollaborator") 597 598 data := struct { 599 Did string `json:"did"` 600 }{} 601 602 ownerDid := chi.URLParam(r, "did") 603 repo := chi.URLParam(r, "name") 604 605 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 606 writeError(w, "invalid request body", http.StatusBadRequest) 607 return 608 } 609 610 if err := h.db.AddDid(data.Did); err != nil { 611 l.Error("adding did", "error", err.Error()) 612 writeError(w, err.Error(), http.StatusInternalServerError) 613 return 614 } 615 h.jc.AddDid(data.Did) 616 617 repoName, _ := securejoin.SecureJoin(ownerDid, repo) 618 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil { 619 l.Error("adding repo collaborator", "error", err.Error()) 620 writeError(w, err.Error(), http.StatusInternalServerError) 621 return 622 } 623 624 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 625 l.Error("fetching and adding keys", "error", err.Error()) 626 writeError(w, err.Error(), http.StatusInternalServerError) 627 return 628 } 629 630 w.WriteHeader(http.StatusNoContent) 631} 632 633func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { 634 l := h.l.With("handler", "Init") 635 636 if h.knotInitialized { 637 writeError(w, "knot already initialized", http.StatusConflict) 638 return 639 } 640 641 data := struct { 642 Did string `json:"did"` 643 }{} 644 645 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 646 l.Error("failed to decode request body", "error", err.Error()) 647 writeError(w, "invalid request body", http.StatusBadRequest) 648 return 649 } 650 651 if data.Did == "" { 652 l.Error("empty DID in request", "did", data.Did) 653 writeError(w, "did is empty", http.StatusBadRequest) 654 return 655 } 656 657 if err := h.db.AddDid(data.Did); err != nil { 658 l.Error("failed to add DID", "error", err.Error()) 659 writeError(w, err.Error(), http.StatusInternalServerError) 660 return 661 } 662 663 h.jc.UpdateDids([]string{data.Did}) 664 if err := h.e.AddOwner(ThisServer, data.Did); err != nil { 665 l.Error("adding owner", "error", err.Error()) 666 writeError(w, err.Error(), http.StatusInternalServerError) 667 return 668 } 669 670 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 671 l.Error("fetching and adding keys", "error", err.Error()) 672 writeError(w, err.Error(), http.StatusInternalServerError) 673 return 674 } 675 676 close(h.init) 677 678 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret)) 679 mac.Write([]byte("ok")) 680 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil))) 681 682 w.WriteHeader(http.StatusNoContent) 683} 684 685func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 686 w.Write([]byte("ok")) 687}