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