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/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/rbac"
33 "tangled.sh/tangled.sh/core/types"
34)
35
36func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
37 w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
38}
39
40func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) {
41 w.Header().Set("Content-Type", "application/json")
42
43 capabilities := map[string]any{
44 "pull_requests": map[string]any{
45 "format_patch": true,
46 "patch_submissions": true,
47 "branch_submissions": true,
48 "fork_submissions": true,
49 },
50 }
51
52 jsonData, err := json.Marshal(capabilities)
53 if err != nil {
54 http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError)
55 return
56 }
57
58 w.Write(jsonData)
59}
60
61func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
62 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
63 l := h.l.With("path", path, "handler", "RepoIndex")
64 ref := chi.URLParam(r, "ref")
65 ref, _ = url.PathUnescape(ref)
66
67 gr, err := git.Open(path, ref)
68 if err != nil {
69 plain, err2 := git.PlainOpen(path)
70 if err2 != nil {
71 l.Error("opening repo", "error", err2.Error())
72 notFound(w)
73 return
74 }
75 branches, _ := plain.Branches()
76
77 log.Println(err)
78
79 if errors.Is(err, plumbing.ErrReferenceNotFound) {
80 resp := types.RepoIndexResponse{
81 IsEmpty: true,
82 Branches: branches,
83 }
84 writeJSON(w, resp)
85 return
86 } else {
87 l.Error("opening repo", "error", err.Error())
88 notFound(w)
89 return
90 }
91 }
92
93 var (
94 commits []*object.Commit
95 total int
96 branches []types.Branch
97 files []types.NiceTree
98 tags []object.Tag
99 )
100
101 var wg sync.WaitGroup
102 errorsCh := make(chan error, 5)
103
104 wg.Add(1)
105 go func() {
106 defer wg.Done()
107 cs, err := gr.Commits(0, 60)
108 if err != nil {
109 errorsCh <- fmt.Errorf("commits: %w", err)
110 return
111 }
112 commits = cs
113 }()
114
115 wg.Add(1)
116 go func() {
117 defer wg.Done()
118 t, err := gr.TotalCommits()
119 if err != nil {
120 errorsCh <- fmt.Errorf("calculating total: %w", err)
121 return
122 }
123 total = t
124 }()
125
126 wg.Add(1)
127 go func() {
128 defer wg.Done()
129 bs, err := gr.Branches()
130 if err != nil {
131 errorsCh <- fmt.Errorf("fetching branches: %w", err)
132 return
133 }
134 branches = bs
135 }()
136
137 wg.Add(1)
138 go func() {
139 defer wg.Done()
140 ts, err := gr.Tags()
141 if err != nil {
142 errorsCh <- fmt.Errorf("fetching tags: %w", err)
143 return
144 }
145 tags = ts
146 }()
147
148 wg.Add(1)
149 go func() {
150 defer wg.Done()
151 fs, err := gr.FileTree(r.Context(), "")
152 if err != nil {
153 errorsCh <- fmt.Errorf("fetching filetree: %w", err)
154 return
155 }
156 files = fs
157 }()
158
159 wg.Wait()
160 close(errorsCh)
161
162 // show any errors
163 for err := range errorsCh {
164 l.Error("loading repo", "error", err.Error())
165 writeError(w, err.Error(), http.StatusInternalServerError)
166 return
167 }
168
169 rtags := []*types.TagReference{}
170 for _, tag := range tags {
171 var target *object.Tag
172 if tag.Target != plumbing.ZeroHash {
173 target = &tag
174 }
175 tr := types.TagReference{
176 Tag: target,
177 }
178
179 tr.Reference = types.Reference{
180 Name: tag.Name,
181 Hash: tag.Hash.String(),
182 }
183
184 if tag.Message != "" {
185 tr.Message = tag.Message
186 }
187
188 rtags = append(rtags, &tr)
189 }
190
191 var readmeContent string
192 var readmeFile string
193 for _, readme := range h.c.Repo.Readme {
194 content, _ := gr.FileContent(readme)
195 if len(content) > 0 {
196 readmeContent = string(content)
197 readmeFile = readme
198 }
199 }
200
201 if ref == "" {
202 mainBranch, err := gr.FindMainBranch()
203 if err != nil {
204 writeError(w, err.Error(), http.StatusInternalServerError)
205 l.Error("finding main branch", "error", err.Error())
206 return
207 }
208 ref = mainBranch
209 }
210
211 resp := types.RepoIndexResponse{
212 IsEmpty: false,
213 Ref: ref,
214 Commits: commits,
215 Description: getDescription(path),
216 Readme: readmeContent,
217 ReadmeFileName: readmeFile,
218 Files: files,
219 Branches: branches,
220 Tags: rtags,
221 TotalCommits: total,
222 }
223
224 writeJSON(w, resp)
225 return
226}
227
228func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
229 treePath := chi.URLParam(r, "*")
230 ref := chi.URLParam(r, "ref")
231 ref, _ = url.PathUnescape(ref)
232
233 l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath)
234
235 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
236 gr, err := git.Open(path, ref)
237 if err != nil {
238 notFound(w)
239 return
240 }
241
242 files, err := gr.FileTree(r.Context(), treePath)
243 if err != nil {
244 writeError(w, err.Error(), http.StatusInternalServerError)
245 l.Error("file tree", "error", err.Error())
246 return
247 }
248
249 resp := types.RepoTreeResponse{
250 Ref: ref,
251 Parent: treePath,
252 Description: getDescription(path),
253 DotDot: filepath.Dir(treePath),
254 Files: files,
255 }
256
257 writeJSON(w, resp)
258 return
259}
260
261func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) {
262 treePath := chi.URLParam(r, "*")
263 ref := chi.URLParam(r, "ref")
264 ref, _ = url.PathUnescape(ref)
265
266 l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath)
267
268 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
269 gr, err := git.Open(path, ref)
270 if err != nil {
271 notFound(w)
272 return
273 }
274
275 contents, err := gr.RawContent(treePath)
276 if err != nil {
277 writeError(w, err.Error(), http.StatusBadRequest)
278 l.Error("file content", "error", err.Error())
279 return
280 }
281
282 mimeType := http.DetectContentType(contents)
283
284 // exception for svg
285 if filepath.Ext(treePath) == ".svg" {
286 mimeType = "image/svg+xml"
287 }
288
289 contentHash := sha256.Sum256(contents)
290 eTag := fmt.Sprintf("\"%x\"", contentHash)
291
292 // allow image, video, and text/plain files to be served directly
293 switch {
294 case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"):
295 if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag {
296 w.WriteHeader(http.StatusNotModified)
297 return
298 }
299 w.Header().Set("ETag", eTag)
300
301 case strings.HasPrefix(mimeType, "text/plain"):
302 w.Header().Set("Cache-Control", "public, no-cache")
303
304 default:
305 l.Error("attempted to serve disallowed file type", "mimetype", mimeType)
306 writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
307 return
308 }
309
310 w.Header().Set("Content-Type", mimeType)
311 w.Write(contents)
312}
313
314func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) {
315 treePath := chi.URLParam(r, "*")
316 ref := chi.URLParam(r, "ref")
317 ref, _ = url.PathUnescape(ref)
318
319 l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath)
320
321 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
322 gr, err := git.Open(path, ref)
323 if err != nil {
324 notFound(w)
325 return
326 }
327
328 var isBinaryFile bool = false
329 contents, err := gr.FileContent(treePath)
330 if errors.Is(err, git.ErrBinaryFile) {
331 isBinaryFile = true
332 } else if errors.Is(err, object.ErrFileNotFound) {
333 notFound(w)
334 return
335 } else if err != nil {
336 writeError(w, err.Error(), http.StatusInternalServerError)
337 return
338 }
339
340 bytes := []byte(contents)
341 // safe := string(sanitize(bytes))
342 sizeHint := len(bytes)
343
344 resp := types.RepoBlobResponse{
345 Ref: ref,
346 Contents: string(bytes),
347 Path: treePath,
348 IsBinary: isBinaryFile,
349 SizeHint: uint64(sizeHint),
350 }
351
352 h.showFile(resp, w, l)
353}
354
355func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
356 name := chi.URLParam(r, "name")
357 file := chi.URLParam(r, "file")
358
359 l := h.l.With("handler", "Archive", "name", name, "file", file)
360
361 // TODO: extend this to add more files compression (e.g.: xz)
362 if !strings.HasSuffix(file, ".tar.gz") {
363 notFound(w)
364 return
365 }
366
367 ref := strings.TrimSuffix(file, ".tar.gz")
368
369 unescapedRef, err := url.PathUnescape(ref)
370 if err != nil {
371 notFound(w)
372 return
373 }
374
375 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-")
376
377 // This allows the browser to use a proper name for the file when
378 // downloading
379 filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename)
380 setContentDisposition(w, filename)
381 setGZipMIME(w)
382
383 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
384 gr, err := git.Open(path, unescapedRef)
385 if err != nil {
386 notFound(w)
387 return
388 }
389
390 gw := gzip.NewWriter(w)
391 defer gw.Close()
392
393 prefix := fmt.Sprintf("%s-%s", name, safeRefFilename)
394 err = gr.WriteTar(gw, prefix)
395 if err != nil {
396 // once we start writing to the body we can't report error anymore
397 // so we are only left with printing the error.
398 l.Error("writing tar file", "error", err.Error())
399 return
400 }
401
402 err = gw.Flush()
403 if err != nil {
404 // once we start writing to the body we can't report error anymore
405 // so we are only left with printing the error.
406 l.Error("flushing?", "error", err.Error())
407 return
408 }
409}
410
411func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
412 ref := chi.URLParam(r, "ref")
413 ref, _ = url.PathUnescape(ref)
414
415 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
416
417 l := h.l.With("handler", "Log", "ref", ref, "path", path)
418
419 gr, err := git.Open(path, ref)
420 if err != nil {
421 notFound(w)
422 return
423 }
424
425 // Get page parameters
426 page := 1
427 pageSize := 30
428
429 if pageParam := r.URL.Query().Get("page"); pageParam != "" {
430 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
431 page = p
432 }
433 }
434
435 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
436 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
437 pageSize = ps
438 }
439 }
440
441 // convert to offset/limit
442 offset := (page - 1) * pageSize
443 limit := pageSize
444
445 commits, err := gr.Commits(offset, limit)
446 if err != nil {
447 writeError(w, err.Error(), http.StatusInternalServerError)
448 l.Error("fetching commits", "error", err.Error())
449 return
450 }
451
452 total := len(commits)
453
454 resp := types.RepoLogResponse{
455 Commits: commits,
456 Ref: ref,
457 Description: getDescription(path),
458 Log: true,
459 Total: total,
460 Page: page,
461 PerPage: pageSize,
462 }
463
464 writeJSON(w, resp)
465 return
466}
467
468func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
469 ref := chi.URLParam(r, "ref")
470 ref, _ = url.PathUnescape(ref)
471
472 l := h.l.With("handler", "Diff", "ref", ref)
473
474 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
475 gr, err := git.Open(path, ref)
476 if err != nil {
477 notFound(w)
478 return
479 }
480
481 diff, err := gr.Diff()
482 if err != nil {
483 writeError(w, err.Error(), http.StatusInternalServerError)
484 l.Error("getting diff", "error", err.Error())
485 return
486 }
487
488 resp := types.RepoCommitResponse{
489 Ref: ref,
490 Diff: diff,
491 }
492
493 writeJSON(w, resp)
494 return
495}
496
497func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
498 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
499 l := h.l.With("handler", "Refs")
500
501 gr, err := git.Open(path, "")
502 if err != nil {
503 notFound(w)
504 return
505 }
506
507 tags, err := gr.Tags()
508 if err != nil {
509 // Non-fatal, we *should* have at least one branch to show.
510 l.Warn("getting tags", "error", err.Error())
511 }
512
513 rtags := []*types.TagReference{}
514 for _, tag := range tags {
515 var target *object.Tag
516 if tag.Target != plumbing.ZeroHash {
517 target = &tag
518 }
519 tr := types.TagReference{
520 Tag: target,
521 }
522
523 tr.Reference = types.Reference{
524 Name: tag.Name,
525 Hash: tag.Hash.String(),
526 }
527
528 if tag.Message != "" {
529 tr.Message = tag.Message
530 }
531
532 rtags = append(rtags, &tr)
533 }
534
535 resp := types.RepoTagsResponse{
536 Tags: rtags,
537 }
538
539 writeJSON(w, resp)
540 return
541}
542
543func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
544 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
545
546 gr, err := git.PlainOpen(path)
547 if err != nil {
548 notFound(w)
549 return
550 }
551
552 branches, _ := gr.Branches()
553
554 resp := types.RepoBranchesResponse{
555 Branches: branches,
556 }
557
558 writeJSON(w, resp)
559 return
560}
561
562func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) {
563 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
564 branchName := chi.URLParam(r, "branch")
565 branchName, _ = url.PathUnescape(branchName)
566
567 l := h.l.With("handler", "Branch")
568
569 gr, err := git.PlainOpen(path)
570 if err != nil {
571 notFound(w)
572 return
573 }
574
575 ref, err := gr.Branch(branchName)
576 if err != nil {
577 l.Error("getting branch", "error", err.Error())
578 writeError(w, err.Error(), http.StatusInternalServerError)
579 return
580 }
581
582 commit, err := gr.Commit(ref.Hash())
583 if err != nil {
584 l.Error("getting commit object", "error", err.Error())
585 writeError(w, err.Error(), http.StatusInternalServerError)
586 return
587 }
588
589 defaultBranch, err := gr.FindMainBranch()
590 isDefault := false
591 if err != nil {
592 l.Error("getting default branch", "error", err.Error())
593 // do not quit though
594 } else if defaultBranch == branchName {
595 isDefault = true
596 }
597
598 resp := types.RepoBranchResponse{
599 Branch: types.Branch{
600 Reference: types.Reference{
601 Name: ref.Name().Short(),
602 Hash: ref.Hash().String(),
603 },
604 Commit: commit,
605 IsDefault: isDefault,
606 },
607 }
608
609 writeJSON(w, resp)
610 return
611}
612
613func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
614 l := h.l.With("handler", "Keys")
615
616 switch r.Method {
617 case http.MethodGet:
618 keys, err := h.db.GetAllPublicKeys()
619 if err != nil {
620 writeError(w, err.Error(), http.StatusInternalServerError)
621 l.Error("getting public keys", "error", err.Error())
622 return
623 }
624
625 data := make([]map[string]any, 0)
626 for _, key := range keys {
627 j := key.JSON()
628 data = append(data, j)
629 }
630 writeJSON(w, data)
631 return
632
633 case http.MethodPut:
634 pk := db.PublicKey{}
635 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
636 writeError(w, "invalid request body", http.StatusBadRequest)
637 return
638 }
639
640 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
641 if err != nil {
642 writeError(w, "invalid pubkey", http.StatusBadRequest)
643 }
644
645 if err := h.db.AddPublicKey(pk); err != nil {
646 writeError(w, err.Error(), http.StatusInternalServerError)
647 l.Error("adding public key", "error", err.Error())
648 return
649 }
650
651 w.WriteHeader(http.StatusNoContent)
652 return
653 }
654}
655
656func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
657 l := h.l.With("handler", "NewRepo")
658
659 data := struct {
660 Did string `json:"did"`
661 Name string `json:"name"`
662 DefaultBranch string `json:"default_branch,omitempty"`
663 }{}
664
665 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
666 writeError(w, "invalid request body", http.StatusBadRequest)
667 return
668 }
669
670 if data.DefaultBranch == "" {
671 data.DefaultBranch = h.c.Repo.MainBranch
672 }
673
674 did := data.Did
675 name := data.Name
676 defaultBranch := data.DefaultBranch
677
678 if err := validateRepoName(name); err != nil {
679 l.Error("creating repo", "error", err.Error())
680 writeError(w, err.Error(), http.StatusBadRequest)
681 return
682 }
683
684 relativeRepoPath := filepath.Join(did, name)
685 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
686 err := git.InitBare(repoPath, defaultBranch)
687 if err != nil {
688 l.Error("initializing bare repo", "error", err.Error())
689 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
690 writeError(w, "That repo already exists!", http.StatusConflict)
691 return
692 } else {
693 writeError(w, err.Error(), http.StatusInternalServerError)
694 return
695 }
696 }
697
698 // add perms for this user to access the repo
699 err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath)
700 if err != nil {
701 l.Error("adding repo permissions", "error", err.Error())
702 writeError(w, err.Error(), http.StatusInternalServerError)
703 return
704 }
705
706 hook.SetupRepo(
707 hook.Config(
708 hook.WithScanPath(h.c.Repo.ScanPath),
709 hook.WithInternalApi(h.c.Server.InternalListenAddr),
710 ),
711 repoPath,
712 )
713
714 w.WriteHeader(http.StatusNoContent)
715}
716
717func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) {
718 l := h.l.With("handler", "RepoForkSync")
719
720 data := struct {
721 Did string `json:"did"`
722 Source string `json:"source"`
723 Name string `json:"name,omitempty"`
724 HiddenRef string `json:"hiddenref"`
725 }{}
726
727 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
728 writeError(w, "invalid request body", http.StatusBadRequest)
729 return
730 }
731
732 did := data.Did
733 source := data.Source
734
735 if did == "" || source == "" {
736 l.Error("invalid request body, empty did or name")
737 w.WriteHeader(http.StatusBadRequest)
738 return
739 }
740
741 var name string
742 if data.Name != "" {
743 name = data.Name
744 } else {
745 name = filepath.Base(source)
746 }
747
748 branch := chi.URLParam(r, "branch")
749 branch, _ = url.PathUnescape(branch)
750
751 relativeRepoPath := filepath.Join(did, name)
752 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
753
754 gr, err := git.PlainOpen(repoPath)
755 if err != nil {
756 log.Println(err)
757 notFound(w)
758 return
759 }
760
761 forkCommit, err := gr.ResolveRevision(branch)
762 if err != nil {
763 l.Error("error resolving ref revision", "msg", err.Error())
764 writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest)
765 return
766 }
767
768 sourceCommit, err := gr.ResolveRevision(data.HiddenRef)
769 if err != nil {
770 l.Error("error resolving hidden ref revision", "msg", err.Error())
771 writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest)
772 return
773 }
774
775 status := types.UpToDate
776 if forkCommit.Hash.String() != sourceCommit.Hash.String() {
777 isAncestor, err := forkCommit.IsAncestor(sourceCommit)
778 if err != nil {
779 log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err)
780 return
781 }
782
783 if isAncestor {
784 status = types.FastForwardable
785 } else {
786 status = types.Conflict
787 }
788 }
789
790 w.Header().Set("Content-Type", "application/json")
791 json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status})
792}
793
794func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) {
795 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
796 ref := chi.URLParam(r, "ref")
797 ref, _ = url.PathUnescape(ref)
798
799 l := h.l.With("handler", "RepoLanguages")
800
801 gr, err := git.Open(repoPath, ref)
802 if err != nil {
803 l.Error("opening repo", "error", err.Error())
804 notFound(w)
805 return
806 }
807
808 ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
809 defer cancel()
810
811 sizes, err := gr.AnalyzeLanguages(ctx)
812 if err != nil {
813 l.Error("failed to analyze languages", "error", err.Error())
814 writeError(w, err.Error(), http.StatusNoContent)
815 return
816 }
817
818 resp := types.RepoLanguageResponse{Languages: sizes}
819
820 writeJSON(w, resp)
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, rbac.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 hook.SetupRepo(
925 hook.Config(
926 hook.WithScanPath(h.c.Repo.ScanPath),
927 hook.WithInternalApi(h.c.Server.InternalListenAddr),
928 ),
929 repoPath,
930 )
931
932 w.WriteHeader(http.StatusNoContent)
933}
934
935func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
936 l := h.l.With("handler", "RemoveRepo")
937
938 data := struct {
939 Did string `json:"did"`
940 Name string `json:"name"`
941 }{}
942
943 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
944 writeError(w, "invalid request body", http.StatusBadRequest)
945 return
946 }
947
948 did := data.Did
949 name := data.Name
950
951 if did == "" || name == "" {
952 l.Error("invalid request body, empty did or name")
953 w.WriteHeader(http.StatusBadRequest)
954 return
955 }
956
957 relativeRepoPath := filepath.Join(did, name)
958 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
959 err := os.RemoveAll(repoPath)
960 if err != nil {
961 l.Error("removing repo", "error", err.Error())
962 writeError(w, err.Error(), http.StatusInternalServerError)
963 return
964 }
965
966 w.WriteHeader(http.StatusNoContent)
967
968}
969func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
970 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
971
972 data := types.MergeRequest{}
973
974 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
975 writeError(w, err.Error(), http.StatusBadRequest)
976 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err)
977 return
978 }
979
980 mo := &git.MergeOptions{
981 AuthorName: data.AuthorName,
982 AuthorEmail: data.AuthorEmail,
983 CommitBody: data.CommitBody,
984 CommitMessage: data.CommitMessage,
985 }
986
987 patch := data.Patch
988 branch := data.Branch
989 gr, err := git.Open(path, branch)
990 if err != nil {
991 notFound(w)
992 return
993 }
994
995 mo.FormatPatch = patchutil.IsFormatPatch(patch)
996
997 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
998 var mergeErr *git.ErrMerge
999 if errors.As(err, &mergeErr) {
1000 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
1001 for i, conflict := range mergeErr.Conflicts {
1002 conflicts[i] = types.ConflictInfo{
1003 Filename: conflict.Filename,
1004 Reason: conflict.Reason,
1005 }
1006 }
1007 response := types.MergeCheckResponse{
1008 IsConflicted: true,
1009 Conflicts: conflicts,
1010 Message: mergeErr.Message,
1011 }
1012 writeConflict(w, response)
1013 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr)
1014 } else {
1015 writeError(w, err.Error(), http.StatusBadRequest)
1016 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error())
1017 }
1018 return
1019 }
1020
1021 w.WriteHeader(http.StatusOK)
1022}
1023
1024func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) {
1025 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1026
1027 var data struct {
1028 Patch string `json:"patch"`
1029 Branch string `json:"branch"`
1030 }
1031
1032 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1033 writeError(w, err.Error(), http.StatusBadRequest)
1034 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err)
1035 return
1036 }
1037
1038 patch := data.Patch
1039 branch := data.Branch
1040 gr, err := git.Open(path, branch)
1041 if err != nil {
1042 notFound(w)
1043 return
1044 }
1045
1046 err = gr.MergeCheck([]byte(patch), branch)
1047 if err == nil {
1048 response := types.MergeCheckResponse{
1049 IsConflicted: false,
1050 }
1051 writeJSON(w, response)
1052 return
1053 }
1054
1055 var mergeErr *git.ErrMerge
1056 if errors.As(err, &mergeErr) {
1057 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
1058 for i, conflict := range mergeErr.Conflicts {
1059 conflicts[i] = types.ConflictInfo{
1060 Filename: conflict.Filename,
1061 Reason: conflict.Reason,
1062 }
1063 }
1064 response := types.MergeCheckResponse{
1065 IsConflicted: true,
1066 Conflicts: conflicts,
1067 Message: mergeErr.Message,
1068 }
1069 writeConflict(w, response)
1070 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error())
1071 return
1072 }
1073 writeError(w, err.Error(), http.StatusInternalServerError)
1074 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
1075}
1076
1077func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) {
1078 rev1 := chi.URLParam(r, "rev1")
1079 rev1, _ = url.PathUnescape(rev1)
1080
1081 rev2 := chi.URLParam(r, "rev2")
1082 rev2, _ = url.PathUnescape(rev2)
1083
1084 l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2)
1085
1086 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1087 gr, err := git.PlainOpen(path)
1088 if err != nil {
1089 notFound(w)
1090 return
1091 }
1092
1093 commit1, err := gr.ResolveRevision(rev1)
1094 if err != nil {
1095 l.Error("error resolving revision 1", "msg", err.Error())
1096 writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest)
1097 return
1098 }
1099
1100 commit2, err := gr.ResolveRevision(rev2)
1101 if err != nil {
1102 l.Error("error resolving revision 2", "msg", err.Error())
1103 writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest)
1104 return
1105 }
1106
1107 rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2)
1108 if err != nil {
1109 l.Error("error comparing revisions", "msg", err.Error())
1110 writeError(w, "error comparing revisions", http.StatusBadRequest)
1111 return
1112 }
1113
1114 writeJSON(w, types.RepoFormatPatchResponse{
1115 Rev1: commit1.Hash.String(),
1116 Rev2: commit2.Hash.String(),
1117 FormatPatch: formatPatch,
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.AddKnotMember(rbac.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, rbac.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.AddKnotOwner(rbac.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}