all repos

mugit @ cc59ce4becd876f686c626f299c8969541c1d90f

🐮 git server that your cow will love

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

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