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