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