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