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