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