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