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