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 Patch: rawPatch,
1010 })
1011 return
1012}
1013
1014func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) {
1015 l := h.l.With("handler", "NewHiddenRef")
1016
1017 forkRef := chi.URLParam(r, "forkRef")
1018 forkRef, _ = url.PathUnescape(forkRef)
1019
1020 remoteRef := chi.URLParam(r, "remoteRef")
1021 remoteRef, _ = url.PathUnescape(remoteRef)
1022
1023 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1024 gr, err := git.PlainOpen(path)
1025 if err != nil {
1026 notFound(w)
1027 return
1028 }
1029
1030 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef)
1031 if err != nil {
1032 l.Error("error tracking hidden remote ref", "msg", err.Error())
1033 writeError(w, "error tracking hidden remote ref", http.StatusBadRequest)
1034 return
1035 }
1036
1037 w.WriteHeader(http.StatusNoContent)
1038 return
1039}
1040
1041func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
1042 l := h.l.With("handler", "AddMember")
1043
1044 data := struct {
1045 Did string `json:"did"`
1046 }{}
1047
1048 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1049 writeError(w, "invalid request body", http.StatusBadRequest)
1050 return
1051 }
1052
1053 did := data.Did
1054
1055 if err := h.db.AddDid(did); err != nil {
1056 l.Error("adding did", "error", err.Error())
1057 writeError(w, err.Error(), http.StatusInternalServerError)
1058 return
1059 }
1060 h.jc.AddDid(did)
1061
1062 if err := h.e.AddMember(ThisServer, did); err != nil {
1063 l.Error("adding member", "error", err.Error())
1064 writeError(w, err.Error(), http.StatusInternalServerError)
1065 return
1066 }
1067
1068 if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
1069 l.Error("fetching and adding keys", "error", err.Error())
1070 writeError(w, err.Error(), http.StatusInternalServerError)
1071 return
1072 }
1073
1074 w.WriteHeader(http.StatusNoContent)
1075}
1076
1077func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
1078 l := h.l.With("handler", "AddRepoCollaborator")
1079
1080 data := struct {
1081 Did string `json:"did"`
1082 }{}
1083
1084 ownerDid := chi.URLParam(r, "did")
1085 repo := chi.URLParam(r, "name")
1086
1087 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1088 writeError(w, "invalid request body", http.StatusBadRequest)
1089 return
1090 }
1091
1092 if err := h.db.AddDid(data.Did); err != nil {
1093 l.Error("adding did", "error", err.Error())
1094 writeError(w, err.Error(), http.StatusInternalServerError)
1095 return
1096 }
1097 h.jc.AddDid(data.Did)
1098
1099 repoName, _ := securejoin.SecureJoin(ownerDid, repo)
1100 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
1101 l.Error("adding repo collaborator", "error", err.Error())
1102 writeError(w, err.Error(), http.StatusInternalServerError)
1103 return
1104 }
1105
1106 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
1107 l.Error("fetching and adding keys", "error", err.Error())
1108 writeError(w, err.Error(), http.StatusInternalServerError)
1109 return
1110 }
1111
1112 w.WriteHeader(http.StatusNoContent)
1113}
1114
1115func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) {
1116 l := h.l.With("handler", "DefaultBranch")
1117 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1118
1119 gr, err := git.Open(path, "")
1120 if err != nil {
1121 notFound(w)
1122 return
1123 }
1124
1125 branch, err := gr.FindMainBranch()
1126 if err != nil {
1127 writeError(w, err.Error(), http.StatusInternalServerError)
1128 l.Error("getting default branch", "error", err.Error())
1129 return
1130 }
1131
1132 writeJSON(w, types.RepoDefaultBranchResponse{
1133 Branch: branch,
1134 })
1135}
1136
1137func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
1138 l := h.l.With("handler", "SetDefaultBranch")
1139 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1140
1141 data := struct {
1142 Branch string `json:"branch"`
1143 }{}
1144
1145 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1146 writeError(w, err.Error(), http.StatusBadRequest)
1147 return
1148 }
1149
1150 gr, err := git.PlainOpen(path)
1151 if err != nil {
1152 notFound(w)
1153 return
1154 }
1155
1156 err = gr.SetDefaultBranch(data.Branch)
1157 if err != nil {
1158 writeError(w, err.Error(), http.StatusInternalServerError)
1159 l.Error("setting default branch", "error", err.Error())
1160 return
1161 }
1162
1163 w.WriteHeader(http.StatusNoContent)
1164}
1165
1166func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
1167 l := h.l.With("handler", "Init")
1168
1169 if h.knotInitialized {
1170 writeError(w, "knot already initialized", http.StatusConflict)
1171 return
1172 }
1173
1174 data := struct {
1175 Did string `json:"did"`
1176 }{}
1177
1178 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1179 l.Error("failed to decode request body", "error", err.Error())
1180 writeError(w, "invalid request body", http.StatusBadRequest)
1181 return
1182 }
1183
1184 if data.Did == "" {
1185 l.Error("empty DID in request", "did", data.Did)
1186 writeError(w, "did is empty", http.StatusBadRequest)
1187 return
1188 }
1189
1190 if err := h.db.AddDid(data.Did); err != nil {
1191 l.Error("failed to add DID", "error", err.Error())
1192 writeError(w, err.Error(), http.StatusInternalServerError)
1193 return
1194 }
1195 h.jc.AddDid(data.Did)
1196
1197 if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
1198 l.Error("adding owner", "error", err.Error())
1199 writeError(w, err.Error(), http.StatusInternalServerError)
1200 return
1201 }
1202
1203 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
1204 l.Error("fetching and adding keys", "error", err.Error())
1205 writeError(w, err.Error(), http.StatusInternalServerError)
1206 return
1207 }
1208
1209 close(h.init)
1210
1211 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
1212 mac.Write([]byte("ok"))
1213 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
1214
1215 w.WriteHeader(http.StatusNoContent)
1216}
1217
1218func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
1219 w.Write([]byte("ok"))
1220}
1221
1222func validateRepoName(name string) error {
1223 // check for path traversal attempts
1224 if name == "." || name == ".." ||
1225 strings.Contains(name, "/") || strings.Contains(name, "\\") {
1226 return fmt.Errorf("Repository name contains invalid path characters")
1227 }
1228
1229 // check for sequences that could be used for traversal when normalized
1230 if strings.Contains(name, "./") || strings.Contains(name, "../") ||
1231 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
1232 return fmt.Errorf("Repository name contains invalid path sequence")
1233 }
1234
1235 // then continue with character validation
1236 for _, char := range name {
1237 if !((char >= 'a' && char <= 'z') ||
1238 (char >= 'A' && char <= 'Z') ||
1239 (char >= '0' && char <= '9') ||
1240 char == '-' || char == '_' || char == '.') {
1241 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
1242 }
1243 }
1244
1245 // additional check to prevent multiple sequential dots
1246 if strings.Contains(name, "..") {
1247 return fmt.Errorf("Repository name cannot contain sequential dots")
1248 }
1249
1250 // if all checks pass
1251 return nil
1252}