all repos

mugit @ 0c6e821

🐮 git server that your cow will love

mugit/internal/ssh/ssh.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
git: move gitx into git, 3 months ago
1
package ssh
2
3
import (
4
	"fmt"
5
	"log/slog"
6
	"slices"
7
	"strconv"
8
9
	"github.com/gliderlabs/ssh"
10
	"olexsmir.xyz/mugit/internal/config"
11
	"olexsmir.xyz/mugit/internal/git"
12
13
	gossh "golang.org/x/crypto/ssh"
14
)
15
16
type authorizedKeyType string
17
18
const authorizedKey authorizedKeyType = "authorized"
19
20
type Server struct {
21
	c        *config.Config
22
	authKeys []gossh.PublicKey
23
}
24
25
func NewServer(cfg *config.Config) *Server {
26
	return &Server{
27
		c:        cfg,
28
		authKeys: []gossh.PublicKey{},
29
	}
30
}
31
32
func (s *Server) Start() error {
33
	if err := s.parseAuthKeys(); err != nil {
34
		return err
35
	}
36
37
	srv := &ssh.Server{
38
		Addr:             ":" + strconv.Itoa(s.c.SSH.Port),
39
		Handler:          s.handler,
40
		PublicKeyHandler: s.authhandler,
41
	}
42
43
	if err := srv.SetOption(ssh.HostKeyFile(s.c.SSH.HostKey)); err != nil {
44
		// TODO: validate `gossh.ParsePrivateKey`
45
		return err
46
	}
47
48
	return srv.ListenAndServe()
49
}
50
51
func (s *Server) authhandler(ctx ssh.Context, key ssh.PublicKey) bool {
52
	fingerprint := gossh.FingerprintSHA256(key)
53
54
	if ctx.User() != "git" {
55
		slog.Info("non git ssh request", "user", ctx.User(), "fingerprint", fingerprint)
56
		return false
57
	}
58
59
	slog.Info("ssh request", "fingerprint", fingerprint)
60
61
	ctx.SetValue(authorizedKey, s.isAuthorized(key))
62
	return true
63
}
64
65
func (s *Server) handler(sess ssh.Session) {
66
	ctx := sess.Context()
67
	authorized := ctx.Value(authorizedKey).(bool)
68
69
	cmd := sess.Command()
70
	if len(cmd) < 2 {
71
		s.error(sess, badRequestErrMsg, nil)
72
		return
73
	}
74
75
	gitCmd := cmd[0]
76
	userProvidedRepoName := cmd[1]
77
	repoPath, err := git.ResolvePath(s.c.Repo.Dir, git.ResolveName(userProvidedRepoName))
78
	if err != nil {
79
		s.error(sess, internalServerErrMsg, err)
80
		return
81
	}
82
83
	repo, err := git.Open(repoPath, "")
84
	if err != nil {
85
		s.gitError(sess, repoNotFoundErrMsg, err)
86
		return
87
	}
88
89
	switch gitCmd {
90
	case "git-upload-pack":
91
		isPrivate, err := repo.IsPrivate()
92
		if err != nil {
93
			s.gitError(sess, badRequestErrMsg, nil)
94
			return
95
		}
96
97
		if isPrivate && !authorized {
98
			s.gitError(sess, badRequestErrMsg, nil)
99
			return
100
		}
101
102
		if err := repo.UploadPack(ctx, false, "", sess, sess); err != nil {
103
			s.gitError(sess, internalServerErrMsg, err)
104
			return
105
		}
106
107
		sess.Exit(0)
108
109
	case "git-upload-archive":
110
		isPrivate, err := repo.IsPrivate()
111
		if err != nil {
112
			s.gitError(sess, badRequestErrMsg, nil)
113
			return
114
		}
115
116
		if isPrivate && !authorized {
117
			s.gitError(sess, badRequestErrMsg, nil)
118
			return
119
		}
120
121
		if err := repo.UploadArchive(ctx, sess, sess); err != nil {
122
			s.gitError(sess, internalServerErrMsg, err)
123
			return
124
		}
125
126
		sess.Exit(0)
127
128
	case "git-receive-pack":
129
		if !authorized {
130
			s.gitError(sess, unauthorizedErrMsg, nil)
131
			return
132
		}
133
134
		if err := repo.ReceivePack(ctx, sess, sess, sess.Stderr()); err != nil {
135
			s.gitError(sess, internalServerErrMsg, err)
136
			return
137
		}
138
139
		sess.Exit(0)
140
141
	default:
142
		s.error(sess, badRequestErrMsg, nil)
143
		return
144
	}
145
}
146
147
func (s *Server) isAuthorized(iden gossh.PublicKey) bool {
148
	return slices.ContainsFunc(s.authKeys, func(i gossh.PublicKey) bool {
149
		return ssh.KeysEqual(iden, i)
150
	})
151
}
152
153
func (s *Server) parseAuthKeys() error {
154
	parsedKeys := make([]gossh.PublicKey, len(s.c.SSH.Keys))
155
	for i, key := range s.c.SSH.Keys {
156
		pkey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(key))
157
		if err != nil {
158
			return err
159
		}
160
		parsedKeys[i] = pkey
161
	}
162
	s.authKeys = parsedKeys
163
	return nil
164
}
165
166
const (
167
	internalServerErrMsg = "internal server error\n"
168
	badRequestErrMsg     = "bad request\n"
169
	unauthorizedErrMsg   = "pushing only allowed to authorized users\n"
170
	repoNotFoundErrMsg   = "repository not found\n"
171
)
172
173
func (s *Server) error(sess ssh.Session, msg string, err error) {
174
	slog.Error("ssh error", "msg", msg, "err", err)
175
	fmt.Fprintf(sess.Stderr(), "%s", msg)
176
	sess.Exit(1)
177
}
178
179
func (s *Server) gitError(sess ssh.Session, msg string, err error) {
180
	slog.Error("ssh git error", "msg", msg, "err", err)
181
	git.PackError(sess, msg)
182
	git.PackFlush(sess)
183
	sess.Exit(1)
184
}