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