all repos

mugit @ 9c4890a208ab26eb80734a2494c8985e1e91069e

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
git: support git archive --remote, 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-upload-archive":
111
		isPrivate, err := repo.IsPrivate()
112
		if err != nil {
113
			s.gitError(sess, badRequestErrMsg, nil)
114
			return
115
		}
116
117
		if isPrivate && !authorized {
118
			s.gitError(sess, badRequestErrMsg, nil)
119
			return
120
		}
121
122
		if err := gitx.UploadArchive(ctx, repoPath, sess, sess); err != nil {
123
			s.gitError(sess, internalServerErrMsg, err)
124
			return
125
		}
126
127
		sess.Exit(0)
128
129
	case "git-receive-pack":
130
		if !authorized {
131
			s.gitError(sess, unauthorizedErrMsg, nil)
132
			return
133
		}
134
135
		if err := gitx.ReceivePack(ctx, repoPath, sess, sess, sess.Stderr()); err != nil {
136
			s.gitError(sess, internalServerErrMsg, err)
137
			return
138
		}
139
140
		sess.Exit(0)
141
142
	default:
143
		s.error(sess, badRequestErrMsg, nil)
144
		return
145
	}
146
}
147
148
func (s *Server) isAuthorized(iden gossh.PublicKey) bool {
149
	return slices.ContainsFunc(s.authKeys, func(i gossh.PublicKey) bool {
150
		return ssh.KeysEqual(iden, i)
151
	})
152
}
153
154
func (s *Server) parseAuthKeys() error {
155
	parsedKeys := make([]gossh.PublicKey, len(s.c.SSH.Keys))
156
	for i, key := range s.c.SSH.Keys {
157
		pkey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(key))
158
		if err != nil {
159
			return err
160
		}
161
		parsedKeys[i] = pkey
162
	}
163
	s.authKeys = parsedKeys
164
	return nil
165
}
166
167
const (
168
	internalServerErrMsg = "internal server error\n"
169
	badRequestErrMsg     = "bad request\n"
170
	unauthorizedErrMsg   = "pushing only allowed to authorized users\n"
171
	repoNotFoundErrMsg   = "repository not found\n"
172
)
173
174
func (s *Server) error(sess ssh.Session, msg string, err error) {
175
	slog.Error("ssh error", "msg", msg, "err", err)
176
	fmt.Fprintf(sess.Stderr(), "%s", msg)
177
	sess.Exit(1)
178
}
179
180
func (s *Server) gitError(sess ssh.Session, msg string, err error) {
181
	slog.Error("ssh git error", "msg", msg, "err", err)
182
	gitx.PackError(sess, msg)
183
	gitx.PackFlush(sess)
184
	sess.Exit(1)
185
}