1package commandargs 2 3import ( 4 "fmt" 5 "regexp" 6 "strings" 7 8 "github.com/mattn/go-shellwords" 9 "gitlab.com/gitlab-org/gitlab-shell/internal/sshenv" 10) 11 12const ( 13 Discover CommandType = "discover" 14 TwoFactorRecover CommandType = "2fa_recovery_codes" 15 TwoFactorVerify CommandType = "2fa_verify" 16 LfsAuthenticate CommandType = "git-lfs-authenticate" 17 ReceivePack CommandType = "git-receive-pack" 18 UploadPack CommandType = "git-upload-pack" 19 UploadArchive CommandType = "git-upload-archive" 20 PersonalAccessToken CommandType = "personal_access_token" 21) 22 23var ( 24 whoKeyRegex = regexp.MustCompile(`\Akey-(?P<keyid>\d+)\z`) 25 whoUsernameRegex = regexp.MustCompile(`\Ausername-(?P<username>\S+)\z`) 26) 27 28type Shell struct { 29 Arguments []string 30 GitlabUsername string 31 GitlabKeyId string 32 SshArgs []string 33 CommandType CommandType 34 Env sshenv.Env 35} 36 37func (s *Shell) Parse() error { 38 if err := s.validate(); err != nil { 39 return err 40 } 41 42 s.parseWho() 43 44 return nil 45} 46 47func (s *Shell) GetArguments() []string { 48 return s.Arguments 49} 50 51func (s *Shell) validate() error { 52 if !s.Env.IsSSHConnection { 53 return fmt.Errorf("Only SSH allowed") 54 } 55 56 if err := s.ParseCommand(s.Env.OriginalCommand); err != nil { 57 return fmt.Errorf("Invalid SSH command: %w", err) 58 } 59 60 return nil 61} 62 63func (s *Shell) parseWho() { 64 for _, argument := range s.Arguments { 65 if keyId := tryParseKeyId(argument); keyId != "" { 66 s.GitlabKeyId = keyId 67 break 68 } 69 70 if username := tryParseUsername(argument); username != "" { 71 s.GitlabUsername = username 72 break 73 } 74 } 75} 76 77func tryParse(r *regexp.Regexp, argument string) string { 78 // sshd may execute the session for AuthorizedKeysCommand in multiple ways: 79 // 1. key-id 80 // 2. /path/to/shell -c key-id 81 args := strings.Split(argument, " ") 82 lastArg := args[len(args)-1] 83 84 matchInfo := r.FindStringSubmatch(lastArg) 85 if len(matchInfo) == 2 { 86 // The first element is the full matched string 87 // The second element is the named `keyid` or `username` 88 return matchInfo[1] 89 } 90 91 return "" 92} 93 94func tryParseKeyId(argument string) string { 95 return tryParse(whoKeyRegex, argument) 96} 97 98func tryParseUsername(argument string) string { 99 return tryParse(whoUsernameRegex, argument) 100} 101 102func (s *Shell) ParseCommand(commandString string) error { 103 args, err := shellwords.Parse(commandString) 104 if err != nil { 105 return err 106 } 107 108 // Handle Git for Windows 2.14 using "git upload-pack" instead of git-upload-pack 109 if len(args) > 1 && args[0] == "git" { 110 command := args[0] + "-" + args[1] 111 commandArgs := args[2:] 112 113 args = append([]string{command}, commandArgs...) 114 } 115 116 s.SshArgs = args 117 118 s.defineCommandType() 119 120 return nil 121} 122 123func (s *Shell) defineCommandType() { 124 if len(s.SshArgs) == 0 { 125 s.CommandType = Discover 126 } else { 127 s.CommandType = CommandType(s.SshArgs[0]) 128 } 129} 130