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