package sshconn import ( "fmt" "io" "net" "os" "time" "git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model" gossh "golang.org/x/crypto/ssh" ) type Client struct { Host model.Host client *gossh.Client session *gossh.Session stdin io.WriteCloser stdout io.Reader stderr io.Reader } func Dial(host model.Host) (*Client, error) { addr := net.JoinHostPort(host.Hostname, fmt.Sprintf("%d", host.Port)) if host.Hostname == "" { addr = net.JoinHostPort(host.IP, "22") } config := &gossh.ClientConfig{ User: host.Username, HostKeyCallback: gossh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } switch host.AuthType { case "password", "": config.Auth = []gossh.AuthMethod{ gossh.Password(host.Password), } case "key": key, err := loadPrivateKey(host.Password) if err != nil { return nil, fmt.Errorf("load key: %w", err) } config.Auth = []gossh.AuthMethod{ gossh.PublicKeys(key), } case "both": key, err := loadPrivateKey(host.Password) if err != nil { return nil, fmt.Errorf("load key: %w", err) } config.Auth = []gossh.AuthMethod{ gossh.Password(host.Password), gossh.PublicKeys(key), } default: return nil, fmt.Errorf("unknown auth type: %s", host.AuthType) } client, err := gossh.Dial("tcp", addr, config) if err != nil { return nil, fmt.Errorf("dial: %w", err) } return &Client{ Host: host, client: client, }, nil } func (c *Client) Shell(stdin io.Reader, stdout io.Writer, stderr io.Writer, rows, cols int) error { session, err := c.client.NewSession() if err != nil { return fmt.Errorf("session: %w", err) } c.session = session modes := gossh.TerminalModes{ gossh.ECHO: 1, gossh.TTY_OP_ISPEED: 14400, gossh.TTY_OP_OSPEED: 14400, } if err := session.RequestPty("xterm-256color", rows, cols, modes); err != nil { return fmt.Errorf("pty: %w", err) } c.stdin, _ = session.StdinPipe() c.stdout, _ = session.StdoutPipe() c.stderr, _ = session.StderrPipe() go io.Copy(stdout, c.stdout) go io.Copy(stderr, c.stderr) go io.Copy(c.stdin, stdin) if err := session.Shell(); err != nil { return fmt.Errorf("shell: %w", err) } return session.Wait() } func (c *Client) Resize(rows, cols int) error { if c.session == nil { return nil } return c.session.WindowChange(rows, cols) } func (c *Client) Close() error { if c.session != nil { c.session.Close() } return c.client.Close() } func (c *Client) IsConnected() bool { return c.client != nil } func loadPrivateKey(keyContent string) (gossh.Signer, error) { if keyContent == "" { paths := []string{ os.ExpandEnv("$HOME/.ssh/id_ed25519"), os.ExpandEnv("$HOME/.ssh/id_rsa"), os.ExpandEnv("$HOME/.ssh/id_ecdsa"), } for _, p := range paths { data, err := os.ReadFile(p) if err == nil { return gossh.ParsePrivateKey(data) } } return nil, fmt.Errorf("no SSH key found") } return gossh.ParsePrivateKey([]byte(keyContent)) }