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