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