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}