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