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