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