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/lfs"
14 "github.com/charmbracelet/soft-serve/server/proto"
15 "github.com/charmbracelet/soft-serve/server/sshutils"
16 "github.com/charmbracelet/soft-serve/server/utils"
17 "github.com/charmbracelet/ssh"
18)
19
20func handleGit(s ssh.Session) {
21 ctx := s.Context()
22 cfg := config.FromContext(ctx)
23 be := backend.FromContext(ctx)
24 logger := log.FromContext(ctx)
25 cmdLine := s.Command()
26 start := time.Now()
27
28 // repo should be in the form of "repo.git"
29 name := utils.SanitizeRepo(cmdLine[1])
30 pk := s.PublicKey()
31 ak := sshutils.MarshalAuthorizedKey(pk)
32 user := proto.UserFromContext(ctx)
33 accessLevel := be.AccessLevelForUser(ctx, name, user)
34 // git bare repositories should end in ".git"
35 // https://git-scm.com/docs/gitrepository-layout
36 repoDir := name + ".git"
37 reposDir := filepath.Join(cfg.DataPath, "repos")
38 if err := git.EnsureWithin(reposDir, repoDir); err != nil {
39 sshFatal(s, err)
40 return
41 }
42
43 // Set repo in context
44 repo, _ := be.Repository(ctx, name)
45 ctx.SetValue(proto.ContextKeyRepository, repo)
46
47 // Environment variables to pass down to git hooks.
48 envs := []string{
49 "SOFT_SERVE_REPO_NAME=" + name,
50 "SOFT_SERVE_REPO_PATH=" + filepath.Join(reposDir, repoDir),
51 "SOFT_SERVE_PUBLIC_KEY=" + ak,
52 "SOFT_SERVE_LOG_PATH=" + filepath.Join(cfg.DataPath, "log", "hooks.log"),
53 }
54
55 if user != nil {
56 envs = append(envs,
57 "SOFT_SERVE_USERNAME="+user.Username(),
58 )
59 }
60
61 // Add ssh session & config environ
62 envs = append(envs, s.Environ()...)
63 envs = append(envs, cfg.Environ()...)
64
65 repoPath := filepath.Join(reposDir, repoDir)
66 service := git.Service(cmdLine[0])
67 cmd := git.ServiceCommand{
68 Stdin: s,
69 Stdout: s,
70 Stderr: s.Stderr(),
71 Env: envs,
72 Dir: repoPath,
73 }
74
75 logger.Debug("git middleware", "cmd", service, "access", accessLevel.String())
76
77 switch service {
78 case git.ReceivePackService:
79 receivePackCounter.WithLabelValues(name).Inc()
80 defer func() {
81 receivePackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
82 }()
83 if accessLevel < access.ReadWriteAccess {
84 sshFatal(s, git.ErrNotAuthed)
85 return
86 }
87 if repo == nil {
88 if _, err := be.CreateRepository(ctx, name, proto.RepositoryOptions{Private: false}); err != nil {
89 log.Errorf("failed to create repo: %s", err)
90 sshFatal(s, err)
91 return
92 }
93 createRepoCounter.WithLabelValues(name).Inc()
94 }
95
96 if err := git.ReceivePack(ctx, cmd); err != nil {
97 sshFatal(s, git.ErrSystemMalfunction)
98 }
99
100 if err := git.EnsureDefaultBranch(ctx, cmd); err != nil {
101 sshFatal(s, git.ErrSystemMalfunction)
102 }
103
104 receivePackCounter.WithLabelValues(name).Inc()
105 return
106 case git.UploadPackService, git.UploadArchiveService:
107 if accessLevel < access.ReadOnlyAccess {
108 sshFatal(s, git.ErrNotAuthed)
109 return
110 }
111
112 switch service {
113 case git.UploadArchiveService:
114 uploadArchiveCounter.WithLabelValues(name).Inc()
115 defer func() {
116 uploadArchiveSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
117 }()
118 default:
119 uploadPackCounter.WithLabelValues(name).Inc()
120 defer func() {
121 uploadPackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
122 }()
123 }
124
125 err := service.Handler(ctx, cmd)
126 if errors.Is(err, git.ErrInvalidRepo) {
127 sshFatal(s, git.ErrInvalidRepo)
128 } else if err != nil {
129 logger.Error("git middleware", "err", err)
130 sshFatal(s, git.ErrSystemMalfunction)
131 }
132
133 return
134 case git.LFSTransferService, git.LFSAuthenticateService:
135 if accessLevel < access.ReadWriteAccess {
136 sshFatal(s, git.ErrNotAuthed)
137 return
138 }
139
140 if len(cmdLine) != 3 ||
141 (cmdLine[2] != lfs.OperationDownload && cmdLine[2] != lfs.OperationUpload) {
142 sshFatal(s, git.ErrInvalidRequest)
143 return
144 }
145
146 cmd.Args = []string{
147 name,
148 cmdLine[2],
149 }
150
151 if err := service.Handler(ctx, cmd); err != nil {
152 logger.Error("git middleware", "err", err)
153 sshFatal(s, git.ErrSystemMalfunction)
154 return
155 }
156 }
157}