package ssh import ( "context" "fmt" "net" "time" "git.tukangketik.id/swanadiva/hostkeeper/internal/errors" "git.tukangketik.id/swanadiva/hostkeeper/internal/models" cryptossh "golang.org/x/crypto/ssh" ) // Client represents an SSH client type Client struct { host *models.Host timeout time.Duration client *cryptossh.Client config *cryptossh.ClientConfig } // NewClient creates a new SSH client func NewClient(host *models.Host, timeout time.Duration) *Client { return &Client{ host: host, timeout: timeout, } } // Connect establishes an SSH connection func (c *Client) Connect(ctx context.Context) error { // Create SSH configuration if err := c.setupConfig(); err != nil { return fmt.Errorf("failed to setup SSH config: %w", err) } // Create connection context with timeout connCtx, cancel := context.WithTimeout(ctx, c.timeout) defer cancel() // Establish TCP connection address := fmt.Sprintf("%s:%d", c.host.Hostname, c.host.Port) conn, err := c.dialTCP(connCtx, address) if err != nil { return errors.HandleSSHError(err) } // Establish SSH connection over TCP sshConn, chans, reqs, err := cryptossh.NewClientConn(conn, address, c.config) if err != nil { conn.Close() return errors.HandleSSHError(err) } c.client = cryptossh.NewClient(sshConn, chans, reqs) return nil } // dialTCP establishes a TCP connection func (c *Client) dialTCP(ctx context.Context, address string) (net.Conn, error) { d := net.Dialer{} return d.DialContext(ctx, "tcp", address) } // setupConfig creates SSH client configuration func (c *Client) setupConfig() error { config := &cryptossh.ClientConfig{ User: c.host.Username, HostKeyCallback: cryptossh.InsecureIgnoreHostKey(), //nolint:gosec // Phase 1 - will be improved in Phase 2 Timeout: c.timeout, } // Configure authentication methods authMethods, err := c.getAuthMethods() if err != nil { return fmt.Errorf("failed to setup authentication: %w", err) } config.Auth = authMethods c.config = config return nil } // Execute runs a command on the remote server func (c *Client) Execute(_ context.Context, cmd string) (string, error) { if c.client == nil { return "", fmt.Errorf("not connected to server") } session, err := c.client.NewSession() if err != nil { return "", fmt.Errorf("failed to create session: %w", err) } defer session.Close() output, err := session.CombinedOutput(cmd) if err != nil { return string(output), fmt.Errorf("command execution failed: %w", err) } return string(output), nil } // Close closes the SSH connection func (c *Client) Close() error { if c.client != nil { return c.client.Close() } return nil } // GetClient returns the underlying SSH client func (c *Client) GetClient() *cryptossh.Client { return c.client } // IsConnected returns true if the client has an active connection func (c *Client) IsConnected() bool { return c.client != nil }