all repos

mugit @ 46e2219

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
ssh: dont open repo on invalid commands, 1 month ago
1
package ssh
2
3
import (
4
	"context"
5
	"fmt"
6
	"io"
7
	"log/slog"
8
	"strings"
9
10
	"olexsmir.xyz/mugit/internal/config"
11
	"olexsmir.xyz/mugit/internal/git"
12
13
	gossh "golang.org/x/crypto/ssh"
14
)
15
16
type Shell struct {
17
	cfg *config.Config
18
19
	keys []gossh.PublicKey
20
}
21
22
func NewShell(cfg *config.Config) (*Shell, error) {
23
	parsedKeys := make([]gossh.PublicKey, len(cfg.SSH.Keys))
24
	for i, key := range cfg.SSH.Keys {
25
		pkey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(key))
26
		if err != nil {
27
			return nil, err
28
		}
29
		parsedKeys[i] = pkey
30
	}
31
32
	return &Shell{
33
		cfg:  cfg,
34
		keys: parsedKeys,
35
	}, nil
36
}
37
38
var validCommands = map[string]bool{
39
	"git-upload-pack":    true,
40
	"git-upload-archive": true,
41
	"git-receive-pack":   true,
42
}
43
44
func (s *Shell) HandleCommand(ctx context.Context, cmd string, stdin io.Reader, stdout, stderr io.Writer) error {
45
	gitCmd, repoName, err := s.parseCommand(cmd)
46
	if err != nil {
47
		slog.Error("ssh invalid command", "error", err, "raw_cmd", cmd)
48
		return err
49
	}
50
51
	if !validCommands[gitCmd] {
52
		slog.Error("access denied: invalid git command")
53
		return fmt.Errorf("access denied: invalid git command")
54
	}
55
56
	repoPath, err := git.ResolvePath(s.cfg.Repo.Dir, git.ResolveName(repoName))
57
	if err != nil {
58
		slog.Error("ssh access denied", "cmd", gitCmd, "repo", repoName, "error", err)
59
		return err
60
	}
61
62
	repo, err := git.Open(repoPath, "")
63
	if err != nil {
64
		slog.Error("ssh access denied", "cmd", gitCmd, "repo", repoName, "error", err)
65
		return err
66
	}
67
68
	switch gitCmd {
69
	case "git-upload-pack":
70
		err = repo.UploadPack(ctx, false, "", stdin, stdout)
71
	case "git-upload-archive":
72
		err = repo.UploadArchive(ctx, stdin, stdout)
73
	case "git-receive-pack":
74
		err = repo.ReceivePack(ctx, stdin, stdout, stderr)
75
	default:
76
		err = fmt.Errorf("access denied: invalid git command %q", gitCmd)
77
	}
78
79
	if err != nil {
80
		slog.Error("ssh operation failed", "cmd", gitCmd, "repo", repoName, "error", err)
81
	}
82
83
	return err
84
}
85
86
func (s *Shell) AuthorizedKeys(executablePath string) string {
87
	var out strings.Builder
88
	for _, key := range s.cfg.SSH.Keys {
89
		fmt.Fprintf(&out, `command="%s shell",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s`+"\n",
90
			executablePath, key)
91
	}
92
	return out.String()
93
}
94
95
func (s *Shell) parseCommand(cmd string) (gitCmd, repoName string, err error) {
96
	cmdParts := strings.Fields(cmd)
97
	if len(cmdParts) < 2 {
98
		return "", "", fmt.Errorf("invalid command: expected 'git-cmd repo', got %q", cmd)
99
	}
100
101
	gitCmd = cmdParts[0]
102
	repoName = strings.Trim(cmdParts[1], "'\"")
103
	if repoName == "" {
104
		return "", "", fmt.Errorf("invalid command: empty repository name")
105
	}
106
107
	return gitCmd, repoName, nil
108
}