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