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 contentHash := sha256.Sum256(contents)
287 eTag := fmt.Sprintf("\"%x\"", contentHash)
288
289 // allow image, video, and text/plain files to be served directly
290 switch {
291 case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"):
292 if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag {
293 w.WriteHeader(http.StatusNotModified)
294 return
295 }
296 w.Header().Set("ETag", eTag)
297
298 case strings.HasPrefix(mimeType, "text/plain"):
299 w.Header().Set("Cache-Control", "public, no-cache")
300
301 default:
302 l.Error("attempted to serve disallowed file type", "mimetype", mimeType)
303 writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
304 return
305 }
306
307 w.Header().Set("Content-Type", mimeType)
308 w.Write(contents)
309}
310
311func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) {
312 treePath := chi.URLParam(r, "*")
313 ref := chi.URLParam(r, "ref")
314 ref, _ = url.PathUnescape(ref)
315
316 l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath)
317
318 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
319 gr, err := git.Open(path, ref)
320 if err != nil {
321 notFound(w)
322 return
323 }
324
325 var isBinaryFile bool = false
326 contents, err := gr.FileContent(treePath)
327 if errors.Is(err, git.ErrBinaryFile) {
328 isBinaryFile = true
329 } else if errors.Is(err, object.ErrFileNotFound) {
330 notFound(w)
331 return
332 } else if err != nil {
333 writeError(w, err.Error(), http.StatusInternalServerError)
334 return
335 }
336
337 bytes := []byte(contents)
338 // safe := string(sanitize(bytes))
339 sizeHint := len(bytes)
340
341 resp := types.RepoBlobResponse{
342 Ref: ref,
343 Contents: string(bytes),
344 Path: treePath,
345 IsBinary: isBinaryFile,
346 SizeHint: uint64(sizeHint),
347 }
348
349 h.showFile(resp, w, l)
350}
351
352func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
353 name := chi.URLParam(r, "name")
354 file := chi.URLParam(r, "file")
355
356 l := h.l.With("handler", "Archive", "name", name, "file", file)
357
358 // TODO: extend this to add more files compression (e.g.: xz)
359 if !strings.HasSuffix(file, ".tar.gz") {
360 notFound(w)
361 return
362 }
363
364 ref := strings.TrimSuffix(file, ".tar.gz")
365
366 unescapedRef, err := url.PathUnescape(ref)
367 if err != nil {
368 notFound(w)
369 return
370 }
371
372 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-")
373
374 // This allows the browser to use a proper name for the file when
375 // downloading
376 filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename)
377 setContentDisposition(w, filename)
378 setGZipMIME(w)
379
380 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
381 gr, err := git.Open(path, unescapedRef)
382 if err != nil {
383 notFound(w)
384 return
385 }
386
387 gw := gzip.NewWriter(w)
388 defer gw.Close()
389
390 prefix := fmt.Sprintf("%s-%s", name, safeRefFilename)
391 err = gr.WriteTar(gw, prefix)
392 if err != nil {
393 // once we start writing to the body we can't report error anymore
394 // so we are only left with printing the error.
395 l.Error("writing tar file", "error", err.Error())
396 return
397 }
398
399 err = gw.Flush()
400 if err != nil {
401 // once we start writing to the body we can't report error anymore
402 // so we are only left with printing the error.
403 l.Error("flushing?", "error", err.Error())
404 return
405 }
406}
407
408func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
409 ref := chi.URLParam(r, "ref")
410 ref, _ = url.PathUnescape(ref)
411
412 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
413
414 l := h.l.With("handler", "Log", "ref", ref, "path", path)
415
416 gr, err := git.Open(path, ref)
417 if err != nil {
418 notFound(w)
419 return
420 }
421
422 // Get page parameters
423 page := 1
424 pageSize := 30
425
426 if pageParam := r.URL.Query().Get("page"); pageParam != "" {
427 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
428 page = p
429 }
430 }
431
432 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
433 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
434 pageSize = ps
435 }
436 }
437
438 // convert to offset/limit
439 offset := (page - 1) * pageSize
440 limit := pageSize
441
442 commits, err := gr.Commits(offset, limit)
443 if err != nil {
444 writeError(w, err.Error(), http.StatusInternalServerError)
445 l.Error("fetching commits", "error", err.Error())
446 return
447 }
448
449 total := len(commits)
450
451 resp := types.RepoLogResponse{
452 Commits: commits,
453 Ref: ref,
454 Description: getDescription(path),
455 Log: true,
456 Total: total,
457 Page: page,
458 PerPage: pageSize,
459 }
460
461 writeJSON(w, resp)
462 return
463}
464
465func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
466 ref := chi.URLParam(r, "ref")
467 ref, _ = url.PathUnescape(ref)
468
469 l := h.l.With("handler", "Diff", "ref", ref)
470
471 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
472 gr, err := git.Open(path, ref)
473 if err != nil {
474 notFound(w)
475 return
476 }
477
478 diff, err := gr.Diff()
479 if err != nil {
480 writeError(w, err.Error(), http.StatusInternalServerError)
481 l.Error("getting diff", "error", err.Error())
482 return
483 }
484
485 resp := types.RepoCommitResponse{
486 Ref: ref,
487 Diff: diff,
488 }
489
490 writeJSON(w, resp)
491 return
492}
493
494func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
495 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
496 l := h.l.With("handler", "Refs")
497
498 gr, err := git.Open(path, "")
499 if err != nil {
500 notFound(w)
501 return
502 }
503
504 tags, err := gr.Tags()
505 if err != nil {
506 // Non-fatal, we *should* have at least one branch to show.
507 l.Warn("getting tags", "error", err.Error())
508 }
509
510 rtags := []*types.TagReference{}
511 for _, tag := range tags {
512 var target *object.Tag
513 if tag.Target != plumbing.ZeroHash {
514 target = &tag
515 }
516 tr := types.TagReference{
517 Tag: target,
518 }
519
520 tr.Reference = types.Reference{
521 Name: tag.Name,
522 Hash: tag.Hash.String(),
523 }
524
525 if tag.Message != "" {
526 tr.Message = tag.Message
527 }
528
529 rtags = append(rtags, &tr)
530 }
531
532 resp := types.RepoTagsResponse{
533 Tags: rtags,
534 }
535
536 writeJSON(w, resp)
537 return
538}
539
540func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
541 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
542
543 gr, err := git.PlainOpen(path)
544 if err != nil {
545 notFound(w)
546 return
547 }
548
549 branches, _ := gr.Branches()
550
551 resp := types.RepoBranchesResponse{
552 Branches: branches,
553 }
554
555 writeJSON(w, resp)
556 return
557}
558
559func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) {
560 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
561 branchName := chi.URLParam(r, "branch")
562 branchName, _ = url.PathUnescape(branchName)
563
564 l := h.l.With("handler", "Branch")
565
566 gr, err := git.PlainOpen(path)
567 if err != nil {
568 notFound(w)
569 return
570 }
571
572 ref, err := gr.Branch(branchName)
573 if err != nil {
574 l.Error("getting branch", "error", err.Error())
575 writeError(w, err.Error(), http.StatusInternalServerError)
576 return
577 }
578
579 commit, err := gr.Commit(ref.Hash())
580 if err != nil {
581 l.Error("getting commit object", "error", err.Error())
582 writeError(w, err.Error(), http.StatusInternalServerError)
583 return
584 }
585
586 defaultBranch, err := gr.FindMainBranch()
587 isDefault := false
588 if err != nil {
589 l.Error("getting default branch", "error", err.Error())
590 // do not quit though
591 } else if defaultBranch == branchName {
592 isDefault = true
593 }
594
595 resp := types.RepoBranchResponse{
596 Branch: types.Branch{
597 Reference: types.Reference{
598 Name: ref.Name().Short(),
599 Hash: ref.Hash().String(),
600 },
601 Commit: commit,
602 IsDefault: isDefault,
603 },
604 }
605
606 writeJSON(w, resp)
607 return
608}
609
610func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
611 l := h.l.With("handler", "Keys")
612
613 switch r.Method {
614 case http.MethodGet:
615 keys, err := h.db.GetAllPublicKeys()
616 if err != nil {
617 writeError(w, err.Error(), http.StatusInternalServerError)
618 l.Error("getting public keys", "error", err.Error())
619 return
620 }
621
622 data := make([]map[string]any, 0)
623 for _, key := range keys {
624 j := key.JSON()
625 data = append(data, j)
626 }
627 writeJSON(w, data)
628 return
629
630 case http.MethodPut:
631 pk := db.PublicKey{}
632 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
633 writeError(w, "invalid request body", http.StatusBadRequest)
634 return
635 }
636
637 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
638 if err != nil {
639 writeError(w, "invalid pubkey", http.StatusBadRequest)
640 }
641
642 if err := h.db.AddPublicKey(pk); err != nil {
643 writeError(w, err.Error(), http.StatusInternalServerError)
644 l.Error("adding public key", "error", err.Error())
645 return
646 }
647
648 w.WriteHeader(http.StatusNoContent)
649 return
650 }
651}
652
653func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) {
654 l := h.l.With("handler", "RepoForkAheadBehind")
655
656 data := struct {
657 Did string `json:"did"`
658 Source string `json:"source"`
659 Name string `json:"name,omitempty"`
660 HiddenRef string `json:"hiddenref"`
661 }{}
662
663 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
664 writeError(w, "invalid request body", http.StatusBadRequest)
665 return
666 }
667
668 did := data.Did
669 source := data.Source
670
671 if did == "" || source == "" {
672 l.Error("invalid request body, empty did or name")
673 w.WriteHeader(http.StatusBadRequest)
674 return
675 }
676
677 var name string
678 if data.Name != "" {
679 name = data.Name
680 } else {
681 name = filepath.Base(source)
682 }
683
684 branch := chi.URLParam(r, "branch")
685 branch, _ = url.PathUnescape(branch)
686
687 relativeRepoPath := filepath.Join(did, name)
688 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
689
690 gr, err := git.PlainOpen(repoPath)
691 if err != nil {
692 log.Println(err)
693 notFound(w)
694 return
695 }
696
697 forkCommit, err := gr.ResolveRevision(branch)
698 if err != nil {
699 l.Error("error resolving ref revision", "msg", err.Error())
700 writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest)
701 return
702 }
703
704 sourceCommit, err := gr.ResolveRevision(data.HiddenRef)
705 if err != nil {
706 l.Error("error resolving hidden ref revision", "msg", err.Error())
707 writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest)
708 return
709 }
710
711 status := types.UpToDate
712 if forkCommit.Hash.String() != sourceCommit.Hash.String() {
713 isAncestor, err := forkCommit.IsAncestor(sourceCommit)
714 if err != nil {
715 log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err)
716 return
717 }
718
719 if isAncestor {
720 status = types.FastForwardable
721 } else {
722 status = types.Conflict
723 }
724 }
725
726 w.Header().Set("Content-Type", "application/json")
727 json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status})
728}
729
730func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) {
731 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
732 ref := chi.URLParam(r, "ref")
733 ref, _ = url.PathUnescape(ref)
734
735 l := h.l.With("handler", "RepoLanguages")
736
737 gr, err := git.Open(repoPath, ref)
738 if err != nil {
739 l.Error("opening repo", "error", err.Error())
740 notFound(w)
741 return
742 }
743
744 ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
745 defer cancel()
746
747 sizes, err := gr.AnalyzeLanguages(ctx)
748 if err != nil {
749 l.Error("failed to analyze languages", "error", err.Error())
750 writeError(w, err.Error(), http.StatusNoContent)
751 return
752 }
753
754 resp := types.RepoLanguageResponse{Languages: sizes}
755
756 writeJSON(w, resp)
757}
758
759func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) {
760 l := h.l.With("handler", "RepoForkSync")
761
762 data := struct {
763 Did string `json:"did"`
764 Source string `json:"source"`
765 Name string `json:"name,omitempty"`
766 }{}
767
768 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
769 writeError(w, "invalid request body", http.StatusBadRequest)
770 return
771 }
772
773 did := data.Did
774 source := data.Source
775
776 if did == "" || source == "" {
777 l.Error("invalid request body, empty did or name")
778 w.WriteHeader(http.StatusBadRequest)
779 return
780 }
781
782 var name string
783 if data.Name != "" {
784 name = data.Name
785 } else {
786 name = filepath.Base(source)
787 }
788
789 branch := chi.URLParam(r, "*")
790 branch, _ = url.PathUnescape(branch)
791
792 relativeRepoPath := filepath.Join(did, name)
793 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
794
795 gr, err := git.Open(repoPath, branch)
796 if err != nil {
797 log.Println(err)
798 notFound(w)
799 return
800 }
801
802 err = gr.Sync()
803 if err != nil {
804 l.Error("error syncing repo fork", "error", err.Error())
805 writeError(w, err.Error(), http.StatusInternalServerError)
806 return
807 }
808
809 w.WriteHeader(http.StatusNoContent)
810}
811
812func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) {
813 l := h.l.With("handler", "RepoFork")
814
815 data := struct {
816 Did string `json:"did"`
817 Source string `json:"source"`
818 Name string `json:"name,omitempty"`
819 }{}
820
821 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
822 writeError(w, "invalid request body", http.StatusBadRequest)
823 return
824 }
825
826 did := data.Did
827 source := data.Source
828
829 if did == "" || source == "" {
830 l.Error("invalid request body, empty did or name")
831 w.WriteHeader(http.StatusBadRequest)
832 return
833 }
834
835 var name string
836 if data.Name != "" {
837 name = data.Name
838 } else {
839 name = filepath.Base(source)
840 }
841
842 relativeRepoPath := filepath.Join(did, name)
843 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
844
845 err := git.Fork(repoPath, source)
846 if err != nil {
847 l.Error("forking repo", "error", err.Error())
848 writeError(w, err.Error(), http.StatusInternalServerError)
849 return
850 }
851
852 // add perms for this user to access the repo
853 err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath)
854 if err != nil {
855 l.Error("adding repo permissions", "error", err.Error())
856 writeError(w, err.Error(), http.StatusInternalServerError)
857 return
858 }
859
860 hook.SetupRepo(
861 hook.Config(
862 hook.WithScanPath(h.c.Repo.ScanPath),
863 hook.WithInternalApi(h.c.Server.InternalListenAddr),
864 ),
865 repoPath,
866 )
867
868 w.WriteHeader(http.StatusNoContent)
869}
870
871func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
872 l := h.l.With("handler", "RemoveRepo")
873
874 data := struct {
875 Did string `json:"did"`
876 Name string `json:"name"`
877 }{}
878
879 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
880 writeError(w, "invalid request body", http.StatusBadRequest)
881 return
882 }
883
884 did := data.Did
885 name := data.Name
886
887 if did == "" || name == "" {
888 l.Error("invalid request body, empty did or name")
889 w.WriteHeader(http.StatusBadRequest)
890 return
891 }
892
893 relativeRepoPath := filepath.Join(did, name)
894 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
895 err := os.RemoveAll(repoPath)
896 if err != nil {
897 l.Error("removing repo", "error", err.Error())
898 writeError(w, err.Error(), http.StatusInternalServerError)
899 return
900 }
901
902 w.WriteHeader(http.StatusNoContent)
903
904}
905func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
906 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
907
908 data := types.MergeRequest{}
909
910 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
911 writeError(w, err.Error(), http.StatusBadRequest)
912 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err)
913 return
914 }
915
916 mo := &git.MergeOptions{
917 AuthorName: data.AuthorName,
918 AuthorEmail: data.AuthorEmail,
919 CommitBody: data.CommitBody,
920 CommitMessage: data.CommitMessage,
921 }
922
923 patch := data.Patch
924 branch := data.Branch
925 gr, err := git.Open(path, branch)
926 if err != nil {
927 notFound(w)
928 return
929 }
930
931 mo.FormatPatch = patchutil.IsFormatPatch(patch)
932
933 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
934 var mergeErr *git.ErrMerge
935 if errors.As(err, &mergeErr) {
936 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
937 for i, conflict := range mergeErr.Conflicts {
938 conflicts[i] = types.ConflictInfo{
939 Filename: conflict.Filename,
940 Reason: conflict.Reason,
941 }
942 }
943 response := types.MergeCheckResponse{
944 IsConflicted: true,
945 Conflicts: conflicts,
946 Message: mergeErr.Message,
947 }
948 writeConflict(w, response)
949 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr)
950 } else {
951 writeError(w, err.Error(), http.StatusBadRequest)
952 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error())
953 }
954 return
955 }
956
957 w.WriteHeader(http.StatusOK)
958}
959
960func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) {
961 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
962
963 var data struct {
964 Patch string `json:"patch"`
965 Branch string `json:"branch"`
966 }
967
968 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
969 writeError(w, err.Error(), http.StatusBadRequest)
970 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err)
971 return
972 }
973
974 patch := data.Patch
975 branch := data.Branch
976 gr, err := git.Open(path, branch)
977 if err != nil {
978 notFound(w)
979 return
980 }
981
982 err = gr.MergeCheck([]byte(patch), branch)
983 if err == nil {
984 response := types.MergeCheckResponse{
985 IsConflicted: false,
986 }
987 writeJSON(w, response)
988 return
989 }
990
991 var mergeErr *git.ErrMerge
992 if errors.As(err, &mergeErr) {
993 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
994 for i, conflict := range mergeErr.Conflicts {
995 conflicts[i] = types.ConflictInfo{
996 Filename: conflict.Filename,
997 Reason: conflict.Reason,
998 }
999 }
1000 response := types.MergeCheckResponse{
1001 IsConflicted: true,
1002 Conflicts: conflicts,
1003 Message: mergeErr.Message,
1004 }
1005 writeConflict(w, response)
1006 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error())
1007 return
1008 }
1009 writeError(w, err.Error(), http.StatusInternalServerError)
1010 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
1011}
1012
1013func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) {
1014 rev1 := chi.URLParam(r, "rev1")
1015 rev1, _ = url.PathUnescape(rev1)
1016
1017 rev2 := chi.URLParam(r, "rev2")
1018 rev2, _ = url.PathUnescape(rev2)
1019
1020 l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2)
1021
1022 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1023 gr, err := git.PlainOpen(path)
1024 if err != nil {
1025 notFound(w)
1026 return
1027 }
1028
1029 commit1, err := gr.ResolveRevision(rev1)
1030 if err != nil {
1031 l.Error("error resolving revision 1", "msg", err.Error())
1032 writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest)
1033 return
1034 }
1035
1036 commit2, err := gr.ResolveRevision(rev2)
1037 if err != nil {
1038 l.Error("error resolving revision 2", "msg", err.Error())
1039 writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest)
1040 return
1041 }
1042
1043 rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2)
1044 if err != nil {
1045 l.Error("error comparing revisions", "msg", err.Error())
1046 writeError(w, "error comparing revisions", http.StatusBadRequest)
1047 return
1048 }
1049
1050 writeJSON(w, types.RepoFormatPatchResponse{
1051 Rev1: commit1.Hash.String(),
1052 Rev2: commit2.Hash.String(),
1053 FormatPatch: formatPatch,
1054 Patch: rawPatch,
1055 })
1056 return
1057}
1058
1059func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) {
1060 l := h.l.With("handler", "NewHiddenRef")
1061
1062 forkRef := chi.URLParam(r, "forkRef")
1063 forkRef, _ = url.PathUnescape(forkRef)
1064
1065 remoteRef := chi.URLParam(r, "remoteRef")
1066 remoteRef, _ = url.PathUnescape(remoteRef)
1067
1068 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1069 gr, err := git.PlainOpen(path)
1070 if err != nil {
1071 notFound(w)
1072 return
1073 }
1074
1075 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef)
1076 if err != nil {
1077 l.Error("error tracking hidden remote ref", "msg", err.Error())
1078 writeError(w, "error tracking hidden remote ref", http.StatusBadRequest)
1079 return
1080 }
1081
1082 w.WriteHeader(http.StatusNoContent)
1083 return
1084}
1085
1086func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
1087 l := h.l.With("handler", "AddMember")
1088
1089 data := struct {
1090 Did string `json:"did"`
1091 }{}
1092
1093 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1094 writeError(w, "invalid request body", http.StatusBadRequest)
1095 return
1096 }
1097
1098 did := data.Did
1099
1100 if err := h.db.AddDid(did); err != nil {
1101 l.Error("adding did", "error", err.Error())
1102 writeError(w, err.Error(), http.StatusInternalServerError)
1103 return
1104 }
1105 h.jc.AddDid(did)
1106
1107 if err := h.e.AddKnotMember(rbac.ThisServer, did); err != nil {
1108 l.Error("adding member", "error", err.Error())
1109 writeError(w, err.Error(), http.StatusInternalServerError)
1110 return
1111 }
1112
1113 if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
1114 l.Error("fetching and adding keys", "error", err.Error())
1115 writeError(w, err.Error(), http.StatusInternalServerError)
1116 return
1117 }
1118
1119 w.WriteHeader(http.StatusNoContent)
1120}
1121
1122func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
1123 l := h.l.With("handler", "AddRepoCollaborator")
1124
1125 data := struct {
1126 Did string `json:"did"`
1127 }{}
1128
1129 ownerDid := chi.URLParam(r, "did")
1130 repo := chi.URLParam(r, "name")
1131
1132 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1133 writeError(w, "invalid request body", http.StatusBadRequest)
1134 return
1135 }
1136
1137 if err := h.db.AddDid(data.Did); err != nil {
1138 l.Error("adding did", "error", err.Error())
1139 writeError(w, err.Error(), http.StatusInternalServerError)
1140 return
1141 }
1142 h.jc.AddDid(data.Did)
1143
1144 repoName, _ := securejoin.SecureJoin(ownerDid, repo)
1145 if err := h.e.AddCollaborator(data.Did, rbac.ThisServer, repoName); err != nil {
1146 l.Error("adding repo collaborator", "error", err.Error())
1147 writeError(w, err.Error(), http.StatusInternalServerError)
1148 return
1149 }
1150
1151 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
1152 l.Error("fetching and adding keys", "error", err.Error())
1153 writeError(w, err.Error(), http.StatusInternalServerError)
1154 return
1155 }
1156
1157 w.WriteHeader(http.StatusNoContent)
1158}
1159
1160func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) {
1161 l := h.l.With("handler", "DefaultBranch")
1162 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1163
1164 gr, err := git.Open(path, "")
1165 if err != nil {
1166 notFound(w)
1167 return
1168 }
1169
1170 branch, err := gr.FindMainBranch()
1171 if err != nil {
1172 writeError(w, err.Error(), http.StatusInternalServerError)
1173 l.Error("getting default branch", "error", err.Error())
1174 return
1175 }
1176
1177 writeJSON(w, types.RepoDefaultBranchResponse{
1178 Branch: branch,
1179 })
1180}
1181
1182func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
1183 l := h.l.With("handler", "SetDefaultBranch")
1184 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1185
1186 data := struct {
1187 Branch string `json:"branch"`
1188 }{}
1189
1190 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1191 writeError(w, err.Error(), http.StatusBadRequest)
1192 return
1193 }
1194
1195 gr, err := git.PlainOpen(path)
1196 if err != nil {
1197 notFound(w)
1198 return
1199 }
1200
1201 err = gr.SetDefaultBranch(data.Branch)
1202 if err != nil {
1203 writeError(w, err.Error(), http.StatusInternalServerError)
1204 l.Error("setting default branch", "error", err.Error())
1205 return
1206 }
1207
1208 w.WriteHeader(http.StatusNoContent)
1209}
1210
1211func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
1212 w.Write([]byte("ok"))
1213}
1214
1215func validateRepoName(name string) error {
1216 // check for path traversal attempts
1217 if name == "." || name == ".." ||
1218 strings.Contains(name, "/") || strings.Contains(name, "\\") {
1219 return fmt.Errorf("Repository name contains invalid path characters")
1220 }
1221
1222 // check for sequences that could be used for traversal when normalized
1223 if strings.Contains(name, "./") || strings.Contains(name, "../") ||
1224 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
1225 return fmt.Errorf("Repository name contains invalid path sequence")
1226 }
1227
1228 // then continue with character validation
1229 for _, char := range name {
1230 if !((char >= 'a' && char <= 'z') ||
1231 (char >= 'A' && char <= 'Z') ||
1232 (char >= '0' && char <= '9') ||
1233 char == '-' || char == '_' || char == '.') {
1234 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
1235 }
1236 }
1237
1238 // additional check to prevent multiple sequential dots
1239 if strings.Contains(name, "..") {
1240 return fmt.Errorf("Repository name cannot contain sequential dots")
1241 }
1242
1243 // if all checks pass
1244 return nil
1245}