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