<commit_before>package ssh

import (
	"bytes"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"text/template"
	"time"

	"github.com/wallix/awless/logger"

	gossh "golang.org/x/crypto/ssh"
	agent "golang.org/x/crypto/ssh/agent"
	"golang.org/x/crypto/ssh/knownhosts"
	"golang.org/x/crypto/ssh/terminal"
)

type auth struct {
	desc       string
	keyPath    string
	authmethod gossh.AuthMethod
}

type configAndAuth struct {
	clientConfig *gossh.ClientConfig
	auth         auth
}

type Client struct {
	*gossh.Client
	auths                   []auth
	selectedAuth            auth
	IP, User                string
	Port                    int
	HostKeyCallback         gossh.HostKeyCallback
	StrictHostKeyChecking   bool
	InteractiveTerminalFunc func(*gossh.Client) error
	logger                  *logger.Logger
}

func InitClient(keyname string, keyFolders ...string) (*Client, error) {
	// ignore any errors resolving the private key
	// if it doesn't resolve it just won't be used
	privkey, _ := resolvePrivateKey(keyname, keyFolders...)

	auths, err := resolveAuths(privkey)
	if err != nil {
		return nil, err
	}

	cli := &Client{
		logger:                  logger.DiscardLogger,
		InteractiveTerminalFunc: func(*gossh.Client) error { return nil },
		auths: auths,
		StrictHostKeyChecking: true,
	}

	return cli, nil
}

func (c *Client) SetLogger(l *logger.Logger) {
	c.logger = l
}

func (c *Client) SetStrictHostKeyChecking(hostKeyChecking bool) {
	c.StrictHostKeyChecking = hostKeyChecking
}

func (c *Client) DialWithUsers(usernames ...string) (*Client, error) {
	var failures int
	var err error
	var client *gossh.Client

	authDescs := []string{}
	for _, am := range c.auths {
		authDescs = append(authDescs, am.desc)
	}
	c.logger.Infof("trying with username(s) [%s] and auth method(s) [%s]", strings.Join(usernames, ", "), strings.Join(authDescs, ", "))

	for _, user := range usernames {
		c.logger.Verbosef("trying with user %s", user)
		for _, configAndAuth := range c.buildConfigAndAuths(user) {
			c.logger.Verbosef("trying with config %v", configAndAuth.clientConfig)
			client, err = gossh.Dial("tcp", fmt.Sprintf("%s:%d", c.IP, c.Port), configAndAuth.clientConfig)
			if err == nil {
				c.User = user
				c.Client = client
				c.selectedAuth = configAndAuth.auth
				return c, nil
			}
			if err != nil && strings.Contains(err.Error(), "unable to authenticate") {
				failures++
				continue
			} else if err != nil {
				return c, err
			}
		}
	}

	return c, fmt.Errorf("with users %q: %s", usernames, err)
}

func (c *Client) Connect() error {
	defer func() {
		if c.Client != nil {
			c.Client.Close()
		}
	}()

	args, installed := c.localExec()
	if installed {
		c.logger.Infof("Login as '%s' on '%s', using %s for auth with ssh client '%s'\n", c.User, c.IP, c.selectedAuth.desc, args[0])
		return syscall.Exec(args[0], args, os.Environ())
	}

	c.logger.Infof("No SSH. Fallback on builtin client. Login as '%s' on '%s', using %s for auth\n", c.User, c.IP, c.selectedAuth.desc)
	return c.InteractiveTerminalFunc(c.Client)
}

func (c *Client) buildConfigAndAuths(username string) []configAndAuth {
	var cas []configAndAuth
	for _, auth := range c.auths {

		cAndA := configAndAuth{
			&gossh.ClientConfig{
				User:            username,
				Auth:            []gossh.AuthMethod{auth.authmethod},
				Timeout:         2 * time.Second,
				HostKeyCallback: gossh.InsecureIgnoreHostKey(),
			},
			auth,<commit_msg>ssh: improving & compacting logging info<commit_after>package ssh

import (
	"bytes"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"text/template"
	"time"

	"github.com/wallix/awless/logger"

	gossh "golang.org/x/crypto/ssh"
	agent "golang.org/x/crypto/ssh/agent"
	"golang.org/x/crypto/ssh/knownhosts"
	"golang.org/x/crypto/ssh/terminal"
)

type auth struct {
	desc       string
	keyPath    string
	authmethod gossh.AuthMethod
}

type configAndAuth struct {
	clientConfig *gossh.ClientConfig
	auth         auth
}

type Client struct {
	*gossh.Client
	auths                   []auth
	selectedAuth            auth
	IP, User                string
	Port                    int
	HostKeyCallback         gossh.HostKeyCallback
	StrictHostKeyChecking   bool
	InteractiveTerminalFunc func(*gossh.Client) error
	logger                  *logger.Logger
}

func InitClient(keyname string, keyFolders ...string) (*Client, error) {
	// ignore any errors resolving the private key
	// if it doesn't resolve it just won't be used
	privkey, _ := resolvePrivateKey(keyname, keyFolders...)

	auths, err := resolveAuths(privkey)
	if err != nil {
		return nil, err
	}

	cli := &Client{
		logger:                  logger.DiscardLogger,
		InteractiveTerminalFunc: func(*gossh.Client) error { return nil },
		auths: auths,
		StrictHostKeyChecking: true,
	}

	return cli, nil
}

func (c *Client) SetLogger(l *logger.Logger) {
	c.logger = l
}

func (c *Client) SetStrictHostKeyChecking(hostKeyChecking bool) {
	c.StrictHostKeyChecking = hostKeyChecking
}

func (c *Client) DialWithUsers(usernames ...string) (*Client, error) {
	var failures int
	var err error
	var client *gossh.Client

	authDescs := []string{}
	for _, am := range c.auths {
		authDescs = append(authDescs, am.desc)
	}
	c.logger.Infof("authentication methods found [%s]", strings.Join(authDescs, ", "))

	for _, user := range usernames {
		c.logger.Verbosef("trying with user %s", user)
		for _, configAndAuth := range c.buildConfigAndAuths(user) {
			c.logger.Verbosef("trying with config %v", configAndAuth.clientConfig)
			client, err = gossh.Dial("tcp", fmt.Sprintf("%s:%d", c.IP, c.Port), configAndAuth.clientConfig)
			if err == nil {
				c.User = user
				c.Client = client
				c.selectedAuth = configAndAuth.auth
				return c, nil
			}
			if err != nil && strings.Contains(err.Error(), "unable to authenticate") {
				failures++
				continue
			} else if err != nil {
				return c, err
			}
		}
	}

	return c, fmt.Errorf("with users %q: %s", usernames, err)
}

func (c *Client) Connect() error {
	defer func() {
		if c.Client != nil {
			c.Client.Close()
		}
	}()

	args, installed := c.localExec()
	if installed {
		c.logger.Infof("Login as '%s' on '%s'; auth with %s; client '%s'\n", c.User, c.IP, c.selectedAuth.desc, args[0])
		return syscall.Exec(args[0], args, os.Environ())
	}

	c.logger.Infof("No SSH. Fallback on builtin client. Login as '%s' on '%s', using %s for auth\n", c.User, c.IP, c.selectedAuth.desc)
	return c.InteractiveTerminalFunc(c.Client)
}

func (c *Client) buildConfigAndAuths(username string) []configAndAuth {
	var cas []configAndAuth
	for _, auth := range c.auths {

		cAndA := configAndAuth{
			&gossh.ClientConfig{
				User:            username,
				Auth:            []gossh.AuthMethod{auth.authmethod},
				Timeout:         2 * time.Second,
				HostKeyCallback: gossh.InsecureIgnoreHostKey(),
			},
			auth,
{'fraction_non_alphanumeric': 0.13394390383514596, 'fraction_numerical': 0.0017172295363480253, 'mean_word_length': 3.1875374475733973, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3245356', 'n_tokens_mistral': 2694, 'n_tokens_neox': 2475, 'n_words': 609}