diff options
Diffstat (limited to 'go/internal/command/commandargs')
-rw-r--r-- | go/internal/command/commandargs/command_args.go | 108 | ||||
-rw-r--r-- | go/internal/command/commandargs/command_args_test.go | 128 | ||||
-rw-r--r-- | go/internal/command/commandargs/generic_args.go | 14 | ||||
-rw-r--r-- | go/internal/command/commandargs/shell.go | 131 |
4 files changed, 243 insertions, 138 deletions
diff --git a/go/internal/command/commandargs/command_args.go b/go/internal/command/commandargs/command_args.go index d8fe32d..5338d6b 100644 --- a/go/internal/command/commandargs/command_args.go +++ b/go/internal/command/commandargs/command_args.go @@ -1,111 +1,27 @@ package commandargs import ( - "errors" - "os" - "regexp" - - "github.com/mattn/go-shellwords" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/executable" ) type CommandType string -const ( - Discover CommandType = "discover" - TwoFactorRecover CommandType = "2fa_recovery_codes" - LfsAuthenticate CommandType = "git-lfs-authenticate" - ReceivePack CommandType = "git-receive-pack" - UploadPack CommandType = "git-upload-pack" - UploadArchive CommandType = "git-upload-archive" -) - -var ( - whoKeyRegex = regexp.MustCompile(`\bkey-(?P<keyid>\d+)\b`) - whoUsernameRegex = regexp.MustCompile(`\busername-(?P<username>\S+)\b`) -) - -type CommandArgs struct { - GitlabUsername string - GitlabKeyId string - SshArgs []string - CommandType CommandType -} - -func Parse(arguments []string) (*CommandArgs, error) { - if sshConnection := os.Getenv("SSH_CONNECTION"); sshConnection == "" { - return nil, errors.New("Only ssh allowed") - } - - args := &CommandArgs{} - args.parseWho(arguments) - - if err := args.parseCommand(os.Getenv("SSH_ORIGINAL_COMMAND")); err != nil { - return nil, errors.New("Invalid ssh command") - } - args.defineCommandType() - - return args, nil -} - -func (c *CommandArgs) parseWho(arguments []string) { - for _, argument := range arguments { - if keyId := tryParseKeyId(argument); keyId != "" { - c.GitlabKeyId = keyId - break - } - - if username := tryParseUsername(argument); username != "" { - c.GitlabUsername = username - break - } - } +type CommandArgs interface { + Parse() error + GetArguments() []string } -func tryParseKeyId(argument string) string { - matchInfo := whoKeyRegex.FindStringSubmatch(argument) - if len(matchInfo) == 2 { - // The first element is the full matched string - // The second element is the named `keyid` - return matchInfo[1] - } - - return "" -} +func Parse(e *executable.Executable, arguments []string) (CommandArgs, error) { + var args CommandArgs = &GenericArgs{Arguments: arguments} -func tryParseUsername(argument string) string { - matchInfo := whoUsernameRegex.FindStringSubmatch(argument) - if len(matchInfo) == 2 { - // The first element is the full matched string - // The second element is the named `username` - return matchInfo[1] + switch e.Name { + case executable.GitlabShell: + args = &Shell{Arguments: arguments} } - return "" -} - -func (c *CommandArgs) parseCommand(commandString string) error { - args, err := shellwords.Parse(commandString) - if err != nil { - return err + if err := args.Parse(); err != nil { + return nil, 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...) - } - - c.SshArgs = args - - return nil -} - -func (c *CommandArgs) defineCommandType() { - if len(c.SshArgs) == 0 { - c.CommandType = Discover - } else { - c.CommandType = CommandType(c.SshArgs[0]) - } + return args, nil } diff --git a/go/internal/command/commandargs/command_args_test.go b/go/internal/command/commandargs/command_args_test.go index e60bb92..148c987 100644 --- a/go/internal/command/commandargs/command_args_test.go +++ b/go/internal/command/commandargs/command_args_test.go @@ -3,100 +3,127 @@ package commandargs import ( "testing" - "github.com/stretchr/testify/require" - + "gitlab.com/gitlab-org/gitlab-shell/go/internal/executable" "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" + + "github.com/stretchr/testify/require" ) func TestParseSuccess(t *testing.T) { testCases := []struct { desc string - arguments []string + executable *executable.Executable environment map[string]string - expectedArgs *CommandArgs + arguments []string + expectedArgs CommandArgs }{ // Setting the used env variables for every case to ensure we're // not using anything set in the original env. { - desc: "It sets discover as the command when the command string was empty", + desc: "It sets discover as the command when the command string was empty", + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": "", }, - expectedArgs: &CommandArgs{SshArgs: []string{}, CommandType: Discover}, + arguments: []string{}, + expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{}, CommandType: Discover}, }, { - desc: "It finds the key id in any passed arguments", + desc: "It finds the key id in any passed arguments", + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": "", }, arguments: []string{"hello", "key-123"}, - expectedArgs: &CommandArgs{SshArgs: []string{}, CommandType: Discover, GitlabKeyId: "123"}, + expectedArgs: &Shell{Arguments: []string{"hello", "key-123"}, SshArgs: []string{}, CommandType: Discover, GitlabKeyId: "123"}, }, { - desc: "It finds the username in any passed arguments", + desc: "It finds the username in any passed arguments", + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": "", }, arguments: []string{"hello", "username-jane-doe"}, - expectedArgs: &CommandArgs{SshArgs: []string{}, CommandType: Discover, GitlabUsername: "jane-doe"}, + expectedArgs: &Shell{Arguments: []string{"hello", "username-jane-doe"}, SshArgs: []string{}, CommandType: Discover, GitlabUsername: "jane-doe"}, }, { - desc: "It parses 2fa_recovery_codes command", + desc: "It parses 2fa_recovery_codes command", + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": "2fa_recovery_codes", }, - expectedArgs: &CommandArgs{SshArgs: []string{"2fa_recovery_codes"}, CommandType: TwoFactorRecover}, + arguments: []string{}, + expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"2fa_recovery_codes"}, CommandType: TwoFactorRecover}, }, { - desc: "It parses git-receive-pack command", + desc: "It parses git-receive-pack command", + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": "git-receive-pack group/repo", }, - expectedArgs: &CommandArgs{SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, + arguments: []string{}, + expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, }, { - desc: "It parses git-receive-pack command and a project with single quotes", + desc: "It parses git-receive-pack command and a project with single quotes", + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": "git receive-pack 'group/repo'", }, - expectedArgs: &CommandArgs{SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, + arguments: []string{}, + expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, }, { - desc: `It parses "git receive-pack" command`, + desc: `It parses "git receive-pack" command`, + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": `git receive-pack "group/repo"`, }, - expectedArgs: &CommandArgs{SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, + arguments: []string{}, + expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, }, { - desc: `It parses a command followed by control characters`, + desc: `It parses a command followed by control characters`, + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": `git-receive-pack group/repo; any command`, }, - expectedArgs: &CommandArgs{SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, + arguments: []string{}, + expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, }, { - desc: "It parses git-upload-pack command", + desc: "It parses git-upload-pack command", + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": `git upload-pack "group/repo"`, }, - expectedArgs: &CommandArgs{SshArgs: []string{"git-upload-pack", "group/repo"}, CommandType: UploadPack}, + arguments: []string{}, + expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-upload-pack", "group/repo"}, CommandType: UploadPack}, }, { - desc: "It parses git-upload-archive command", + desc: "It parses git-upload-archive command", + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": "git-upload-archive 'group/repo'", }, - expectedArgs: &CommandArgs{SshArgs: []string{"git-upload-archive", "group/repo"}, CommandType: UploadArchive}, + arguments: []string{}, + expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-upload-archive", "group/repo"}, CommandType: UploadArchive}, }, { - desc: "It parses git-lfs-authenticate command", + desc: "It parses git-lfs-authenticate command", + executable: &executable.Executable{Name: executable.GitlabShell}, environment: map[string]string{ "SSH_CONNECTION": "1", "SSH_ORIGINAL_COMMAND": "git-lfs-authenticate 'group/repo' download", }, - expectedArgs: &CommandArgs{SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}, CommandType: LfsAuthenticate}, + arguments: []string{}, + expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}, CommandType: LfsAuthenticate}, + }, { + desc: "Unknown executable", + executable: &executable.Executable{Name: "unknown"}, + arguments: []string{}, + expectedArgs: &GenericArgs{Arguments: []string{}}, }, } @@ -105,7 +132,7 @@ func TestParseSuccess(t *testing.T) { restoreEnv := testhelper.TempEnv(tc.environment) defer restoreEnv() - result, err := Parse(tc.arguments) + result, err := Parse(tc.executable, tc.arguments) require.NoError(t, err) require.Equal(t, tc.expectedArgs, result) @@ -114,22 +141,39 @@ func TestParseSuccess(t *testing.T) { } func TestParseFailure(t *testing.T) { - t.Run("It fails if SSH connection is not set", func(t *testing.T) { - _, err := Parse([]string{}) - - require.Error(t, err, "Only ssh allowed") - }) + testCases := []struct { + desc string + executable *executable.Executable + environment map[string]string + arguments []string + expectedError string + }{ + { + desc: "It fails if SSH connection is not set", + executable: &executable.Executable{Name: executable.GitlabShell}, + arguments: []string{}, + expectedError: "Only SSH allowed", + }, + { + desc: "It fails if SSH command is invalid", + executable: &executable.Executable{Name: executable.GitlabShell}, + environment: map[string]string{ + "SSH_CONNECTION": "1", + "SSH_ORIGINAL_COMMAND": `git receive-pack "`, + }, + arguments: []string{}, + expectedError: "Invalid SSH allowed", + }, + } - t.Run("It fails if SSH command is invalid", func(t *testing.T) { - environment := map[string]string{ - "SSH_CONNECTION": "1", - "SSH_ORIGINAL_COMMAND": `git receive-pack "`, - } - restoreEnv := testhelper.TempEnv(environment) - defer restoreEnv() + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + restoreEnv := testhelper.TempEnv(tc.environment) + defer restoreEnv() - _, err := Parse([]string{}) + _, err := Parse(tc.executable, tc.arguments) - require.Error(t, err, "Invalid ssh command") - }) + require.Error(t, err, tc.expectedError) + }) + } } diff --git a/go/internal/command/commandargs/generic_args.go b/go/internal/command/commandargs/generic_args.go new file mode 100644 index 0000000..96bed99 --- /dev/null +++ b/go/internal/command/commandargs/generic_args.go @@ -0,0 +1,14 @@ +package commandargs + +type GenericArgs struct { + Arguments []string +} + +func (b *GenericArgs) Parse() error { + // Do nothing + return nil +} + +func (b *GenericArgs) GetArguments() []string { + return b.Arguments +} diff --git a/go/internal/command/commandargs/shell.go b/go/internal/command/commandargs/shell.go new file mode 100644 index 0000000..7e2b72e --- /dev/null +++ b/go/internal/command/commandargs/shell.go @@ -0,0 +1,131 @@ +package commandargs + +import ( + "errors" + "os" + "regexp" + + "github.com/mattn/go-shellwords" +) + +const ( + Discover CommandType = "discover" + TwoFactorRecover CommandType = "2fa_recovery_codes" + LfsAuthenticate CommandType = "git-lfs-authenticate" + ReceivePack CommandType = "git-receive-pack" + UploadPack CommandType = "git-upload-pack" + UploadArchive CommandType = "git-upload-archive" +) + +var ( + whoKeyRegex = regexp.MustCompile(`\bkey-(?P<keyid>\d+)\b`) + whoUsernameRegex = regexp.MustCompile(`\busername-(?P<username>\S+)\b`) +) + +type Shell struct { + Arguments []string + GitlabUsername string + GitlabKeyId string + SshArgs []string + CommandType CommandType +} + +func (s *Shell) Parse() error { + if err := s.validate(); err != nil { + return err + } + + s.parseWho() + s.defineCommandType() + + return nil +} + +func (s *Shell) GetArguments() []string { + return s.Arguments +} + +func (s *Shell) validate() error { + if !s.isSshConnection() { + return errors.New("Only SSH allowed") + } + + if !s.isValidSshCommand() { + return errors.New("Invalid SSH command") + } + + return nil +} + +func (s *Shell) isSshConnection() bool { + ok := os.Getenv("SSH_CONNECTION") + return ok != "" +} + +func (s *Shell) isValidSshCommand() bool { + err := s.parseCommand(os.Getenv("SSH_ORIGINAL_COMMAND")) + return err == 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 tryParseKeyId(argument string) string { + matchInfo := whoKeyRegex.FindStringSubmatch(argument) + if len(matchInfo) == 2 { + // The first element is the full matched string + // The second element is the named `keyid` + return matchInfo[1] + } + + return "" +} + +func tryParseUsername(argument string) string { + matchInfo := whoUsernameRegex.FindStringSubmatch(argument) + if len(matchInfo) == 2 { + // The first element is the full matched string + // The second element is the named `username` + return matchInfo[1] + } + + return "" +} + +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 + + return nil +} + +func (s *Shell) defineCommandType() { + if len(s.SshArgs) == 0 { + s.CommandType = Discover + } else { + s.CommandType = CommandType(s.SshArgs[0]) + } +} |