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