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