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