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