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