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