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 err = hook.SetupRepo(hook.Config(
678 hook.WithScanPath(h.c.Repo.ScanPath),
679 hook.WithInternalApi(h.c.Server.InternalListenAddr),
680 ),
681 repoPath,
682 )
683 if err != nil {
684 l.Error("setting up hooks", "error", err.Error())
685 writeError(w, err.Error(), http.StatusInternalServerError)
686 return
687 }
688
689 w.WriteHeader(http.StatusNoContent)
690}
691
692func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) {
693 l := h.l.With("handler", "RepoForkSync")
694
695 data := struct {
696 Did string `json:"did"`
697 Source string `json:"source"`
698 Name string `json:"name,omitempty"`
699 HiddenRef string `json:"hiddenref"`
700 }{}
701
702 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
703 writeError(w, "invalid request body", http.StatusBadRequest)
704 return
705 }
706
707 did := data.Did
708 source := data.Source
709
710 if did == "" || source == "" {
711 l.Error("invalid request body, empty did or name")
712 w.WriteHeader(http.StatusBadRequest)
713 return
714 }
715
716 var name string
717 if data.Name != "" {
718 name = data.Name
719 } else {
720 name = filepath.Base(source)
721 }
722
723 branch := chi.URLParam(r, "branch")
724 branch, _ = url.PathUnescape(branch)
725
726 relativeRepoPath := filepath.Join(did, name)
727 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
728
729 gr, err := git.PlainOpen(repoPath)
730 if err != nil {
731 log.Println(err)
732 notFound(w)
733 return
734 }
735
736 forkCommit, err := gr.ResolveRevision(branch)
737 if err != nil {
738 l.Error("error resolving ref revision", "msg", err.Error())
739 writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest)
740 return
741 }
742
743 sourceCommit, err := gr.ResolveRevision(data.HiddenRef)
744 if err != nil {
745 l.Error("error resolving hidden ref revision", "msg", err.Error())
746 writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest)
747 return
748 }
749
750 status := types.UpToDate
751 if forkCommit.Hash.String() != sourceCommit.Hash.String() {
752 isAncestor, err := forkCommit.IsAncestor(sourceCommit)
753 if err != nil {
754 log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err)
755 return
756 }
757
758 if isAncestor {
759 status = types.FastForwardable
760 } else {
761 status = types.Conflict
762 }
763 }
764
765 w.Header().Set("Content-Type", "application/json")
766 json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status})
767}
768
769func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) {
770 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
771 ref := chi.URLParam(r, "ref")
772 ref, _ = url.PathUnescape(ref)
773
774 l := h.l.With("handler", "RepoLanguages")
775
776 gr, err := git.Open(path, ref)
777 if err != nil {
778 l.Error("opening repo", "error", err.Error())
779 notFound(w)
780 return
781 }
782
783 languageFileCount := make(map[string]int)
784
785 err = recurseEntireTree(r.Context(), gr, func(absPath string) {
786 lang, safe := enry.GetLanguageByExtension(absPath)
787 if len(lang) == 0 || !safe {
788 content, _ := gr.FileContentN(absPath, 1024)
789 if !safe {
790 lang = enry.GetLanguage(absPath, content)
791 } else {
792 lang, _ = enry.GetLanguageByContent(absPath, content)
793 if len(lang) == 0 {
794 return
795 }
796 }
797 }
798
799 v, ok := languageFileCount[lang]
800 if ok {
801 languageFileCount[lang] = v + 1
802 } else {
803 languageFileCount[lang] = 1
804 }
805 }, "")
806 if err != nil {
807 l.Error("failed to recurse file tree", "error", err.Error())
808 writeError(w, err.Error(), http.StatusNoContent)
809 return
810 }
811
812 resp := types.RepoLanguageResponse{Languages: languageFileCount}
813
814 writeJSON(w, resp)
815 return
816}
817
818func recurseEntireTree(ctx context.Context, git *git.GitRepo, callback func(absPath string), filePath string) error {
819 files, err := git.FileTree(ctx, filePath)
820 if err != nil {
821 log.Println(err)
822 return err
823 }
824
825 for _, file := range files {
826 absPath := path.Join(filePath, file.Name)
827 if !file.IsFile {
828 return recurseEntireTree(ctx, git, callback, absPath)
829 }
830 callback(absPath)
831 }
832
833 return nil
834}
835
836func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) {
837 l := h.l.With("handler", "RepoForkSync")
838
839 data := struct {
840 Did string `json:"did"`
841 Source string `json:"source"`
842 Name string `json:"name,omitempty"`
843 }{}
844
845 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
846 writeError(w, "invalid request body", http.StatusBadRequest)
847 return
848 }
849
850 did := data.Did
851 source := data.Source
852
853 if did == "" || source == "" {
854 l.Error("invalid request body, empty did or name")
855 w.WriteHeader(http.StatusBadRequest)
856 return
857 }
858
859 var name string
860 if data.Name != "" {
861 name = data.Name
862 } else {
863 name = filepath.Base(source)
864 }
865
866 branch := chi.URLParam(r, "branch")
867 branch, _ = url.PathUnescape(branch)
868
869 relativeRepoPath := filepath.Join(did, name)
870 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
871
872 gr, err := git.PlainOpen(repoPath)
873 if err != nil {
874 log.Println(err)
875 notFound(w)
876 return
877 }
878
879 err = gr.Sync(branch)
880 if err != nil {
881 l.Error("error syncing repo fork", "error", err.Error())
882 writeError(w, err.Error(), http.StatusInternalServerError)
883 return
884 }
885
886 w.WriteHeader(http.StatusNoContent)
887}
888
889func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) {
890 l := h.l.With("handler", "RepoFork")
891
892 data := struct {
893 Did string `json:"did"`
894 Source string `json:"source"`
895 Name string `json:"name,omitempty"`
896 }{}
897
898 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
899 writeError(w, "invalid request body", http.StatusBadRequest)
900 return
901 }
902
903 did := data.Did
904 source := data.Source
905
906 if did == "" || source == "" {
907 l.Error("invalid request body, empty did or name")
908 w.WriteHeader(http.StatusBadRequest)
909 return
910 }
911
912 var name string
913 if data.Name != "" {
914 name = data.Name
915 } else {
916 name = filepath.Base(source)
917 }
918
919 relativeRepoPath := filepath.Join(did, name)
920 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
921
922 err := git.Fork(repoPath, source)
923 if err != nil {
924 l.Error("forking repo", "error", err.Error())
925 writeError(w, err.Error(), http.StatusInternalServerError)
926 return
927 }
928
929 // add perms for this user to access the repo
930 err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
931 if err != nil {
932 l.Error("adding repo permissions", "error", err.Error())
933 writeError(w, err.Error(), http.StatusInternalServerError)
934 return
935 }
936
937 w.WriteHeader(http.StatusNoContent)
938}
939
940func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
941 l := h.l.With("handler", "RemoveRepo")
942
943 data := struct {
944 Did string `json:"did"`
945 Name string `json:"name"`
946 }{}
947
948 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
949 writeError(w, "invalid request body", http.StatusBadRequest)
950 return
951 }
952
953 did := data.Did
954 name := data.Name
955
956 if did == "" || name == "" {
957 l.Error("invalid request body, empty did or name")
958 w.WriteHeader(http.StatusBadRequest)
959 return
960 }
961
962 relativeRepoPath := filepath.Join(did, name)
963 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
964 err := os.RemoveAll(repoPath)
965 if err != nil {
966 l.Error("removing repo", "error", err.Error())
967 writeError(w, err.Error(), http.StatusInternalServerError)
968 return
969 }
970
971 w.WriteHeader(http.StatusNoContent)
972
973}
974func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
975 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
976
977 data := types.MergeRequest{}
978
979 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
980 writeError(w, err.Error(), http.StatusBadRequest)
981 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err)
982 return
983 }
984
985 mo := &git.MergeOptions{
986 AuthorName: data.AuthorName,
987 AuthorEmail: data.AuthorEmail,
988 CommitBody: data.CommitBody,
989 CommitMessage: data.CommitMessage,
990 }
991
992 patch := data.Patch
993 branch := data.Branch
994 gr, err := git.Open(path, branch)
995 if err != nil {
996 notFound(w)
997 return
998 }
999
1000 mo.FormatPatch = patchutil.IsFormatPatch(patch)
1001
1002 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
1003 var mergeErr *git.ErrMerge
1004 if errors.As(err, &mergeErr) {
1005 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
1006 for i, conflict := range mergeErr.Conflicts {
1007 conflicts[i] = types.ConflictInfo{
1008 Filename: conflict.Filename,
1009 Reason: conflict.Reason,
1010 }
1011 }
1012 response := types.MergeCheckResponse{
1013 IsConflicted: true,
1014 Conflicts: conflicts,
1015 Message: mergeErr.Message,
1016 }
1017 writeConflict(w, response)
1018 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr)
1019 } else {
1020 writeError(w, err.Error(), http.StatusBadRequest)
1021 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error())
1022 }
1023 return
1024 }
1025
1026 w.WriteHeader(http.StatusOK)
1027}
1028
1029func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) {
1030 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1031
1032 var data struct {
1033 Patch string `json:"patch"`
1034 Branch string `json:"branch"`
1035 }
1036
1037 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1038 writeError(w, err.Error(), http.StatusBadRequest)
1039 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err)
1040 return
1041 }
1042
1043 patch := data.Patch
1044 branch := data.Branch
1045 gr, err := git.Open(path, branch)
1046 if err != nil {
1047 notFound(w)
1048 return
1049 }
1050
1051 err = gr.MergeCheck([]byte(patch), branch)
1052 if err == nil {
1053 response := types.MergeCheckResponse{
1054 IsConflicted: false,
1055 }
1056 writeJSON(w, response)
1057 return
1058 }
1059
1060 var mergeErr *git.ErrMerge
1061 if errors.As(err, &mergeErr) {
1062 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
1063 for i, conflict := range mergeErr.Conflicts {
1064 conflicts[i] = types.ConflictInfo{
1065 Filename: conflict.Filename,
1066 Reason: conflict.Reason,
1067 }
1068 }
1069 response := types.MergeCheckResponse{
1070 IsConflicted: true,
1071 Conflicts: conflicts,
1072 Message: mergeErr.Message,
1073 }
1074 writeConflict(w, response)
1075 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error())
1076 return
1077 }
1078 writeError(w, err.Error(), http.StatusInternalServerError)
1079 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
1080}
1081
1082func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) {
1083 rev1 := chi.URLParam(r, "rev1")
1084 rev1, _ = url.PathUnescape(rev1)
1085
1086 rev2 := chi.URLParam(r, "rev2")
1087 rev2, _ = url.PathUnescape(rev2)
1088
1089 l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2)
1090
1091 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1092 gr, err := git.PlainOpen(path)
1093 if err != nil {
1094 notFound(w)
1095 return
1096 }
1097
1098 commit1, err := gr.ResolveRevision(rev1)
1099 if err != nil {
1100 l.Error("error resolving revision 1", "msg", err.Error())
1101 writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest)
1102 return
1103 }
1104
1105 commit2, err := gr.ResolveRevision(rev2)
1106 if err != nil {
1107 l.Error("error resolving revision 2", "msg", err.Error())
1108 writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest)
1109 return
1110 }
1111
1112 mergeBase, err := gr.MergeBase(commit1, commit2)
1113 if err != nil {
1114 l.Error("failed to find merge-base", "msg", err.Error())
1115 writeError(w, "failed to calculate diff", http.StatusBadRequest)
1116 return
1117 }
1118
1119 rawPatch, formatPatch, err := gr.FormatPatch(mergeBase, commit2)
1120 if err != nil {
1121 l.Error("error comparing revisions", "msg", err.Error())
1122 writeError(w, "error comparing revisions", http.StatusBadRequest)
1123 return
1124 }
1125
1126 writeJSON(w, types.RepoFormatPatchResponse{
1127 Rev1: commit1.Hash.String(),
1128 Rev2: commit2.Hash.String(),
1129 FormatPatch: formatPatch,
1130 MergeBase: mergeBase.Hash.String(),
1131 Patch: rawPatch,
1132 })
1133 return
1134}
1135
1136func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) {
1137 l := h.l.With("handler", "NewHiddenRef")
1138
1139 forkRef := chi.URLParam(r, "forkRef")
1140 forkRef, _ = url.PathUnescape(forkRef)
1141
1142 remoteRef := chi.URLParam(r, "remoteRef")
1143 remoteRef, _ = url.PathUnescape(remoteRef)
1144
1145 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1146 gr, err := git.PlainOpen(path)
1147 if err != nil {
1148 notFound(w)
1149 return
1150 }
1151
1152 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef)
1153 if err != nil {
1154 l.Error("error tracking hidden remote ref", "msg", err.Error())
1155 writeError(w, "error tracking hidden remote ref", http.StatusBadRequest)
1156 return
1157 }
1158
1159 w.WriteHeader(http.StatusNoContent)
1160 return
1161}
1162
1163func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
1164 l := h.l.With("handler", "AddMember")
1165
1166 data := struct {
1167 Did string `json:"did"`
1168 }{}
1169
1170 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1171 writeError(w, "invalid request body", http.StatusBadRequest)
1172 return
1173 }
1174
1175 did := data.Did
1176
1177 if err := h.db.AddDid(did); err != nil {
1178 l.Error("adding did", "error", err.Error())
1179 writeError(w, err.Error(), http.StatusInternalServerError)
1180 return
1181 }
1182 h.jc.AddDid(did)
1183
1184 if err := h.e.AddMember(ThisServer, did); err != nil {
1185 l.Error("adding member", "error", err.Error())
1186 writeError(w, err.Error(), http.StatusInternalServerError)
1187 return
1188 }
1189
1190 if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
1191 l.Error("fetching and adding keys", "error", err.Error())
1192 writeError(w, err.Error(), http.StatusInternalServerError)
1193 return
1194 }
1195
1196 w.WriteHeader(http.StatusNoContent)
1197}
1198
1199func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
1200 l := h.l.With("handler", "AddRepoCollaborator")
1201
1202 data := struct {
1203 Did string `json:"did"`
1204 }{}
1205
1206 ownerDid := chi.URLParam(r, "did")
1207 repo := chi.URLParam(r, "name")
1208
1209 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1210 writeError(w, "invalid request body", http.StatusBadRequest)
1211 return
1212 }
1213
1214 if err := h.db.AddDid(data.Did); err != nil {
1215 l.Error("adding did", "error", err.Error())
1216 writeError(w, err.Error(), http.StatusInternalServerError)
1217 return
1218 }
1219 h.jc.AddDid(data.Did)
1220
1221 repoName, _ := securejoin.SecureJoin(ownerDid, repo)
1222 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
1223 l.Error("adding repo collaborator", "error", err.Error())
1224 writeError(w, err.Error(), http.StatusInternalServerError)
1225 return
1226 }
1227
1228 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
1229 l.Error("fetching and adding keys", "error", err.Error())
1230 writeError(w, err.Error(), http.StatusInternalServerError)
1231 return
1232 }
1233
1234 w.WriteHeader(http.StatusNoContent)
1235}
1236
1237func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) {
1238 l := h.l.With("handler", "DefaultBranch")
1239 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1240
1241 gr, err := git.Open(path, "")
1242 if err != nil {
1243 notFound(w)
1244 return
1245 }
1246
1247 branch, err := gr.FindMainBranch()
1248 if err != nil {
1249 writeError(w, err.Error(), http.StatusInternalServerError)
1250 l.Error("getting default branch", "error", err.Error())
1251 return
1252 }
1253
1254 writeJSON(w, types.RepoDefaultBranchResponse{
1255 Branch: branch,
1256 })
1257}
1258
1259func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
1260 l := h.l.With("handler", "SetDefaultBranch")
1261 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1262
1263 data := struct {
1264 Branch string `json:"branch"`
1265 }{}
1266
1267 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1268 writeError(w, err.Error(), http.StatusBadRequest)
1269 return
1270 }
1271
1272 gr, err := git.PlainOpen(path)
1273 if err != nil {
1274 notFound(w)
1275 return
1276 }
1277
1278 err = gr.SetDefaultBranch(data.Branch)
1279 if err != nil {
1280 writeError(w, err.Error(), http.StatusInternalServerError)
1281 l.Error("setting default branch", "error", err.Error())
1282 return
1283 }
1284
1285 w.WriteHeader(http.StatusNoContent)
1286}
1287
1288func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
1289 l := h.l.With("handler", "Init")
1290
1291 if h.knotInitialized {
1292 writeError(w, "knot already initialized", http.StatusConflict)
1293 return
1294 }
1295
1296 data := struct {
1297 Did string `json:"did"`
1298 }{}
1299
1300 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1301 l.Error("failed to decode request body", "error", err.Error())
1302 writeError(w, "invalid request body", http.StatusBadRequest)
1303 return
1304 }
1305
1306 if data.Did == "" {
1307 l.Error("empty DID in request", "did", data.Did)
1308 writeError(w, "did is empty", http.StatusBadRequest)
1309 return
1310 }
1311
1312 if err := h.db.AddDid(data.Did); err != nil {
1313 l.Error("failed to add DID", "error", err.Error())
1314 writeError(w, err.Error(), http.StatusInternalServerError)
1315 return
1316 }
1317 h.jc.AddDid(data.Did)
1318
1319 if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
1320 l.Error("adding owner", "error", err.Error())
1321 writeError(w, err.Error(), http.StatusInternalServerError)
1322 return
1323 }
1324
1325 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
1326 l.Error("fetching and adding keys", "error", err.Error())
1327 writeError(w, err.Error(), http.StatusInternalServerError)
1328 return
1329 }
1330
1331 close(h.init)
1332
1333 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
1334 mac.Write([]byte("ok"))
1335 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
1336
1337 w.WriteHeader(http.StatusNoContent)
1338}
1339
1340func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
1341 w.Write([]byte("ok"))
1342}
1343
1344func validateRepoName(name string) error {
1345 // check for path traversal attempts
1346 if name == "." || name == ".." ||
1347 strings.Contains(name, "/") || strings.Contains(name, "\\") {
1348 return fmt.Errorf("Repository name contains invalid path characters")
1349 }
1350
1351 // check for sequences that could be used for traversal when normalized
1352 if strings.Contains(name, "./") || strings.Contains(name, "../") ||
1353 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
1354 return fmt.Errorf("Repository name contains invalid path sequence")
1355 }
1356
1357 // then continue with character validation
1358 for _, char := range name {
1359 if !((char >= 'a' && char <= 'z') ||
1360 (char >= 'A' && char <= 'Z') ||
1361 (char >= '0' && char <= '9') ||
1362 char == '-' || char == '_' || char == '.') {
1363 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
1364 }
1365 }
1366
1367 // additional check to prevent multiple sequential dots
1368 if strings.Contains(name, "..") {
1369 return fmt.Errorf("Repository name cannot contain sequential dots")
1370 }
1371
1372 // if all checks pass
1373 return nil
1374}