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