1package knotserver
2
3import (
4 "compress/gzip"
5 "context"
6 "crypto/sha256"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "log"
11 "net/http"
12 "net/url"
13 "os"
14 "path/filepath"
15 "strconv"
16 "strings"
17 "sync"
18 "time"
19
20 securejoin "github.com/cyphar/filepath-securejoin"
21 "github.com/gliderlabs/ssh"
22 "github.com/go-chi/chi/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/hook"
26 "tangled.sh/tangled.sh/core/knotserver/db"
27 "tangled.sh/tangled.sh/core/knotserver/git"
28 "tangled.sh/tangled.sh/core/patchutil"
29 "tangled.sh/tangled.sh/core/rbac"
30 "tangled.sh/tangled.sh/core/types"
31)
32
33func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
34 w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
35}
36
37func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) {
38 w.Header().Set("Content-Type", "application/json")
39
40 capabilities := map[string]any{
41 "pull_requests": map[string]any{
42 "format_patch": true,
43 "patch_submissions": true,
44 "branch_submissions": true,
45 "fork_submissions": true,
46 },
47 }
48
49 jsonData, err := json.Marshal(capabilities)
50 if err != nil {
51 http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError)
52 return
53 }
54
55 w.Write(jsonData)
56}
57
58func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
59 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
60 l := h.l.With("path", path, "handler", "RepoIndex")
61 ref := chi.URLParam(r, "ref")
62 ref, _ = url.PathUnescape(ref)
63
64 gr, err := git.Open(path, ref)
65 if err != nil {
66 plain, err2 := git.PlainOpen(path)
67 if err2 != nil {
68 l.Error("opening repo", "error", err2.Error())
69 notFound(w)
70 return
71 }
72 branches, _ := plain.Branches()
73
74 log.Println(err)
75
76 if errors.Is(err, plumbing.ErrReferenceNotFound) {
77 resp := types.RepoIndexResponse{
78 IsEmpty: true,
79 Branches: branches,
80 }
81 writeJSON(w, resp)
82 return
83 } else {
84 l.Error("opening repo", "error", err.Error())
85 notFound(w)
86 return
87 }
88 }
89
90 var (
91 commits []*object.Commit
92 total int
93 branches []types.Branch
94 files []types.NiceTree
95 tags []object.Tag
96 )
97
98 var wg sync.WaitGroup
99 errorsCh := make(chan error, 5)
100
101 wg.Add(1)
102 go func() {
103 defer wg.Done()
104 cs, err := gr.Commits(0, 60)
105 if err != nil {
106 errorsCh <- fmt.Errorf("commits: %w", err)
107 return
108 }
109 commits = cs
110 }()
111
112 wg.Add(1)
113 go func() {
114 defer wg.Done()
115 t, err := gr.TotalCommits()
116 if err != nil {
117 errorsCh <- fmt.Errorf("calculating total: %w", err)
118 return
119 }
120 total = t
121 }()
122
123 wg.Add(1)
124 go func() {
125 defer wg.Done()
126 bs, err := gr.Branches()
127 if err != nil {
128 errorsCh <- fmt.Errorf("fetching branches: %w", err)
129 return
130 }
131 branches = bs
132 }()
133
134 wg.Add(1)
135 go func() {
136 defer wg.Done()
137 ts, err := gr.Tags()
138 if err != nil {
139 errorsCh <- fmt.Errorf("fetching tags: %w", err)
140 return
141 }
142 tags = ts
143 }()
144
145 wg.Add(1)
146 go func() {
147 defer wg.Done()
148 fs, err := gr.FileTree(r.Context(), "")
149 if err != nil {
150 errorsCh <- fmt.Errorf("fetching filetree: %w", err)
151 return
152 }
153 files = fs
154 }()
155
156 wg.Wait()
157 close(errorsCh)
158
159 // show any errors
160 for err := range errorsCh {
161 l.Error("loading repo", "error", err.Error())
162 writeError(w, err.Error(), http.StatusInternalServerError)
163 return
164 }
165
166 rtags := []*types.TagReference{}
167 for _, tag := range tags {
168 var target *object.Tag
169 if tag.Target != plumbing.ZeroHash {
170 target = &tag
171 }
172 tr := types.TagReference{
173 Tag: target,
174 }
175
176 tr.Reference = types.Reference{
177 Name: tag.Name,
178 Hash: tag.Hash.String(),
179 }
180
181 if tag.Message != "" {
182 tr.Message = tag.Message
183 }
184
185 rtags = append(rtags, &tr)
186 }
187
188 var readmeContent string
189 var readmeFile string
190 for _, readme := range h.c.Repo.Readme {
191 content, _ := gr.FileContent(readme)
192 if len(content) > 0 {
193 readmeContent = string(content)
194 readmeFile = readme
195 }
196 }
197
198 if ref == "" {
199 mainBranch, err := gr.FindMainBranch()
200 if err != nil {
201 writeError(w, err.Error(), http.StatusInternalServerError)
202 l.Error("finding main branch", "error", err.Error())
203 return
204 }
205 ref = mainBranch
206 }
207
208 resp := types.RepoIndexResponse{
209 IsEmpty: false,
210 Ref: ref,
211 Commits: commits,
212 Description: getDescription(path),
213 Readme: readmeContent,
214 ReadmeFileName: readmeFile,
215 Files: files,
216 Branches: branches,
217 Tags: rtags,
218 TotalCommits: total,
219 }
220
221 writeJSON(w, resp)
222 return
223}
224
225func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
226 treePath := chi.URLParam(r, "*")
227 ref := chi.URLParam(r, "ref")
228 ref, _ = url.PathUnescape(ref)
229
230 l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath)
231
232 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
233 gr, err := git.Open(path, ref)
234 if err != nil {
235 notFound(w)
236 return
237 }
238
239 files, err := gr.FileTree(r.Context(), treePath)
240 if err != nil {
241 writeError(w, err.Error(), http.StatusInternalServerError)
242 l.Error("file tree", "error", err.Error())
243 return
244 }
245
246 resp := types.RepoTreeResponse{
247 Ref: ref,
248 Parent: treePath,
249 Description: getDescription(path),
250 DotDot: filepath.Dir(treePath),
251 Files: files,
252 }
253
254 writeJSON(w, resp)
255 return
256}
257
258func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) {
259 treePath := chi.URLParam(r, "*")
260 ref := chi.URLParam(r, "ref")
261 ref, _ = url.PathUnescape(ref)
262
263 l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath)
264
265 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
266 gr, err := git.Open(path, ref)
267 if err != nil {
268 notFound(w)
269 return
270 }
271
272 contents, err := gr.RawContent(treePath)
273 if err != nil {
274 writeError(w, err.Error(), http.StatusBadRequest)
275 l.Error("file content", "error", err.Error())
276 return
277 }
278
279 mimeType := http.DetectContentType(contents)
280
281 // exception for svg
282 if filepath.Ext(treePath) == ".svg" {
283 mimeType = "image/svg+xml"
284 }
285
286 // allow image, video, and text/plain files to be served directly
287 switch {
288 case strings.HasPrefix(mimeType, "image/"):
289 // allowed
290 case strings.HasPrefix(mimeType, "video/"):
291 // allowed
292 case strings.HasPrefix(mimeType, "text/plain"):
293 // allowed
294 default:
295 l.Error("attempted to serve disallowed file type", "mimetype", mimeType)
296 writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
297 return
298 }
299
300 w.Header().Set("Cache-Control", "public, max-age=86400") // cache for 24 hours
301 w.Header().Set("ETag", fmt.Sprintf("%x", sha256.Sum256(contents)))
302 w.Header().Set("Content-Type", mimeType)
303 w.Write(contents)
304}
305
306func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) {
307 treePath := chi.URLParam(r, "*")
308 ref := chi.URLParam(r, "ref")
309 ref, _ = url.PathUnescape(ref)
310
311 l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath)
312
313 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
314 gr, err := git.Open(path, ref)
315 if err != nil {
316 notFound(w)
317 return
318 }
319
320 var isBinaryFile bool = false
321 contents, err := gr.FileContent(treePath)
322 if errors.Is(err, git.ErrBinaryFile) {
323 isBinaryFile = true
324 } else if errors.Is(err, object.ErrFileNotFound) {
325 notFound(w)
326 return
327 } else if err != nil {
328 writeError(w, err.Error(), http.StatusInternalServerError)
329 return
330 }
331
332 bytes := []byte(contents)
333 // safe := string(sanitize(bytes))
334 sizeHint := len(bytes)
335
336 resp := types.RepoBlobResponse{
337 Ref: ref,
338 Contents: string(bytes),
339 Path: treePath,
340 IsBinary: isBinaryFile,
341 SizeHint: uint64(sizeHint),
342 }
343
344 h.showFile(resp, w, l)
345}
346
347func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
348 name := chi.URLParam(r, "name")
349 file := chi.URLParam(r, "file")
350
351 l := h.l.With("handler", "Archive", "name", name, "file", file)
352
353 // TODO: extend this to add more files compression (e.g.: xz)
354 if !strings.HasSuffix(file, ".tar.gz") {
355 notFound(w)
356 return
357 }
358
359 ref := strings.TrimSuffix(file, ".tar.gz")
360
361 unescapedRef, err := url.PathUnescape(ref)
362 if err != nil {
363 notFound(w)
364 return
365 }
366
367 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-")
368
369 // This allows the browser to use a proper name for the file when
370 // downloading
371 filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename)
372 setContentDisposition(w, filename)
373 setGZipMIME(w)
374
375 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
376 gr, err := git.Open(path, unescapedRef)
377 if err != nil {
378 notFound(w)
379 return
380 }
381
382 gw := gzip.NewWriter(w)
383 defer gw.Close()
384
385 prefix := fmt.Sprintf("%s-%s", name, safeRefFilename)
386 err = gr.WriteTar(gw, prefix)
387 if err != nil {
388 // once we start writing to the body we can't report error anymore
389 // so we are only left with printing the error.
390 l.Error("writing tar file", "error", err.Error())
391 return
392 }
393
394 err = gw.Flush()
395 if err != nil {
396 // once we start writing to the body we can't report error anymore
397 // so we are only left with printing the error.
398 l.Error("flushing?", "error", err.Error())
399 return
400 }
401}
402
403func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
404 ref := chi.URLParam(r, "ref")
405 ref, _ = url.PathUnescape(ref)
406
407 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
408
409 l := h.l.With("handler", "Log", "ref", ref, "path", path)
410
411 gr, err := git.Open(path, ref)
412 if err != nil {
413 notFound(w)
414 return
415 }
416
417 // Get page parameters
418 page := 1
419 pageSize := 30
420
421 if pageParam := r.URL.Query().Get("page"); pageParam != "" {
422 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
423 page = p
424 }
425 }
426
427 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
428 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
429 pageSize = ps
430 }
431 }
432
433 // convert to offset/limit
434 offset := (page - 1) * pageSize
435 limit := pageSize
436
437 commits, err := gr.Commits(offset, limit)
438 if err != nil {
439 writeError(w, err.Error(), http.StatusInternalServerError)
440 l.Error("fetching commits", "error", err.Error())
441 return
442 }
443
444 total := len(commits)
445
446 resp := types.RepoLogResponse{
447 Commits: commits,
448 Ref: ref,
449 Description: getDescription(path),
450 Log: true,
451 Total: total,
452 Page: page,
453 PerPage: pageSize,
454 }
455
456 writeJSON(w, resp)
457 return
458}
459
460func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
461 ref := chi.URLParam(r, "ref")
462 ref, _ = url.PathUnescape(ref)
463
464 l := h.l.With("handler", "Diff", "ref", ref)
465
466 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
467 gr, err := git.Open(path, ref)
468 if err != nil {
469 notFound(w)
470 return
471 }
472
473 diff, err := gr.Diff()
474 if err != nil {
475 writeError(w, err.Error(), http.StatusInternalServerError)
476 l.Error("getting diff", "error", err.Error())
477 return
478 }
479
480 resp := types.RepoCommitResponse{
481 Ref: ref,
482 Diff: diff,
483 }
484
485 writeJSON(w, resp)
486 return
487}
488
489func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
490 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
491 l := h.l.With("handler", "Refs")
492
493 gr, err := git.Open(path, "")
494 if err != nil {
495 notFound(w)
496 return
497 }
498
499 tags, err := gr.Tags()
500 if err != nil {
501 // Non-fatal, we *should* have at least one branch to show.
502 l.Warn("getting tags", "error", err.Error())
503 }
504
505 rtags := []*types.TagReference{}
506 for _, tag := range tags {
507 var target *object.Tag
508 if tag.Target != plumbing.ZeroHash {
509 target = &tag
510 }
511 tr := types.TagReference{
512 Tag: target,
513 }
514
515 tr.Reference = types.Reference{
516 Name: tag.Name,
517 Hash: tag.Hash.String(),
518 }
519
520 if tag.Message != "" {
521 tr.Message = tag.Message
522 }
523
524 rtags = append(rtags, &tr)
525 }
526
527 resp := types.RepoTagsResponse{
528 Tags: rtags,
529 }
530
531 writeJSON(w, resp)
532 return
533}
534
535func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
536 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
537
538 gr, err := git.PlainOpen(path)
539 if err != nil {
540 notFound(w)
541 return
542 }
543
544 branches, _ := gr.Branches()
545
546 resp := types.RepoBranchesResponse{
547 Branches: branches,
548 }
549
550 writeJSON(w, resp)
551 return
552}
553
554func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) {
555 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
556 branchName := chi.URLParam(r, "branch")
557 branchName, _ = url.PathUnescape(branchName)
558
559 l := h.l.With("handler", "Branch")
560
561 gr, err := git.PlainOpen(path)
562 if err != nil {
563 notFound(w)
564 return
565 }
566
567 ref, err := gr.Branch(branchName)
568 if err != nil {
569 l.Error("getting branch", "error", err.Error())
570 writeError(w, err.Error(), http.StatusInternalServerError)
571 return
572 }
573
574 commit, err := gr.Commit(ref.Hash())
575 if err != nil {
576 l.Error("getting commit object", "error", err.Error())
577 writeError(w, err.Error(), http.StatusInternalServerError)
578 return
579 }
580
581 defaultBranch, err := gr.FindMainBranch()
582 isDefault := false
583 if err != nil {
584 l.Error("getting default branch", "error", err.Error())
585 // do not quit though
586 } else if defaultBranch == branchName {
587 isDefault = true
588 }
589
590 resp := types.RepoBranchResponse{
591 Branch: types.Branch{
592 Reference: types.Reference{
593 Name: ref.Name().Short(),
594 Hash: ref.Hash().String(),
595 },
596 Commit: commit,
597 IsDefault: isDefault,
598 },
599 }
600
601 writeJSON(w, resp)
602 return
603}
604
605func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
606 l := h.l.With("handler", "Keys")
607
608 switch r.Method {
609 case http.MethodGet:
610 keys, err := h.db.GetAllPublicKeys()
611 if err != nil {
612 writeError(w, err.Error(), http.StatusInternalServerError)
613 l.Error("getting public keys", "error", err.Error())
614 return
615 }
616
617 data := make([]map[string]any, 0)
618 for _, key := range keys {
619 j := key.JSON()
620 data = append(data, j)
621 }
622 writeJSON(w, data)
623 return
624
625 case http.MethodPut:
626 pk := db.PublicKey{}
627 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
628 writeError(w, "invalid request body", http.StatusBadRequest)
629 return
630 }
631
632 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
633 if err != nil {
634 writeError(w, "invalid pubkey", http.StatusBadRequest)
635 }
636
637 if err := h.db.AddPublicKey(pk); err != nil {
638 writeError(w, err.Error(), http.StatusInternalServerError)
639 l.Error("adding public key", "error", err.Error())
640 return
641 }
642
643 w.WriteHeader(http.StatusNoContent)
644 return
645 }
646}
647
648func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) {
649 l := h.l.With("handler", "RepoForkSync")
650
651 data := struct {
652 Did string `json:"did"`
653 Source string `json:"source"`
654 Name string `json:"name,omitempty"`
655 HiddenRef string `json:"hiddenref"`
656 }{}
657
658 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
659 writeError(w, "invalid request body", http.StatusBadRequest)
660 return
661 }
662
663 did := data.Did
664 source := data.Source
665
666 if did == "" || source == "" {
667 l.Error("invalid request body, empty did or name")
668 w.WriteHeader(http.StatusBadRequest)
669 return
670 }
671
672 var name string
673 if data.Name != "" {
674 name = data.Name
675 } else {
676 name = filepath.Base(source)
677 }
678
679 branch := chi.URLParam(r, "branch")
680 branch, _ = url.PathUnescape(branch)
681
682 relativeRepoPath := filepath.Join(did, name)
683 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
684
685 gr, err := git.PlainOpen(repoPath)
686 if err != nil {
687 log.Println(err)
688 notFound(w)
689 return
690 }
691
692 forkCommit, err := gr.ResolveRevision(branch)
693 if err != nil {
694 l.Error("error resolving ref revision", "msg", err.Error())
695 writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest)
696 return
697 }
698
699 sourceCommit, err := gr.ResolveRevision(data.HiddenRef)
700 if err != nil {
701 l.Error("error resolving hidden ref revision", "msg", err.Error())
702 writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest)
703 return
704 }
705
706 status := types.UpToDate
707 if forkCommit.Hash.String() != sourceCommit.Hash.String() {
708 isAncestor, err := forkCommit.IsAncestor(sourceCommit)
709 if err != nil {
710 log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err)
711 return
712 }
713
714 if isAncestor {
715 status = types.FastForwardable
716 } else {
717 status = types.Conflict
718 }
719 }
720
721 w.Header().Set("Content-Type", "application/json")
722 json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status})
723}
724
725func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) {
726 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
727 ref := chi.URLParam(r, "ref")
728 ref, _ = url.PathUnescape(ref)
729
730 l := h.l.With("handler", "RepoLanguages")
731
732 gr, err := git.Open(repoPath, ref)
733 if err != nil {
734 l.Error("opening repo", "error", err.Error())
735 notFound(w)
736 return
737 }
738
739 ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
740 defer cancel()
741
742 sizes, err := gr.AnalyzeLanguages(ctx)
743 if err != nil {
744 l.Error("failed to analyze languages", "error", err.Error())
745 writeError(w, err.Error(), http.StatusNoContent)
746 return
747 }
748
749 resp := types.RepoLanguageResponse{Languages: sizes}
750
751 writeJSON(w, resp)
752}
753
754func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) {
755 l := h.l.With("handler", "RepoForkSync")
756
757 data := struct {
758 Did string `json:"did"`
759 Source string `json:"source"`
760 Name string `json:"name,omitempty"`
761 }{}
762
763 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
764 writeError(w, "invalid request body", http.StatusBadRequest)
765 return
766 }
767
768 did := data.Did
769 source := data.Source
770
771 if did == "" || source == "" {
772 l.Error("invalid request body, empty did or name")
773 w.WriteHeader(http.StatusBadRequest)
774 return
775 }
776
777 var name string
778 if data.Name != "" {
779 name = data.Name
780 } else {
781 name = filepath.Base(source)
782 }
783
784 branch := chi.URLParam(r, "branch")
785 branch, _ = url.PathUnescape(branch)
786
787 relativeRepoPath := filepath.Join(did, name)
788 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
789
790 gr, err := git.PlainOpen(repoPath)
791 if err != nil {
792 log.Println(err)
793 notFound(w)
794 return
795 }
796
797 err = gr.Sync(branch)
798 if err != nil {
799 l.Error("error syncing repo fork", "error", err.Error())
800 writeError(w, err.Error(), http.StatusInternalServerError)
801 return
802 }
803
804 w.WriteHeader(http.StatusNoContent)
805}
806
807func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) {
808 l := h.l.With("handler", "RepoFork")
809
810 data := struct {
811 Did string `json:"did"`
812 Source string `json:"source"`
813 Name string `json:"name,omitempty"`
814 }{}
815
816 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
817 writeError(w, "invalid request body", http.StatusBadRequest)
818 return
819 }
820
821 did := data.Did
822 source := data.Source
823
824 if did == "" || source == "" {
825 l.Error("invalid request body, empty did or name")
826 w.WriteHeader(http.StatusBadRequest)
827 return
828 }
829
830 var name string
831 if data.Name != "" {
832 name = data.Name
833 } else {
834 name = filepath.Base(source)
835 }
836
837 relativeRepoPath := filepath.Join(did, name)
838 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
839
840 err := git.Fork(repoPath, source)
841 if err != nil {
842 l.Error("forking repo", "error", err.Error())
843 writeError(w, err.Error(), http.StatusInternalServerError)
844 return
845 }
846
847 // add perms for this user to access the repo
848 err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath)
849 if err != nil {
850 l.Error("adding repo permissions", "error", err.Error())
851 writeError(w, err.Error(), http.StatusInternalServerError)
852 return
853 }
854
855 hook.SetupRepo(
856 hook.Config(
857 hook.WithScanPath(h.c.Repo.ScanPath),
858 hook.WithInternalApi(h.c.Server.InternalListenAddr),
859 ),
860 repoPath,
861 )
862
863 w.WriteHeader(http.StatusNoContent)
864}
865
866func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
867 l := h.l.With("handler", "RemoveRepo")
868
869 data := struct {
870 Did string `json:"did"`
871 Name string `json:"name"`
872 }{}
873
874 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
875 writeError(w, "invalid request body", http.StatusBadRequest)
876 return
877 }
878
879 did := data.Did
880 name := data.Name
881
882 if did == "" || name == "" {
883 l.Error("invalid request body, empty did or name")
884 w.WriteHeader(http.StatusBadRequest)
885 return
886 }
887
888 relativeRepoPath := filepath.Join(did, name)
889 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
890 err := os.RemoveAll(repoPath)
891 if err != nil {
892 l.Error("removing repo", "error", err.Error())
893 writeError(w, err.Error(), http.StatusInternalServerError)
894 return
895 }
896
897 w.WriteHeader(http.StatusNoContent)
898
899}
900func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
901 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
902
903 data := types.MergeRequest{}
904
905 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
906 writeError(w, err.Error(), http.StatusBadRequest)
907 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err)
908 return
909 }
910
911 mo := &git.MergeOptions{
912 AuthorName: data.AuthorName,
913 AuthorEmail: data.AuthorEmail,
914 CommitBody: data.CommitBody,
915 CommitMessage: data.CommitMessage,
916 }
917
918 patch := data.Patch
919 branch := data.Branch
920 gr, err := git.Open(path, branch)
921 if err != nil {
922 notFound(w)
923 return
924 }
925
926 mo.FormatPatch = patchutil.IsFormatPatch(patch)
927
928 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
929 var mergeErr *git.ErrMerge
930 if errors.As(err, &mergeErr) {
931 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
932 for i, conflict := range mergeErr.Conflicts {
933 conflicts[i] = types.ConflictInfo{
934 Filename: conflict.Filename,
935 Reason: conflict.Reason,
936 }
937 }
938 response := types.MergeCheckResponse{
939 IsConflicted: true,
940 Conflicts: conflicts,
941 Message: mergeErr.Message,
942 }
943 writeConflict(w, response)
944 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr)
945 } else {
946 writeError(w, err.Error(), http.StatusBadRequest)
947 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error())
948 }
949 return
950 }
951
952 w.WriteHeader(http.StatusOK)
953}
954
955func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) {
956 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
957
958 var data struct {
959 Patch string `json:"patch"`
960 Branch string `json:"branch"`
961 }
962
963 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
964 writeError(w, err.Error(), http.StatusBadRequest)
965 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err)
966 return
967 }
968
969 patch := data.Patch
970 branch := data.Branch
971 gr, err := git.Open(path, branch)
972 if err != nil {
973 notFound(w)
974 return
975 }
976
977 err = gr.MergeCheck([]byte(patch), branch)
978 if err == nil {
979 response := types.MergeCheckResponse{
980 IsConflicted: false,
981 }
982 writeJSON(w, response)
983 return
984 }
985
986 var mergeErr *git.ErrMerge
987 if errors.As(err, &mergeErr) {
988 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
989 for i, conflict := range mergeErr.Conflicts {
990 conflicts[i] = types.ConflictInfo{
991 Filename: conflict.Filename,
992 Reason: conflict.Reason,
993 }
994 }
995 response := types.MergeCheckResponse{
996 IsConflicted: true,
997 Conflicts: conflicts,
998 Message: mergeErr.Message,
999 }
1000 writeConflict(w, response)
1001 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error())
1002 return
1003 }
1004 writeError(w, err.Error(), http.StatusInternalServerError)
1005 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
1006}
1007
1008func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) {
1009 rev1 := chi.URLParam(r, "rev1")
1010 rev1, _ = url.PathUnescape(rev1)
1011
1012 rev2 := chi.URLParam(r, "rev2")
1013 rev2, _ = url.PathUnescape(rev2)
1014
1015 l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2)
1016
1017 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1018 gr, err := git.PlainOpen(path)
1019 if err != nil {
1020 notFound(w)
1021 return
1022 }
1023
1024 commit1, err := gr.ResolveRevision(rev1)
1025 if err != nil {
1026 l.Error("error resolving revision 1", "msg", err.Error())
1027 writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest)
1028 return
1029 }
1030
1031 commit2, err := gr.ResolveRevision(rev2)
1032 if err != nil {
1033 l.Error("error resolving revision 2", "msg", err.Error())
1034 writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest)
1035 return
1036 }
1037
1038 rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2)
1039 if err != nil {
1040 l.Error("error comparing revisions", "msg", err.Error())
1041 writeError(w, "error comparing revisions", http.StatusBadRequest)
1042 return
1043 }
1044
1045 writeJSON(w, types.RepoFormatPatchResponse{
1046 Rev1: commit1.Hash.String(),
1047 Rev2: commit2.Hash.String(),
1048 FormatPatch: formatPatch,
1049 Patch: rawPatch,
1050 })
1051 return
1052}
1053
1054func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) {
1055 l := h.l.With("handler", "NewHiddenRef")
1056
1057 forkRef := chi.URLParam(r, "forkRef")
1058 forkRef, _ = url.PathUnescape(forkRef)
1059
1060 remoteRef := chi.URLParam(r, "remoteRef")
1061 remoteRef, _ = url.PathUnescape(remoteRef)
1062
1063 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1064 gr, err := git.PlainOpen(path)
1065 if err != nil {
1066 notFound(w)
1067 return
1068 }
1069
1070 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef)
1071 if err != nil {
1072 l.Error("error tracking hidden remote ref", "msg", err.Error())
1073 writeError(w, "error tracking hidden remote ref", http.StatusBadRequest)
1074 return
1075 }
1076
1077 w.WriteHeader(http.StatusNoContent)
1078 return
1079}
1080
1081func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
1082 l := h.l.With("handler", "AddMember")
1083
1084 data := struct {
1085 Did string `json:"did"`
1086 }{}
1087
1088 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1089 writeError(w, "invalid request body", http.StatusBadRequest)
1090 return
1091 }
1092
1093 did := data.Did
1094
1095 if err := h.db.AddDid(did); err != nil {
1096 l.Error("adding did", "error", err.Error())
1097 writeError(w, err.Error(), http.StatusInternalServerError)
1098 return
1099 }
1100 h.jc.AddDid(did)
1101
1102 if err := h.e.AddKnotMember(rbac.ThisServer, did); err != nil {
1103 l.Error("adding member", "error", err.Error())
1104 writeError(w, err.Error(), http.StatusInternalServerError)
1105 return
1106 }
1107
1108 if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
1109 l.Error("fetching and adding keys", "error", err.Error())
1110 writeError(w, err.Error(), http.StatusInternalServerError)
1111 return
1112 }
1113
1114 w.WriteHeader(http.StatusNoContent)
1115}
1116
1117func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
1118 l := h.l.With("handler", "AddRepoCollaborator")
1119
1120 data := struct {
1121 Did string `json:"did"`
1122 }{}
1123
1124 ownerDid := chi.URLParam(r, "did")
1125 repo := chi.URLParam(r, "name")
1126
1127 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1128 writeError(w, "invalid request body", http.StatusBadRequest)
1129 return
1130 }
1131
1132 if err := h.db.AddDid(data.Did); err != nil {
1133 l.Error("adding did", "error", err.Error())
1134 writeError(w, err.Error(), http.StatusInternalServerError)
1135 return
1136 }
1137 h.jc.AddDid(data.Did)
1138
1139 repoName, _ := securejoin.SecureJoin(ownerDid, repo)
1140 if err := h.e.AddCollaborator(data.Did, rbac.ThisServer, repoName); err != nil {
1141 l.Error("adding repo collaborator", "error", err.Error())
1142 writeError(w, err.Error(), http.StatusInternalServerError)
1143 return
1144 }
1145
1146 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
1147 l.Error("fetching and adding keys", "error", err.Error())
1148 writeError(w, err.Error(), http.StatusInternalServerError)
1149 return
1150 }
1151
1152 w.WriteHeader(http.StatusNoContent)
1153}
1154
1155func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) {
1156 l := h.l.With("handler", "DefaultBranch")
1157 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1158
1159 gr, err := git.Open(path, "")
1160 if err != nil {
1161 notFound(w)
1162 return
1163 }
1164
1165 branch, err := gr.FindMainBranch()
1166 if err != nil {
1167 writeError(w, err.Error(), http.StatusInternalServerError)
1168 l.Error("getting default branch", "error", err.Error())
1169 return
1170 }
1171
1172 writeJSON(w, types.RepoDefaultBranchResponse{
1173 Branch: branch,
1174 })
1175}
1176
1177func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
1178 l := h.l.With("handler", "SetDefaultBranch")
1179 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1180
1181 data := struct {
1182 Branch string `json:"branch"`
1183 }{}
1184
1185 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1186 writeError(w, err.Error(), http.StatusBadRequest)
1187 return
1188 }
1189
1190 gr, err := git.PlainOpen(path)
1191 if err != nil {
1192 notFound(w)
1193 return
1194 }
1195
1196 err = gr.SetDefaultBranch(data.Branch)
1197 if err != nil {
1198 writeError(w, err.Error(), http.StatusInternalServerError)
1199 l.Error("setting default branch", "error", err.Error())
1200 return
1201 }
1202
1203 w.WriteHeader(http.StatusNoContent)
1204}
1205
1206func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
1207 w.Write([]byte("ok"))
1208}
1209
1210func validateRepoName(name string) error {
1211 // check for path traversal attempts
1212 if name == "." || name == ".." ||
1213 strings.Contains(name, "/") || strings.Contains(name, "\\") {
1214 return fmt.Errorf("Repository name contains invalid path characters")
1215 }
1216
1217 // check for sequences that could be used for traversal when normalized
1218 if strings.Contains(name, "./") || strings.Contains(name, "../") ||
1219 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
1220 return fmt.Errorf("Repository name contains invalid path sequence")
1221 }
1222
1223 // then continue with character validation
1224 for _, char := range name {
1225 if !((char >= 'a' && char <= 'z') ||
1226 (char >= 'A' && char <= 'Z') ||
1227 (char >= '0' && char <= '9') ||
1228 char == '-' || char == '_' || char == '.') {
1229 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
1230 }
1231 }
1232
1233 // additional check to prevent multiple sequential dots
1234 if strings.Contains(name, "..") {
1235 return fmt.Errorf("Repository name cannot contain sequential dots")
1236 }
1237
1238 // if all checks pass
1239 return nil
1240}