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