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