summaryrefslogtreecommitdiff
path: root/internal/command/commandargs/shell.go
blob: 0b1e161bef6ceb1cc58178e4bfddf2f294b54f58 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package commandargs

import (
	"fmt"
	"regexp"
	"strings"

	"github.com/mattn/go-shellwords"
	"gitlab.com/gitlab-org/gitlab-shell/v14/internal/sshenv"
)

const (
	Discover            CommandType = "discover"
	TwoFactorRecover    CommandType = "2fa_recovery_codes"
	TwoFactorVerify     CommandType = "2fa_verify"
	LfsAuthenticate     CommandType = "git-lfs-authenticate"
	ReceivePack         CommandType = "git-receive-pack"
	UploadPack          CommandType = "git-upload-pack"
	UploadArchive       CommandType = "git-upload-archive"
	PersonalAccessToken CommandType = "personal_access_token"
)

var (
	whoKeyRegex      = regexp.MustCompile(`\Akey-(?P<keyid>\d+)\z`)
	whoUsernameRegex = regexp.MustCompile(`\Ausername-(?P<username>\S+)\z`)
)

type Shell struct {
	Arguments      []string
	GitlabUsername string
	GitlabKeyId    string
	SshArgs        []string
	CommandType    CommandType
	Env            sshenv.Env
}

func (s *Shell) Parse() error {
	if err := s.validate(); err != nil {
		return err
	}

	s.parseWho()

	return nil
}

func (s *Shell) GetArguments() []string {
	return s.Arguments
}

func (s *Shell) validate() error {
	if !s.Env.IsSSHConnection {
		return fmt.Errorf("Only SSH allowed")
	}

	if err := s.ParseCommand(s.Env.OriginalCommand); err != nil {
		return fmt.Errorf("Invalid SSH command: %w", err)
	}

	return nil
}

func (s *Shell) parseWho() {
	for _, argument := range s.Arguments {
		if keyId := tryParseKeyId(argument); keyId != "" {
			s.GitlabKeyId = keyId
			break
		}

		if username := tryParseUsername(argument); username != "" {
			s.GitlabUsername = username
			break
		}
	}
}

func tryParse(r *regexp.Regexp, argument string) string {
	// sshd may execute the session for AuthorizedKeysCommand in multiple ways:
	// 1. key-id
	// 2. /path/to/shell -c key-id
	args := strings.Split(argument, " ")
	lastArg := args[len(args)-1]

	matchInfo := r.FindStringSubmatch(lastArg)
	if len(matchInfo) == 2 {
		// The first element is the full matched string
		// The second element is the named `keyid` or `username`
		return matchInfo[1]
	}

	return ""
}

func tryParseKeyId(argument string) string {
	return tryParse(whoKeyRegex, argument)
}

func tryParseUsername(argument string) string {
	return tryParse(whoUsernameRegex, argument)
}

func (s *Shell) ParseCommand(commandString string) error {
	args, err := shellwords.Parse(commandString)
	if err != nil {
		return err
	}

	// Handle Git for Windows 2.14 using "git upload-pack" instead of git-upload-pack
	if len(args) > 1 && args[0] == "git" {
		command := args[0] + "-" + args[1]
		commandArgs := args[2:]

		args = append([]string{command}, commandArgs...)
	}

	s.SshArgs = args

	s.defineCommandType()

	return nil
}

func (s *Shell) defineCommandType() {
	if len(s.SshArgs) == 0 {
		s.CommandType = Discover
	} else {
		s.CommandType = CommandType(s.SshArgs[0])
	}
}