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