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