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