diff --git a/pkg/device/genericcli/genericcli.go b/pkg/device/genericcli/genericcli.go index 24c4b2b..a48f900 100644 --- a/pkg/device/genericcli/genericcli.go +++ b/pkg/device/genericcli/genericcli.go @@ -463,17 +463,22 @@ func (m *GenericDevice) SetCLIConnectTimeout(timeout time.Duration) time.Duratio return m.cli.SetConnectTimeout(timeout) } +func writeLoginLine(connector streamer.Connector, cli GenericCLI, payload []byte) error { + if err := connector.Write(payload); err != nil { + return err + } + if len(cli.writeNewline) > 0 { + if err := connector.Write(cli.writeNewline); err != nil { + return fmt.Errorf("write error %w", err) + } + } + return nil +} + func genericLogin(ctx context.Context, connector streamer.Connector, cli GenericCLI) (err error) { if cli.login == nil { return errors.New("login Expr is not set but required for login procedure") } - - passwords := connector.GetCredentials().GetPasswords(ctx) - if len(passwords) == 0 { - return errors.New("empty password") - } - - i := 0 checkExprs := []expr.NamedExpr{ {Name: loginExprName, Exprs: []expr.Expr{cli.login}}, {Name: passwordExprName, Exprs: []expr.Expr{cli.password}}, @@ -491,7 +496,10 @@ func genericLogin(ctx context.Context, connector streamer.Connector, cli Generic }) } - for i < len(passwords) { + passwords := connector.GetCredentials().GetPasswords(ctx) + passwordIdx := 0 + + for { exprsLogin := expr.NewSimpleExprListNamedOrdered(checkExprs) readResLogin, err := connector.ReadTo(ctx, exprsLogin) if err != nil { @@ -505,36 +513,27 @@ func genericLogin(ctx context.Context, connector streamer.Connector, cli Generic if err != nil { return err } - - err = connector.Write([]byte(username)) - if err != nil { + if err := writeLoginLine(connector, cli, []byte(username)); err != nil { return err } - newline := cli.writeNewline - if len(newline) > 0 { - err := connector.Write(newline) - if err != nil { - return fmt.Errorf("write error %w", err) + + case passwordExprName: + if passwordIdx >= len(passwords) { + if len(passwords) == 0 { + return fmt.Errorf("no passwords supplied") } + return gerror.NewAuthException("cli auth user") } - case passwordExprName: - err = connector.Write([]byte(passwords[i].Value())) - if err != nil { + password := []byte(passwords[passwordIdx].Value()) + if err := writeLoginLine(connector, cli, password); err != nil { return err } - newline := cli.writeNewline - if len(newline) > 0 { - err := connector.Write(newline) - if err != nil { - return fmt.Errorf("write error %w", err) - } + passwordIdx++ + case passwdErrExprName: + if passwordIdx >= len(passwords) { + return gerror.NewAuthException("cli auth user") } - i++ - case passwdErrExprName: - continue - case promptExprName: - return nil case cbExprName: var ans []byte for _, v := range cli.loginCB { @@ -547,21 +546,12 @@ func genericLogin(ctx context.Context, connector streamer.Connector, cli Generic if err != nil { return fmt.Errorf("write error %w", err) } - } - } - exprs := expr.NewSimpleExprListNamedOrdered(checkExprs) - readResLogin, err := connector.ReadTo(ctx, exprs) - if err != nil { - return err - } - matchedExprNameLogin := exprs.GetName(readResLogin.GetPatternNo()) - if matchedExprNameLogin == promptExprName { - return nil + case promptExprName: + // got prompt (logged in) + return nil + } } - - return gerror.NewAuthException("cli auth user") - } func GenericExecute(command cmd.Cmd, connector streamer.Connector, cli GenericCLI, logger *zap.Logger) (cmd.CmdRes, error) { diff --git a/pkg/device/genericcli/login_test.go b/pkg/device/genericcli/login_test.go new file mode 100644 index 0000000..c8a1332 --- /dev/null +++ b/pkg/device/genericcli/login_test.go @@ -0,0 +1,173 @@ +package genericcli + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + + gmock "github.com/annetutil/gnetcli/pkg/testutils/mock" + + "github.com/annetutil/gnetcli/pkg/credentials" + "github.com/annetutil/gnetcli/pkg/expr" + "github.com/annetutil/gnetcli/pkg/gerror" + "github.com/annetutil/gnetcli/pkg/streamer/ssh" +) + +const ( + loginPromptExpression = `(\r\n|^)(?P(<\w+>))$` + loginErrorExpression = `(\r\n|^)Error: .+$` + loginUsernameExpression = `.*Username:$` + loginPasswordExpression = `.*Password:$` + loginPasswordErrExpression = `.*Authentication failed(\r\n|\n)$` +) + +func runManualLogin(t *testing.T, dialog []gmock.Action, creds credentials.Credentials) error { + t.Helper() + logger := zap.Must(zap.NewDevelopmentConfig().Build()) + + sshServer, err := gmock.NewMockSSHServer(dialog, gmock.WithLogger(logger)) + require.NoError(t, err) + + ctx := context.Background() + g := new(errgroup.Group) + g.Go(func() error { + return sshServer.Run(ctx) + }) + + host, port := sshServer.GetAddress() + connector := ssh.NewStreamer(host, creds, ssh.WithPort(port), ssh.WithLogger(logger)) + cli := MakeGenericCLI( + expr.NewSimpleExprLast200().FromPattern(loginPromptExpression), + expr.NewSimpleExprLast200().FromPattern(loginErrorExpression), + WithLoginExprs( + expr.NewSimpleExprLast200().FromPattern(loginUsernameExpression), + expr.NewSimpleExprLast200().FromPattern(loginPasswordExpression), + expr.NewSimpleExprLast200().FromPattern(loginPasswordErrExpression), + ), + WithManualAuth(), + ) + dev := MakeGenericDevice(cli, connector, WithDevLogger(logger)) + + connCtx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + require.NoError(t, dev.Connect(connCtx)) + + err = dev.connectCLI(connCtx) + dev.Close() + _ = g.Wait() + return err +} + +func TestManualLoginPasswordFirstTry(t *testing.T) { + dialog := gmock.ConcatMultipleSlices([][]gmock.Action{ + { + gmock.Send("\r\nPassword:"), + gmock.Expect("p4$$М0rD!\n"), + gmock.Send("\r\n"), + }, + }) + creds := credentials.NewSimpleCredentials( + credentials.WithUsername("admin"), + credentials.WithPassword("p4$$М0rD!"), + ) + + err := runManualLogin(t, dialog, creds) + require.NoError(t, err) +} + +func TestManualLoginUsernameAndPassword(t *testing.T) { + dialog := gmock.ConcatMultipleSlices([][]gmock.Action{ + { + gmock.Send("\r\nUsername:"), + gmock.Expect("admin\n"), + gmock.Send("\r\nPassword:"), + gmock.Expect("p4$$М0rD!\n"), + gmock.Send("\r\n"), + }, + }) + creds := credentials.NewSimpleCredentials( + credentials.WithUsername("admin"), + credentials.WithPassword("p4$$М0rD!"), + ) + + err := runManualLogin(t, dialog, creds) + require.NoError(t, err) +} + +func TestManualLoginPasswordRetry(t *testing.T) { + dialog := gmock.ConcatMultipleSlices([][]gmock.Action{ + { + gmock.Send("\r\nPassword:"), + gmock.Expect("p4$$М0rD!\n"), + gmock.Send("\r\nAuthentication failed\r\n"), + gmock.Send("\r\nPassword:"), + gmock.Expect("admin\n"), + gmock.Send("\r\n"), + }, + }) + creds := credentials.NewSimpleCredentials( + credentials.WithUsername("admin"), + credentials.WithPasswords([]credentials.Secret{"p4$$М0rD!", "admin"}), + ) + + err := runManualLogin(t, dialog, creds) + require.NoError(t, err) +} + +func TestManualLoginAllPasswordsRejected(t *testing.T) { + dialog := gmock.ConcatMultipleSlices([][]gmock.Action{ + { + gmock.Send("\r\nPassword:"), + gmock.Expect("p4$$М0rD!\n"), + gmock.Send("\r\nAuthentication failed\r\n"), + gmock.Send("\r\nPassword:"), + gmock.Expect("admin\n"), + gmock.Send("\r\nAuthentication failed\r\n"), + gmock.Close(), + }, + }) + creds := credentials.NewSimpleCredentials( + credentials.WithUsername("admin"), + credentials.WithPasswords([]credentials.Secret{"p4$$М0rD!", "admin"}), + ) + + err := runManualLogin(t, dialog, creds) + require.Error(t, err) + var authErr *gerror.AuthException + require.ErrorAs(t, err, &authErr, "") + require.EqualError(t, err, "auth error cli auth user") +} + +func TestManualLoginEmptyPasswordList(t *testing.T) { + dialog := gmock.ConcatMultipleSlices([][]gmock.Action{ + { + gmock.Send("\r\nPassword:"), + }, + }) + creds := credentials.NewSimpleCredentials( + credentials.WithUsername("admin"), + ) + + err := runManualLogin(t, dialog, creds) + require.EqualError(t, err, "no passwords supplied") +} + +func TestManualLoginNoPassword(t *testing.T) { + dialog := gmock.ConcatMultipleSlices([][]gmock.Action{ + { + gmock.Send("\r\nUsername:"), + gmock.Expect("admin\n"), + gmock.Send("\r\n"), + }, + }) + creds := credentials.NewSimpleCredentials( + credentials.WithUsername("admin"), + ) + + err := runManualLogin(t, dialog, creds) + require.NoError(t, err) +}