1package ssh
2
3import (
4 "errors"
5 "path/filepath"
6 "time"
7
8 "github.com/charmbracelet/log"
9 "github.com/charmbracelet/soft-serve/server/access"
10 "github.com/charmbracelet/soft-serve/server/backend"
11 "github.com/charmbracelet/soft-serve/server/config"
12 "github.com/charmbracelet/soft-serve/server/git"
13 "github.com/charmbracelet/soft-serve/server/proto"
14 "github.com/charmbracelet/soft-serve/server/sshutils"
15 "github.com/charmbracelet/soft-serve/server/utils"
16 "github.com/charmbracelet/ssh"
17)
18
19func handleGit(s ssh.Session) {
20 ctx := s.Context()
21 cfg := config.FromContext(ctx)
22 be := backend.FromContext(ctx)
23 logger := log.FromContext(ctx)
24 cmdLine := s.Command()
25 start := time.Now()
26
27 // repo should be in the form of "repo.git"
28 name := utils.SanitizeRepo(cmdLine[1])
29 pk := s.PublicKey()
30 ak := sshutils.MarshalAuthorizedKey(pk)
31 accessLevel := be.AccessLevelByPublicKey(ctx, name, pk)
32 // git bare repositories should end in ".git"
33 // https://git-scm.com/docs/gitrepository-layout
34 repo := name + ".git"
35 reposDir := filepath.Join(cfg.DataPath, "repos")
36 if err := git.EnsureWithin(reposDir, repo); err != nil {
37 sshFatal(s, err)
38 return
39 }
40
41 // Environment variables to pass down to git hooks.
42 envs := []string{
43 "SOFT_SERVE_REPO_NAME=" + name,
44 "SOFT_SERVE_REPO_PATH=" + filepath.Join(reposDir, repo),
45 "SOFT_SERVE_PUBLIC_KEY=" + ak,
46 "SOFT_SERVE_USERNAME=" + s.User(),
47 "SOFT_SERVE_LOG_PATH=" + filepath.Join(cfg.DataPath, "log", "hooks.log"),
48 }
49
50 // Add ssh session & config environ
51 envs = append(envs, s.Environ()...)
52 envs = append(envs, cfg.Environ()...)
53
54 repoDir := filepath.Join(reposDir, repo)
55 service := git.Service(cmdLine[0])
56 cmd := git.ServiceCommand{
57 Stdin: s,
58 Stdout: s,
59 Stderr: s.Stderr(),
60 Env: envs,
61 Dir: repoDir,
62 }
63
64 logger.Debug("git middleware", "cmd", service, "access", accessLevel.String())
65
66 switch service {
67 case git.ReceivePackService:
68 receivePackCounter.WithLabelValues(name).Inc()
69 defer func() {
70 receivePackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
71 }()
72 if accessLevel < access.ReadWriteAccess {
73 sshFatal(s, git.ErrNotAuthed)
74 return
75 }
76 if _, err := be.Repository(ctx, name); err != nil {
77 if _, err := be.CreateRepository(ctx, name, proto.RepositoryOptions{Private: false}); err != nil {
78 log.Errorf("failed to create repo: %s", err)
79 sshFatal(s, err)
80 return
81 }
82 createRepoCounter.WithLabelValues(name).Inc()
83 }
84
85 if err := git.ReceivePack(ctx, cmd); err != nil {
86 sshFatal(s, git.ErrSystemMalfunction)
87 }
88
89 if err := git.EnsureDefaultBranch(ctx, cmd); err != nil {
90 sshFatal(s, git.ErrSystemMalfunction)
91 }
92
93 receivePackCounter.WithLabelValues(name).Inc()
94 return
95 case git.UploadPackService, git.UploadArchiveService:
96 if accessLevel < access.ReadOnlyAccess {
97 sshFatal(s, git.ErrNotAuthed)
98 return
99 }
100
101 handler := git.UploadPack
102 switch service {
103 case git.UploadArchiveService:
104 handler = git.UploadArchive
105 uploadArchiveCounter.WithLabelValues(name).Inc()
106 defer func() {
107 uploadArchiveSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
108 }()
109 default:
110 uploadPackCounter.WithLabelValues(name).Inc()
111 defer func() {
112 uploadPackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
113 }()
114 }
115
116 err := handler(ctx, cmd)
117 if errors.Is(err, git.ErrInvalidRepo) {
118 sshFatal(s, git.ErrInvalidRepo)
119 } else if err != nil {
120 logger.Error("git middleware", "err", err)
121 sshFatal(s, git.ErrSystemMalfunction)
122 }
123 }
124}