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