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