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 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 Ref: types.Reference{ 346 Name: tag.Name(), 347 Hash: tag.Hash().String(), 348 }, 349 Tag: tag.TagObject(), 350 } 351 352 if tag.Message() != "" { 353 tr.Message = tag.Message() 354 } 355 356 rtags = append(rtags, &tr) 357 } 358 359 resp := types.RepoTagsResponse{ 360 Tags: rtags, 361 } 362 363 writeJSON(w, resp) 364 return 365} 366 367func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) { 368 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 369 l := h.l.With("handler", "Branches") 370 371 gr, err := git.Open(path, "") 372 if err != nil { 373 notFound(w) 374 return 375 } 376 377 branches, err := gr.Branches() 378 if err != nil { 379 l.Error("getting branches", "error", err.Error()) 380 writeError(w, err.Error(), http.StatusInternalServerError) 381 return 382 } 383 384 bs := []types.Branch{} 385 for _, branch := range branches { 386 b := types.Branch{} 387 b.Hash = branch.Hash().String() 388 b.Name = branch.Name().Short() 389 bs = append(bs, b) 390 } 391 392 resp := types.RepoBranchesResponse{ 393 Branches: bs, 394 } 395 396 writeJSON(w, resp) 397 return 398} 399 400func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 401 l := h.l.With("handler", "Keys") 402 403 switch r.Method { 404 case http.MethodGet: 405 keys, err := h.db.GetAllPublicKeys() 406 if err != nil { 407 writeError(w, err.Error(), http.StatusInternalServerError) 408 l.Error("getting public keys", "error", err.Error()) 409 return 410 } 411 412 data := make([]map[string]interface{}, 0) 413 for _, key := range keys { 414 j := key.JSON() 415 data = append(data, j) 416 } 417 writeJSON(w, data) 418 return 419 420 case http.MethodPut: 421 pk := db.PublicKey{} 422 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil { 423 writeError(w, "invalid request body", http.StatusBadRequest) 424 return 425 } 426 427 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key)) 428 if err != nil { 429 writeError(w, "invalid pubkey", http.StatusBadRequest) 430 } 431 432 if err := h.db.AddPublicKey(pk); err != nil { 433 writeError(w, err.Error(), http.StatusInternalServerError) 434 l.Error("adding public key", "error", err.Error()) 435 return 436 } 437 438 w.WriteHeader(http.StatusNoContent) 439 return 440 } 441} 442 443func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) { 444 l := h.l.With("handler", "NewRepo") 445 446 data := struct { 447 Did string `json:"did"` 448 Name string `json:"name"` 449 }{} 450 451 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 452 writeError(w, "invalid request body", http.StatusBadRequest) 453 return 454 } 455 456 did := data.Did 457 name := data.Name 458 459 relativeRepoPath := filepath.Join(did, name) 460 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 461 err := git.InitBare(repoPath) 462 if err != nil { 463 l.Error("initializing bare repo", "error", err.Error()) 464 writeError(w, err.Error(), http.StatusInternalServerError) 465 return 466 } 467 468 // add perms for this user to access the repo 469 err = h.e.AddRepo(did, ThisServer, relativeRepoPath) 470 if err != nil { 471 l.Error("adding repo permissions", "error", err.Error()) 472 writeError(w, err.Error(), http.StatusInternalServerError) 473 return 474 } 475 476 w.WriteHeader(http.StatusNoContent) 477} 478 479func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { 480 l := h.l.With("handler", "AddMember") 481 482 data := struct { 483 Did string `json:"did"` 484 }{} 485 486 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 487 writeError(w, "invalid request body", http.StatusBadRequest) 488 return 489 } 490 491 did := data.Did 492 493 if err := h.db.AddDid(did); err != nil { 494 l.Error("adding did", "error", err.Error()) 495 writeError(w, err.Error(), http.StatusInternalServerError) 496 return 497 } 498 499 h.jc.AddDid(did) 500 if err := h.e.AddMember(ThisServer, did); err != nil { 501 l.Error("adding member", "error", err.Error()) 502 writeError(w, err.Error(), http.StatusInternalServerError) 503 return 504 } 505 506 if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 507 l.Error("fetching and adding keys", "error", err.Error()) 508 writeError(w, err.Error(), http.StatusInternalServerError) 509 return 510 } 511 512 w.WriteHeader(http.StatusNoContent) 513} 514 515func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 516 l := h.l.With("handler", "AddRepoCollaborator") 517 518 data := struct { 519 Did string `json:"did"` 520 }{} 521 522 ownerDid := chi.URLParam(r, "did") 523 repo := chi.URLParam(r, "name") 524 525 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 526 writeError(w, "invalid request body", http.StatusBadRequest) 527 return 528 } 529 530 if err := h.db.AddDid(data.Did); err != nil { 531 l.Error("adding did", "error", err.Error()) 532 writeError(w, err.Error(), http.StatusInternalServerError) 533 return 534 } 535 h.jc.AddDid(data.Did) 536 537 repoName, _ := securejoin.SecureJoin(ownerDid, repo) 538 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil { 539 l.Error("adding repo collaborator", "error", err.Error()) 540 writeError(w, err.Error(), http.StatusInternalServerError) 541 return 542 } 543 544 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 545 l.Error("fetching and adding keys", "error", err.Error()) 546 writeError(w, err.Error(), http.StatusInternalServerError) 547 return 548 } 549 550 w.WriteHeader(http.StatusNoContent) 551} 552 553func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { 554 l := h.l.With("handler", "Init") 555 556 if h.knotInitialized { 557 writeError(w, "knot already initialized", http.StatusConflict) 558 return 559 } 560 561 data := struct { 562 Did string `json:"did"` 563 }{} 564 565 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 566 l.Error("failed to decode request body", "error", err.Error()) 567 writeError(w, "invalid request body", http.StatusBadRequest) 568 return 569 } 570 571 if data.Did == "" { 572 l.Error("empty DID in request", "did", data.Did) 573 writeError(w, "did is empty", http.StatusBadRequest) 574 return 575 } 576 577 if err := h.db.AddDid(data.Did); err != nil { 578 l.Error("failed to add DID", "error", err.Error()) 579 writeError(w, err.Error(), http.StatusInternalServerError) 580 return 581 } 582 583 h.jc.UpdateDids([]string{data.Did}) 584 if err := h.e.AddOwner(ThisServer, data.Did); err != nil { 585 l.Error("adding owner", "error", err.Error()) 586 writeError(w, err.Error(), http.StatusInternalServerError) 587 return 588 } 589 590 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 591 l.Error("fetching and adding keys", "error", err.Error()) 592 writeError(w, err.Error(), http.StatusInternalServerError) 593 return 594 } 595 596 close(h.init) 597 598 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret)) 599 mac.Write([]byte("ok")) 600 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil))) 601 602 w.WriteHeader(http.StatusNoContent) 603} 604 605func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 606 w.Write([]byte("ok")) 607}