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