Sprint 6: JSON file persistence + real SSH terminal + V1 crypto/knownhosts integration
- go.mod replace directive for git.tukangketik.id/swanadiva/hostkeeper → ../../v1 - store/ package: JSON file persistence for hosts, snippets, keys, settings (~/Library/Application Support/hostkeeper/v2/) - model SSH fields: Hostname, Port, Username, AuthType, Password, KeyID - sshconn/ package: real SSH client via golang.org/x/crypto/ssh - Terminal WS: real SSH connection with fallback to mock shell - Dynamic host list in terminal (server-rendered JSON script tag) - All mock data defaults removed from model packages - Settings persist via store/settings.go
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user