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 "log" 13 "net/http" 14 "path/filepath" 15 "strconv" 16 "strings" 17 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) 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://knots.sh")) 29} 30 31func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) { 32 path := filepath.Join(h.c.Repo.ScanPath, didPath(r)) 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 log.Println(err) 41 notFound(w) 42 return 43 } 44 } 45 commits, err := gr.Commits() 46 if err != nil { 47 writeError(w, err.Error(), http.StatusInternalServerError) 48 log.Println(err) 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 log.Printf("no readme found for %s", path) 77 } 78 79 mainBranch, err := gr.FindMainBranch(h.c.Repo.MainBranch) 80 if err != nil { 81 writeError(w, err.Error(), http.StatusInternalServerError) 82 log.Println(err) 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 path := filepath.Join(h.c.Repo.ScanPath, didPath(r)) 104 gr, err := git.Open(path, ref) 105 if err != nil { 106 notFound(w) 107 return 108 } 109 110 files, err := gr.FileTree(treePath) 111 if err != nil { 112 writeError(w, err.Error(), http.StatusInternalServerError) 113 log.Println(err) 114 return 115 } 116 117 data := make(map[string]any) 118 data["ref"] = ref 119 data["parent"] = treePath 120 data["desc"] = getDescription(path) 121 data["dotdot"] = filepath.Dir(treePath) 122 123 h.listFiles(files, data, w) 124 return 125} 126 127func (h *Handle) FileContent(w http.ResponseWriter, r *http.Request) { 128 var raw bool 129 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil { 130 raw = rawParam 131 } 132 133 treePath := chi.URLParam(r, "*") 134 ref := chi.URLParam(r, "ref") 135 136 path := filepath.Join(h.c.Repo.ScanPath, didPath(r)) 137 gr, err := git.Open(path, ref) 138 if err != nil { 139 notFound(w) 140 return 141 } 142 143 contents, err := gr.FileContent(treePath) 144 if err != nil { 145 writeError(w, err.Error(), http.StatusInternalServerError) 146 return 147 } 148 data := make(map[string]any) 149 data["ref"] = ref 150 data["desc"] = getDescription(path) 151 data["path"] = treePath 152 153 safe := sanitize([]byte(contents)) 154 155 if raw { 156 h.showRaw(string(safe), w) 157 } else { 158 h.showFile(string(safe), data, w) 159 } 160} 161 162func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) { 163 name := chi.URLParam(r, "name") 164 file := chi.URLParam(r, "file") 165 166 // TODO: extend this to add more files compression (e.g.: xz) 167 if !strings.HasSuffix(file, ".tar.gz") { 168 notFound(w) 169 return 170 } 171 172 ref := strings.TrimSuffix(file, ".tar.gz") 173 174 // This allows the browser to use a proper name for the file when 175 // downloading 176 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref) 177 setContentDisposition(w, filename) 178 setGZipMIME(w) 179 180 path := filepath.Join(h.c.Repo.ScanPath, didPath(r)) 181 gr, err := git.Open(path, ref) 182 if err != nil { 183 notFound(w) 184 return 185 } 186 187 gw := gzip.NewWriter(w) 188 defer gw.Close() 189 190 prefix := fmt.Sprintf("%s-%s", name, ref) 191 err = gr.WriteTar(gw, prefix) 192 if err != nil { 193 // once we start writing to the body we can't report error anymore 194 // so we are only left with printing the error. 195 log.Println(err) 196 return 197 } 198 199 err = gw.Flush() 200 if err != nil { 201 // once we start writing to the body we can't report error anymore 202 // so we are only left with printing the error. 203 log.Println(err) 204 return 205 } 206} 207 208func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 209 fmt.Println(r.URL.Path) 210 ref := chi.URLParam(r, "ref") 211 212 path := filepath.Join(h.c.Repo.ScanPath, didPath(r)) 213 gr, err := git.Open(path, ref) 214 if err != nil { 215 notFound(w) 216 return 217 } 218 219 commits, err := gr.Commits() 220 if err != nil { 221 writeError(w, err.Error(), http.StatusInternalServerError) 222 log.Println(err) 223 return 224 } 225 226 // Get page parameters 227 page := 1 228 pageSize := 30 229 230 if pageParam := r.URL.Query().Get("page"); pageParam != "" { 231 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 { 232 page = p 233 } 234 } 235 236 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" { 237 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 { 238 pageSize = ps 239 } 240 } 241 242 // Calculate pagination 243 start := (page - 1) * pageSize 244 end := start + pageSize 245 total := len(commits) 246 247 if start >= total { 248 commits = []*object.Commit{} 249 } else { 250 if end > total { 251 end = total 252 } 253 commits = commits[start:end] 254 } 255 256 data := make(map[string]interface{}) 257 data["commits"] = commits 258 data["ref"] = ref 259 data["desc"] = getDescription(path) 260 data["log"] = true 261 data["total"] = total 262 data["page"] = page 263 data["per_page"] = pageSize 264 265 writeJSON(w, data) 266 return 267} 268 269func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) { 270 ref := chi.URLParam(r, "ref") 271 272 path := filepath.Join(h.c.Repo.ScanPath, didPath(r)) 273 gr, err := git.Open(path, ref) 274 if err != nil { 275 notFound(w) 276 return 277 } 278 279 diff, err := gr.Diff() 280 if err != nil { 281 writeError(w, err.Error(), http.StatusInternalServerError) 282 log.Println(err) 283 return 284 } 285 286 data := make(map[string]interface{}) 287 288 data["commit"] = diff.Commit 289 data["stat"] = diff.Stat 290 data["diff"] = diff.Diff 291 data["ref"] = ref 292 data["desc"] = getDescription(path) 293 294 writeJSON(w, data) 295 return 296} 297 298func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) { 299 path := filepath.Join(h.c.Repo.ScanPath, didPath(r)) 300 gr, err := git.Open(path, "") 301 if err != nil { 302 notFound(w) 303 return 304 } 305 306 tags, err := gr.Tags() 307 if err != nil { 308 // Non-fatal, we *should* have at least one branch to show. 309 log.Println(err) 310 } 311 312 branches, err := gr.Branches() 313 if err != nil { 314 log.Println(err) 315 writeError(w, err.Error(), http.StatusInternalServerError) 316 return 317 } 318 319 data := make(map[string]interface{}) 320 321 data["branches"] = branches 322 data["tags"] = tags 323 data["desc"] = getDescription(path) 324 325 writeJSON(w, data) 326 return 327} 328 329func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 330 switch r.Method { 331 case http.MethodGet: 332 keys, err := h.db.GetAllPublicKeys() 333 if err != nil { 334 writeError(w, err.Error(), http.StatusInternalServerError) 335 log.Println(err) 336 return 337 } 338 339 data := make([]map[string]interface{}, 0) 340 for _, key := range keys { 341 j := key.JSON() 342 data = append(data, j) 343 } 344 writeJSON(w, data) 345 return 346 347 case http.MethodPut: 348 pk := db.PublicKey{} 349 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil { 350 writeError(w, "invalid request body", http.StatusBadRequest) 351 return 352 } 353 354 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key)) 355 if err != nil { 356 writeError(w, "invalid pubkey", http.StatusBadRequest) 357 } 358 359 if err := h.db.AddPublicKey(pk); err != nil { 360 writeError(w, err.Error(), http.StatusInternalServerError) 361 log.Printf("adding public key: %s", err) 362 return 363 } 364 365 w.WriteHeader(http.StatusNoContent) 366 return 367 } 368} 369 370func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) { 371 data := struct { 372 DID string `json:"did"` 373 Name string `json:"name"` 374 }{} 375 376 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 377 writeError(w, "invalid request body", http.StatusBadRequest) 378 return 379 } 380 381 did := data.DID 382 name := data.Name 383 384 repoPath := filepath.Join(h.c.Repo.ScanPath, did, name) 385 err := git.InitBare(repoPath) 386 if err != nil { 387 writeError(w, err.Error(), http.StatusInternalServerError) 388 return 389 } 390 391 w.WriteHeader(http.StatusNoContent) 392} 393 394func (h *Handle) AddUser(w http.ResponseWriter, r *http.Request) { 395 data := struct { 396 DID string `json:"did"` 397 PublicKey string `json:"pubkey"` 398 }{} 399 400 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 401 writeError(w, "invalid request body", http.StatusBadRequest) 402 return 403 } 404 405 did := data.DID 406 key := data.PublicKey 407 408 if err := h.db.AddUser(did); err == nil { 409 pk := db.PublicKey{ 410 Did: did, 411 } 412 pk.Key = key 413 pk.Name = "default" 414 err := h.db.AddPublicKey(pk) 415 if err != nil { 416 writeError(w, err.Error(), http.StatusInternalServerError) 417 return 418 } 419 } else { 420 writeError(w, err.Error(), http.StatusInternalServerError) 421 return 422 } 423 424 h.js.UpdateDids([]string{did}) 425 426 w.WriteHeader(http.StatusNoContent) 427} 428 429// TODO: make this set the initial user as the owner 430func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { 431 if h.knotInitialized { 432 writeError(w, "knot already initialized", http.StatusConflict) 433 return 434 } 435 436 data := struct { 437 DID string `json:"did"` 438 PublicKey string `json:"pubkey"` 439 }{} 440 441 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 442 writeError(w, "invalid request body", http.StatusBadRequest) 443 return 444 } 445 446 did := data.DID 447 key := data.PublicKey 448 449 if err := h.db.AddUser(did); err == nil { 450 pk := db.PublicKey{ 451 Did: did, 452 } 453 pk.Key = key 454 pk.Name = "default" 455 err := h.db.AddPublicKey(pk) 456 if err != nil { 457 writeError(w, err.Error(), http.StatusInternalServerError) 458 return 459 } 460 } else { 461 writeError(w, err.Error(), http.StatusInternalServerError) 462 return 463 } 464 465 h.js.UpdateDids([]string{did}) 466 // Signal that the knot is ready 467 close(h.init) 468 w.WriteHeader(http.StatusNoContent) 469} 470 471func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 472 log.Println("got health check") 473 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret)) 474 mac.Write([]byte("ok")) 475 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil))) 476 477 w.Write([]byte("ok")) 478}