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 "github.com/sotangled/tangled/knotserver/db"
26 "github.com/sotangled/tangled/knotserver/git"
27 "github.com/sotangled/tangled/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
187 l := h.l.With("handler", "FileContent", "ref", ref, "treePath", treePath)
188
189 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
190 gr, err := git.Open(path, ref)
191 if err != nil {
192 notFound(w)
193 return
194 }
195
196 var isBinaryFile bool = false
197 contents, err := gr.FileContent(treePath)
198 if errors.Is(err, git.ErrBinaryFile) {
199 isBinaryFile = true
200 } else if errors.Is(err, object.ErrFileNotFound) {
201 notFound(w)
202 return
203 } else if err != nil {
204 writeError(w, err.Error(), http.StatusInternalServerError)
205 return
206 }
207
208 bytes := []byte(contents)
209 // safe := string(sanitize(bytes))
210 sizeHint := len(bytes)
211
212 resp := types.RepoBlobResponse{
213 Ref: ref,
214 Contents: string(bytes),
215 Path: treePath,
216 IsBinary: isBinaryFile,
217 SizeHint: uint64(sizeHint),
218 }
219
220 h.showFile(resp, w, l)
221}
222
223func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
224 name := chi.URLParam(r, "name")
225 file := chi.URLParam(r, "file")
226
227 l := h.l.With("handler", "Archive", "name", name, "file", file)
228
229 // TODO: extend this to add more files compression (e.g.: xz)
230 if !strings.HasSuffix(file, ".tar.gz") {
231 notFound(w)
232 return
233 }
234
235 ref := strings.TrimSuffix(file, ".tar.gz")
236
237 // This allows the browser to use a proper name for the file when
238 // downloading
239 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
240 setContentDisposition(w, filename)
241 setGZipMIME(w)
242
243 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
244 gr, err := git.Open(path, ref)
245 if err != nil {
246 notFound(w)
247 return
248 }
249
250 gw := gzip.NewWriter(w)
251 defer gw.Close()
252
253 prefix := fmt.Sprintf("%s-%s", name, ref)
254 err = gr.WriteTar(gw, prefix)
255 if err != nil {
256 // once we start writing to the body we can't report error anymore
257 // so we are only left with printing the error.
258 l.Error("writing tar file", "error", err.Error())
259 return
260 }
261
262 err = gw.Flush()
263 if err != nil {
264 // once we start writing to the body we can't report error anymore
265 // so we are only left with printing the error.
266 l.Error("flushing?", "error", err.Error())
267 return
268 }
269}
270
271func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
272 ref := chi.URLParam(r, "ref")
273 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
274
275 l := h.l.With("handler", "Log", "ref", ref, "path", path)
276
277 gr, err := git.Open(path, ref)
278 if err != nil {
279 notFound(w)
280 return
281 }
282
283 commits, err := gr.Commits()
284 if err != nil {
285 writeError(w, err.Error(), http.StatusInternalServerError)
286 l.Error("fetching commits", "error", err.Error())
287 return
288 }
289
290 // Get page parameters
291 page := 1
292 pageSize := 30
293
294 if pageParam := r.URL.Query().Get("page"); pageParam != "" {
295 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
296 page = p
297 }
298 }
299
300 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
301 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
302 pageSize = ps
303 }
304 }
305
306 // Calculate pagination
307 start := (page - 1) * pageSize
308 end := start + pageSize
309 total := len(commits)
310
311 if start >= total {
312 commits = []*object.Commit{}
313 } else {
314 if end > total {
315 end = total
316 }
317 commits = commits[start:end]
318 }
319
320 resp := types.RepoLogResponse{
321 Commits: commits,
322 Ref: ref,
323 Description: getDescription(path),
324 Log: true,
325 Total: total,
326 Page: page,
327 PerPage: pageSize,
328 }
329
330 writeJSON(w, resp)
331 return
332}
333
334func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
335 ref := chi.URLParam(r, "ref")
336
337 l := h.l.With("handler", "Diff", "ref", ref)
338
339 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
340 gr, err := git.Open(path, ref)
341 if err != nil {
342 notFound(w)
343 return
344 }
345
346 diff, err := gr.Diff()
347 if err != nil {
348 writeError(w, err.Error(), http.StatusInternalServerError)
349 l.Error("getting diff", "error", err.Error())
350 return
351 }
352
353 resp := types.RepoCommitResponse{
354 Ref: ref,
355 Diff: diff,
356 }
357
358 writeJSON(w, resp)
359 return
360}
361
362func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
363 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
364 l := h.l.With("handler", "Refs")
365
366 gr, err := git.Open(path, "")
367 if err != nil {
368 notFound(w)
369 return
370 }
371
372 tags, err := gr.Tags()
373 if err != nil {
374 // Non-fatal, we *should* have at least one branch to show.
375 l.Warn("getting tags", "error", err.Error())
376 }
377
378 rtags := []*types.TagReference{}
379 for _, tag := range tags {
380 tr := types.TagReference{
381 Tag: tag.TagObject(),
382 }
383
384 tr.Reference = types.Reference{
385 Name: tag.Name(),
386 Hash: tag.Hash().String(),
387 }
388
389 if tag.Message() != "" {
390 tr.Message = tag.Message()
391 }
392
393 rtags = append(rtags, &tr)
394 }
395
396 resp := types.RepoTagsResponse{
397 Tags: rtags,
398 }
399
400 writeJSON(w, resp)
401 return
402}
403
404func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
405 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
406 l := h.l.With("handler", "Branches")
407
408 gr, err := git.Open(path, "")
409 if err != nil {
410 notFound(w)
411 return
412 }
413
414 branches, err := gr.Branches()
415 if err != nil {
416 l.Error("getting branches", "error", err.Error())
417 writeError(w, err.Error(), http.StatusInternalServerError)
418 return
419 }
420
421 bs := []types.Branch{}
422 for _, branch := range branches {
423 b := types.Branch{}
424 b.Hash = branch.Hash().String()
425 b.Name = branch.Name().Short()
426 bs = append(bs, b)
427 }
428
429 resp := types.RepoBranchesResponse{
430 Branches: bs,
431 }
432
433 writeJSON(w, resp)
434 return
435}
436
437func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
438 l := h.l.With("handler", "Keys")
439
440 switch r.Method {
441 case http.MethodGet:
442 keys, err := h.db.GetAllPublicKeys()
443 if err != nil {
444 writeError(w, err.Error(), http.StatusInternalServerError)
445 l.Error("getting public keys", "error", err.Error())
446 return
447 }
448
449 data := make([]map[string]interface{}, 0)
450 for _, key := range keys {
451 j := key.JSON()
452 data = append(data, j)
453 }
454 writeJSON(w, data)
455 return
456
457 case http.MethodPut:
458 pk := db.PublicKey{}
459 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
460 writeError(w, "invalid request body", http.StatusBadRequest)
461 return
462 }
463
464 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
465 if err != nil {
466 writeError(w, "invalid pubkey", http.StatusBadRequest)
467 }
468
469 if err := h.db.AddPublicKey(pk); err != nil {
470 writeError(w, err.Error(), http.StatusInternalServerError)
471 l.Error("adding public key", "error", err.Error())
472 return
473 }
474
475 w.WriteHeader(http.StatusNoContent)
476 return
477 }
478}
479
480func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
481 l := h.l.With("handler", "NewRepo")
482
483 data := struct {
484 Did string `json:"did"`
485 Name string `json:"name"`
486 DefaultBranch string `json:"default_branch,omitempty"`
487 }{}
488
489 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
490 writeError(w, "invalid request body", http.StatusBadRequest)
491 return
492 }
493
494 log.Println("branch", data.DefaultBranch)
495 if data.DefaultBranch == "" {
496 data.DefaultBranch = h.c.Repo.MainBranch
497 }
498
499 did := data.Did
500 name := data.Name
501 defaultBranch := data.DefaultBranch
502
503 relativeRepoPath := filepath.Join(did, name)
504 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
505 err := git.InitBare(repoPath, defaultBranch)
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) RemoveRepo(w http.ResponseWriter, r *http.Request) {
529 l := h.l.With("handler", "RemoveRepo")
530
531 data := struct {
532 Did string `json:"did"`
533 Name string `json:"name"`
534 }{}
535
536 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
537 writeError(w, "invalid request body", http.StatusBadRequest)
538 return
539 }
540
541 did := data.Did
542 name := data.Name
543
544 if did == "" || name == "" {
545 l.Error("invalid request body, empty did or name")
546 w.WriteHeader(http.StatusBadRequest)
547 return
548 }
549
550 relativeRepoPath := filepath.Join(did, name)
551 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
552 err := os.RemoveAll(repoPath)
553 if err != nil {
554 l.Error("removing repo", "error", err.Error())
555 writeError(w, err.Error(), http.StatusInternalServerError)
556 return
557 }
558
559 w.WriteHeader(http.StatusNoContent)
560
561}
562func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
563 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
564
565 data := types.MergeRequest{}
566
567 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
568 writeError(w, err.Error(), http.StatusBadRequest)
569 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err)
570 return
571 }
572
573 mo := &git.MergeOptions{
574 AuthorName: data.AuthorName,
575 AuthorEmail: data.AuthorEmail,
576 CommitBody: data.CommitBody,
577 CommitMessage: data.CommitMessage,
578 }
579
580 patch := data.Patch
581 branch := data.Branch
582 gr, err := git.Open(path, branch)
583 if err != nil {
584 notFound(w)
585 return
586 }
587 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
588 var mergeErr *git.ErrMerge
589 if errors.As(err, &mergeErr) {
590 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
591 for i, conflict := range mergeErr.Conflicts {
592 conflicts[i] = types.ConflictInfo{
593 Filename: conflict.Filename,
594 Reason: conflict.Reason,
595 }
596 }
597 response := types.MergeCheckResponse{
598 IsConflicted: true,
599 Conflicts: conflicts,
600 Message: mergeErr.Message,
601 }
602 writeConflict(w, response)
603 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr)
604 } else {
605 writeError(w, err.Error(), http.StatusBadRequest)
606 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error())
607 }
608 return
609 }
610
611 w.WriteHeader(http.StatusOK)
612}
613
614func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) {
615 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
616
617 var data struct {
618 Patch string `json:"patch"`
619 Branch string `json:"branch"`
620 }
621
622 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
623 writeError(w, err.Error(), http.StatusBadRequest)
624 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err)
625 return
626 }
627
628 patch := data.Patch
629 branch := data.Branch
630 gr, err := git.Open(path, branch)
631 if err != nil {
632 notFound(w)
633 return
634 }
635
636 err = gr.MergeCheck([]byte(patch), branch)
637 if err == nil {
638 response := types.MergeCheckResponse{
639 IsConflicted: false,
640 }
641 writeJSON(w, response)
642 return
643 }
644
645 var mergeErr *git.ErrMerge
646 if errors.As(err, &mergeErr) {
647 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
648 for i, conflict := range mergeErr.Conflicts {
649 conflicts[i] = types.ConflictInfo{
650 Filename: conflict.Filename,
651 Reason: conflict.Reason,
652 }
653 }
654 response := types.MergeCheckResponse{
655 IsConflicted: true,
656 Conflicts: conflicts,
657 Message: mergeErr.Message,
658 }
659 writeConflict(w, response)
660 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error())
661 return
662 }
663 writeError(w, err.Error(), http.StatusInternalServerError)
664 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
665}
666
667func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
668 l := h.l.With("handler", "AddMember")
669
670 data := struct {
671 Did string `json:"did"`
672 }{}
673
674 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
675 writeError(w, "invalid request body", http.StatusBadRequest)
676 return
677 }
678
679 did := data.Did
680
681 if err := h.db.AddDid(did); err != nil {
682 l.Error("adding did", "error", err.Error())
683 writeError(w, err.Error(), http.StatusInternalServerError)
684 return
685 }
686
687 h.jc.AddDid(did)
688 if err := h.e.AddMember(ThisServer, did); err != nil {
689 l.Error("adding member", "error", err.Error())
690 writeError(w, err.Error(), http.StatusInternalServerError)
691 return
692 }
693
694 if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
695 l.Error("fetching and adding keys", "error", err.Error())
696 writeError(w, err.Error(), http.StatusInternalServerError)
697 return
698 }
699
700 w.WriteHeader(http.StatusNoContent)
701}
702
703func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
704 l := h.l.With("handler", "AddRepoCollaborator")
705
706 data := struct {
707 Did string `json:"did"`
708 }{}
709
710 ownerDid := chi.URLParam(r, "did")
711 repo := chi.URLParam(r, "name")
712
713 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
714 writeError(w, "invalid request body", http.StatusBadRequest)
715 return
716 }
717
718 if err := h.db.AddDid(data.Did); err != nil {
719 l.Error("adding did", "error", err.Error())
720 writeError(w, err.Error(), http.StatusInternalServerError)
721 return
722 }
723 h.jc.AddDid(data.Did)
724
725 repoName, _ := securejoin.SecureJoin(ownerDid, repo)
726 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
727 l.Error("adding repo collaborator", "error", err.Error())
728 writeError(w, err.Error(), http.StatusInternalServerError)
729 return
730 }
731
732 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
733 l.Error("fetching and adding keys", "error", err.Error())
734 writeError(w, err.Error(), http.StatusInternalServerError)
735 return
736 }
737
738 w.WriteHeader(http.StatusNoContent)
739}
740
741func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
742 l := h.l.With("handler", "Init")
743
744 if h.knotInitialized {
745 writeError(w, "knot already initialized", http.StatusConflict)
746 return
747 }
748
749 data := struct {
750 Did string `json:"did"`
751 }{}
752
753 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
754 l.Error("failed to decode request body", "error", err.Error())
755 writeError(w, "invalid request body", http.StatusBadRequest)
756 return
757 }
758
759 if data.Did == "" {
760 l.Error("empty DID in request", "did", data.Did)
761 writeError(w, "did is empty", http.StatusBadRequest)
762 return
763 }
764
765 if err := h.db.AddDid(data.Did); err != nil {
766 l.Error("failed to add DID", "error", err.Error())
767 writeError(w, err.Error(), http.StatusInternalServerError)
768 return
769 }
770
771 h.jc.UpdateDids([]string{data.Did})
772 if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
773 l.Error("adding owner", "error", err.Error())
774 writeError(w, err.Error(), http.StatusInternalServerError)
775 return
776 }
777
778 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
779 l.Error("fetching and adding keys", "error", err.Error())
780 writeError(w, err.Error(), http.StatusInternalServerError)
781 return
782 }
783
784 close(h.init)
785
786 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
787 mac.Write([]byte("ok"))
788 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
789
790 w.WriteHeader(http.StatusNoContent)
791}
792
793func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
794 w.Write([]byte("ok"))
795}