1package knotserver
2
3import (
4 "compress/gzip"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "log"
12 "net/http"
13 "os"
14 "path/filepath"
15 "strconv"
16 "strings"
17
18 securejoin "github.com/cyphar/filepath-securejoin"
19 "github.com/gliderlabs/ssh"
20 "github.com/go-chi/chi/v5"
21 gogit "github.com/go-git/go-git/v5"
22 "github.com/go-git/go-git/v5/plumbing"
23 "github.com/go-git/go-git/v5/plumbing/object"
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 log.Println(err)
41 if errors.Is(err, plumbing.ErrReferenceNotFound) {
42 resp := types.RepoIndexResponse{
43 IsEmpty: true,
44 }
45 writeJSON(w, resp)
46 return
47 } else {
48 l.Error("opening repo", "error", err.Error())
49 notFound(w)
50 return
51 }
52 }
53
54 commits, err := gr.Commits()
55 total := len(commits)
56 if err != nil {
57 writeError(w, err.Error(), http.StatusInternalServerError)
58 l.Error("fetching commits", "error", err.Error())
59 return
60 }
61 if len(commits) > 10 {
62 commits = commits[:10]
63 }
64
65 branches, err := gr.Branches()
66 if err != nil {
67 l.Error("getting branches", "error", err.Error())
68 writeError(w, err.Error(), http.StatusInternalServerError)
69 return
70 }
71
72 bs := []types.Branch{}
73 for _, branch := range branches {
74 b := types.Branch{}
75 b.Hash = branch.Hash().String()
76 b.Name = branch.Name().Short()
77 bs = append(bs, b)
78 }
79
80 tags, err := gr.Tags()
81 if err != nil {
82 // Non-fatal, we *should* have at least one branch to show.
83 l.Warn("getting tags", "error", err.Error())
84 }
85
86 rtags := []*types.TagReference{}
87 for _, tag := range tags {
88 tr := types.TagReference{
89 Tag: tag.TagObject(),
90 }
91
92 tr.Reference = types.Reference{
93 Name: tag.Name(),
94 Hash: tag.Hash().String(),
95 }
96
97 if tag.Message() != "" {
98 tr.Message = tag.Message()
99 }
100
101 rtags = append(rtags, &tr)
102 }
103
104 var readmeContent string
105 var readmeFile string
106 for _, readme := range h.c.Repo.Readme {
107 content, _ := gr.FileContent(readme)
108 if len(content) > 0 {
109 readmeContent = string(content)
110 readmeFile = readme
111 }
112 }
113
114 files, err := gr.FileTree("")
115 if err != nil {
116 writeError(w, err.Error(), http.StatusInternalServerError)
117 l.Error("file tree", "error", err.Error())
118 return
119 }
120
121 if ref == "" {
122 mainBranch, err := gr.FindMainBranch()
123 if err != nil {
124 writeError(w, err.Error(), http.StatusInternalServerError)
125 l.Error("finding main branch", "error", err.Error())
126 return
127 }
128 ref = mainBranch
129 }
130
131 resp := types.RepoIndexResponse{
132 IsEmpty: false,
133 Ref: ref,
134 Commits: commits,
135 Description: getDescription(path),
136 Readme: readmeContent,
137 ReadmeFileName: readmeFile,
138 Files: files,
139 Branches: bs,
140 Tags: rtags,
141 TotalCommits: total,
142 }
143
144 writeJSON(w, resp)
145 return
146}
147
148func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
149 treePath := chi.URLParam(r, "*")
150 ref := chi.URLParam(r, "ref")
151
152 l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath)
153
154 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
155 gr, err := git.Open(path, ref)
156 if err != nil {
157 notFound(w)
158 return
159 }
160
161 files, err := gr.FileTree(treePath)
162 if err != nil {
163 writeError(w, err.Error(), http.StatusInternalServerError)
164 l.Error("file tree", "error", err.Error())
165 return
166 }
167
168 resp := types.RepoTreeResponse{
169 Ref: ref,
170 Parent: treePath,
171 Description: getDescription(path),
172 DotDot: filepath.Dir(treePath),
173 Files: files,
174 }
175
176 writeJSON(w, resp)
177 return
178}
179
180func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) {
181 treePath := chi.URLParam(r, "*")
182 ref := chi.URLParam(r, "ref")
183
184 l := h.l.With("handler", "FileContent", "ref", ref, "treePath", treePath)
185
186 path, _ := securejoin.SecureJoin(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 var isBinaryFile bool = false
194 contents, err := gr.FileContent(treePath)
195 if errors.Is(err, git.ErrBinaryFile) {
196 isBinaryFile = true
197 } else if errors.Is(err, object.ErrFileNotFound) {
198 notFound(w)
199 return
200 } else if err != nil {
201 writeError(w, err.Error(), http.StatusInternalServerError)
202 return
203 }
204
205 bytes := []byte(contents)
206 // safe := string(sanitize(bytes))
207 sizeHint := len(bytes)
208
209 resp := types.RepoBlobResponse{
210 Ref: ref,
211 Contents: string(bytes),
212 Path: treePath,
213 IsBinary: isBinaryFile,
214 SizeHint: uint64(sizeHint),
215 }
216
217 h.showFile(resp, w, l)
218}
219
220func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
221 name := chi.URLParam(r, "name")
222 file := chi.URLParam(r, "file")
223
224 l := h.l.With("handler", "Archive", "name", name, "file", file)
225
226 // TODO: extend this to add more files compression (e.g.: xz)
227 if !strings.HasSuffix(file, ".tar.gz") {
228 notFound(w)
229 return
230 }
231
232 ref := strings.TrimSuffix(file, ".tar.gz")
233
234 // This allows the browser to use a proper name for the file when
235 // downloading
236 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
237 setContentDisposition(w, filename)
238 setGZipMIME(w)
239
240 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
241 gr, err := git.Open(path, ref)
242 if err != nil {
243 notFound(w)
244 return
245 }
246
247 gw := gzip.NewWriter(w)
248 defer gw.Close()
249
250 prefix := fmt.Sprintf("%s-%s", name, ref)
251 err = gr.WriteTar(gw, prefix)
252 if err != nil {
253 // once we start writing to the body we can't report error anymore
254 // so we are only left with printing the error.
255 l.Error("writing tar file", "error", err.Error())
256 return
257 }
258
259 err = gw.Flush()
260 if err != nil {
261 // once we start writing to the body we can't report error anymore
262 // so we are only left with printing the error.
263 l.Error("flushing?", "error", err.Error())
264 return
265 }
266}
267
268func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
269 ref := chi.URLParam(r, "ref")
270 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
271
272 l := h.l.With("handler", "Log", "ref", ref, "path", path)
273
274 gr, err := git.Open(path, ref)
275 if err != nil {
276 notFound(w)
277 return
278 }
279
280 commits, err := gr.Commits()
281 if err != nil {
282 writeError(w, err.Error(), http.StatusInternalServerError)
283 l.Error("fetching commits", "error", err.Error())
284 return
285 }
286
287 // Get page parameters
288 page := 1
289 pageSize := 30
290
291 if pageParam := r.URL.Query().Get("page"); pageParam != "" {
292 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
293 page = p
294 }
295 }
296
297 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
298 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
299 pageSize = ps
300 }
301 }
302
303 // Calculate pagination
304 start := (page - 1) * pageSize
305 end := start + pageSize
306 total := len(commits)
307
308 if start >= total {
309 commits = []*object.Commit{}
310 } else {
311 if end > total {
312 end = total
313 }
314 commits = commits[start:end]
315 }
316
317 resp := types.RepoLogResponse{
318 Commits: commits,
319 Ref: ref,
320 Description: getDescription(path),
321 Log: true,
322 Total: total,
323 Page: page,
324 PerPage: pageSize,
325 }
326
327 writeJSON(w, resp)
328 return
329}
330
331func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
332 ref := chi.URLParam(r, "ref")
333
334 l := h.l.With("handler", "Diff", "ref", ref)
335
336 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
337 gr, err := git.Open(path, ref)
338 if err != nil {
339 notFound(w)
340 return
341 }
342
343 diff, err := gr.Diff()
344 if err != nil {
345 writeError(w, err.Error(), http.StatusInternalServerError)
346 l.Error("getting diff", "error", err.Error())
347 return
348 }
349
350 resp := types.RepoCommitResponse{
351 Ref: ref,
352 Diff: diff,
353 }
354
355 writeJSON(w, resp)
356 return
357}
358
359func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
360 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
361 l := h.l.With("handler", "Refs")
362
363 gr, err := git.Open(path, "")
364 if err != nil {
365 notFound(w)
366 return
367 }
368
369 tags, err := gr.Tags()
370 if err != nil {
371 // Non-fatal, we *should* have at least one branch to show.
372 l.Warn("getting tags", "error", err.Error())
373 }
374
375 rtags := []*types.TagReference{}
376 for _, tag := range tags {
377 tr := types.TagReference{
378 Tag: tag.TagObject(),
379 }
380
381 tr.Reference = types.Reference{
382 Name: tag.Name(),
383 Hash: tag.Hash().String(),
384 }
385
386 if tag.Message() != "" {
387 tr.Message = tag.Message()
388 }
389
390 rtags = append(rtags, &tr)
391 }
392
393 resp := types.RepoTagsResponse{
394 Tags: rtags,
395 }
396
397 writeJSON(w, resp)
398 return
399}
400
401func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
402 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
403 l := h.l.With("handler", "Branches")
404
405 gr, err := git.Open(path, "")
406 if err != nil {
407 notFound(w)
408 return
409 }
410
411 branches, err := gr.Branches()
412 if err != nil {
413 l.Error("getting branches", "error", err.Error())
414 writeError(w, err.Error(), http.StatusInternalServerError)
415 return
416 }
417
418 bs := []types.Branch{}
419 for _, branch := range branches {
420 b := types.Branch{}
421 b.Hash = branch.Hash().String()
422 b.Name = branch.Name().Short()
423 bs = append(bs, b)
424 }
425
426 resp := types.RepoBranchesResponse{
427 Branches: bs,
428 }
429
430 writeJSON(w, resp)
431 return
432}
433
434func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
435 l := h.l.With("handler", "Keys")
436
437 switch r.Method {
438 case http.MethodGet:
439 keys, err := h.db.GetAllPublicKeys()
440 if err != nil {
441 writeError(w, err.Error(), http.StatusInternalServerError)
442 l.Error("getting public keys", "error", err.Error())
443 return
444 }
445
446 data := make([]map[string]interface{}, 0)
447 for _, key := range keys {
448 j := key.JSON()
449 data = append(data, j)
450 }
451 writeJSON(w, data)
452 return
453
454 case http.MethodPut:
455 pk := db.PublicKey{}
456 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
457 writeError(w, "invalid request body", http.StatusBadRequest)
458 return
459 }
460
461 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
462 if err != nil {
463 writeError(w, "invalid pubkey", http.StatusBadRequest)
464 }
465
466 if err := h.db.AddPublicKey(pk); err != nil {
467 writeError(w, err.Error(), http.StatusInternalServerError)
468 l.Error("adding public key", "error", err.Error())
469 return
470 }
471
472 w.WriteHeader(http.StatusNoContent)
473 return
474 }
475}
476
477func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
478 l := h.l.With("handler", "NewRepo")
479
480 data := struct {
481 Did string `json:"did"`
482 Name string `json:"name"`
483 DefaultBranch string `json:"default_branch,omitempty"`
484 }{}
485
486 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
487 writeError(w, "invalid request body", http.StatusBadRequest)
488 return
489 }
490
491 log.Println("branch", data.DefaultBranch)
492 if data.DefaultBranch == "" {
493 data.DefaultBranch = h.c.Repo.MainBranch
494 }
495
496 did := data.Did
497 name := data.Name
498 defaultBranch := data.DefaultBranch
499
500 relativeRepoPath := filepath.Join(did, name)
501 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
502 err := git.InitBare(repoPath, defaultBranch)
503 if err != nil {
504 l.Error("initializing bare repo", "error", err.Error())
505 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
506 writeError(w, "That repo already exists!", http.StatusConflict)
507 return
508 } else {
509 writeError(w, err.Error(), http.StatusInternalServerError)
510 return
511 }
512 }
513
514 // add perms for this user to access the repo
515 err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
516 if err != nil {
517 l.Error("adding repo permissions", "error", err.Error())
518 writeError(w, err.Error(), http.StatusInternalServerError)
519 return
520 }
521
522 w.WriteHeader(http.StatusNoContent)
523}
524
525func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
526 l := h.l.With("handler", "RemoveRepo")
527
528 data := struct {
529 Did string `json:"did"`
530 Name string `json:"name"`
531 }{}
532
533 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
534 writeError(w, "invalid request body", http.StatusBadRequest)
535 return
536 }
537
538 did := data.Did
539 name := data.Name
540
541 if did == "" || name == "" {
542 l.Error("invalid request body, empty did or name")
543 w.WriteHeader(http.StatusBadRequest)
544 return
545 }
546
547 relativeRepoPath := filepath.Join(did, name)
548 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
549 err := os.RemoveAll(repoPath)
550 if err != nil {
551 l.Error("removing repo", "error", err.Error())
552 writeError(w, err.Error(), http.StatusInternalServerError)
553 return
554 }
555
556 w.WriteHeader(http.StatusNoContent)
557
558}
559func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
560 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
561
562 data := types.MergeRequest{}
563
564 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
565 writeError(w, err.Error(), http.StatusBadRequest)
566 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err)
567 return
568 }
569
570 mo := &git.MergeOptions{
571 AuthorName: data.AuthorName,
572 AuthorEmail: data.AuthorEmail,
573 CommitBody: data.CommitBody,
574 CommitMessage: data.CommitMessage,
575 }
576
577 patch := data.Patch
578 branch := data.Branch
579 gr, err := git.Open(path, branch)
580 if err != nil {
581 notFound(w)
582 return
583 }
584 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
585 var mergeErr *git.ErrMerge
586 if errors.As(err, &mergeErr) {
587 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
588 for i, conflict := range mergeErr.Conflicts {
589 conflicts[i] = types.ConflictInfo{
590 Filename: conflict.Filename,
591 Reason: conflict.Reason,
592 }
593 }
594 response := types.MergeCheckResponse{
595 IsConflicted: true,
596 Conflicts: conflicts,
597 Message: mergeErr.Message,
598 }
599 writeConflict(w, response)
600 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr)
601 } else {
602 writeError(w, err.Error(), http.StatusBadRequest)
603 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error())
604 }
605 return
606 }
607
608 w.WriteHeader(http.StatusOK)
609}
610
611func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) {
612 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
613
614 var data struct {
615 Patch string `json:"patch"`
616 Branch string `json:"branch"`
617 }
618
619 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
620 writeError(w, err.Error(), http.StatusBadRequest)
621 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err)
622 return
623 }
624
625 patch := data.Patch
626 branch := data.Branch
627 gr, err := git.Open(path, branch)
628 if err != nil {
629 notFound(w)
630 return
631 }
632
633 err = gr.MergeCheck([]byte(patch), branch)
634 if err == nil {
635 response := types.MergeCheckResponse{
636 IsConflicted: false,
637 }
638 writeJSON(w, response)
639 return
640 }
641
642 var mergeErr *git.ErrMerge
643 if errors.As(err, &mergeErr) {
644 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
645 for i, conflict := range mergeErr.Conflicts {
646 conflicts[i] = types.ConflictInfo{
647 Filename: conflict.Filename,
648 Reason: conflict.Reason,
649 }
650 }
651 response := types.MergeCheckResponse{
652 IsConflicted: true,
653 Conflicts: conflicts,
654 Message: mergeErr.Message,
655 }
656 writeConflict(w, response)
657 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error())
658 return
659 }
660 writeError(w, err.Error(), http.StatusInternalServerError)
661 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
662}
663
664func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
665 l := h.l.With("handler", "AddMember")
666
667 data := struct {
668 Did string `json:"did"`
669 }{}
670
671 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
672 writeError(w, "invalid request body", http.StatusBadRequest)
673 return
674 }
675
676 did := data.Did
677
678 if err := h.db.AddDid(did); err != nil {
679 l.Error("adding did", "error", err.Error())
680 writeError(w, err.Error(), http.StatusInternalServerError)
681 return
682 }
683
684 h.jc.AddDid(did)
685 if err := h.e.AddMember(ThisServer, did); err != nil {
686 l.Error("adding member", "error", err.Error())
687 writeError(w, err.Error(), http.StatusInternalServerError)
688 return
689 }
690
691 if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
692 l.Error("fetching and adding keys", "error", err.Error())
693 writeError(w, err.Error(), http.StatusInternalServerError)
694 return
695 }
696
697 w.WriteHeader(http.StatusNoContent)
698}
699
700func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
701 l := h.l.With("handler", "AddRepoCollaborator")
702
703 data := struct {
704 Did string `json:"did"`
705 }{}
706
707 ownerDid := chi.URLParam(r, "did")
708 repo := chi.URLParam(r, "name")
709
710 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
711 writeError(w, "invalid request body", http.StatusBadRequest)
712 return
713 }
714
715 if err := h.db.AddDid(data.Did); err != nil {
716 l.Error("adding did", "error", err.Error())
717 writeError(w, err.Error(), http.StatusInternalServerError)
718 return
719 }
720 h.jc.AddDid(data.Did)
721
722 repoName, _ := securejoin.SecureJoin(ownerDid, repo)
723 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
724 l.Error("adding repo collaborator", "error", err.Error())
725 writeError(w, err.Error(), http.StatusInternalServerError)
726 return
727 }
728
729 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
730 l.Error("fetching and adding keys", "error", err.Error())
731 writeError(w, err.Error(), http.StatusInternalServerError)
732 return
733 }
734
735 w.WriteHeader(http.StatusNoContent)
736}
737
738func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
739 l := h.l.With("handler", "Init")
740
741 if h.knotInitialized {
742 writeError(w, "knot already initialized", http.StatusConflict)
743 return
744 }
745
746 data := struct {
747 Did string `json:"did"`
748 }{}
749
750 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
751 l.Error("failed to decode request body", "error", err.Error())
752 writeError(w, "invalid request body", http.StatusBadRequest)
753 return
754 }
755
756 if data.Did == "" {
757 l.Error("empty DID in request", "did", data.Did)
758 writeError(w, "did is empty", http.StatusBadRequest)
759 return
760 }
761
762 if err := h.db.AddDid(data.Did); err != nil {
763 l.Error("failed to add DID", "error", err.Error())
764 writeError(w, err.Error(), http.StatusInternalServerError)
765 return
766 }
767
768 h.jc.UpdateDids([]string{data.Did})
769 if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
770 l.Error("adding owner", "error", err.Error())
771 writeError(w, err.Error(), http.StatusInternalServerError)
772 return
773 }
774
775 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
776 l.Error("fetching and adding keys", "error", err.Error())
777 writeError(w, err.Error(), http.StatusInternalServerError)
778 return
779 }
780
781 close(h.init)
782
783 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
784 mac.Write([]byte("ok"))
785 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
786
787 w.WriteHeader(http.StatusNoContent)
788}
789
790func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
791 w.Write([]byte("ok"))
792}