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