Skip to main content

omnyssh_core/ssh/
client.rs

1//! SSH connection management.
2//!
3//! Connections delegated to the system SSH binary.
4//! Also provides russh-based client for live metrics.
5
6use serde::{Deserialize, Serialize};
7
8/// Indicates where a host entry originated.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
10#[serde(rename_all = "snake_case")]
11pub enum HostSource {
12    /// Imported from `~/.ssh/config` at startup.
13    SshConfig,
14    /// Added manually through the TUI form.
15    #[default]
16    Manual,
17}
18
19/// A single host entry used for SSH connections.
20///
21/// Populated either from `~/.ssh/config` (via the parser) or from
22/// `~/.config/omnyssh/hosts.toml` (manual entries added through the TUI).
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Host {
25    /// Display name / alias (e.g. `"web-prod-1"`).
26    pub name: String,
27    /// Hostname or IP address to connect to.
28    pub hostname: String,
29    /// SSH user. Defaults to `"root"` when not specified.
30    #[serde(default = "default_user")]
31    pub user: String,
32    /// SSH port. Defaults to `22` when not specified.
33    #[serde(default = "default_port")]
34    pub port: u16,
35    /// Path to the private key file (e.g. `~/.ssh/id_ed25519`).
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub identity_file: Option<String>,
38    /// Password for password-based authentication (not recommended, used for initial setup).
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub password: Option<String>,
41    /// ProxyJump host alias (for bastion / jump-host setups).
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub proxy_jump: Option<String>,
44    /// Organisational tags (e.g. `["production", "web"]`).
45    #[serde(default, skip_serializing_if = "Vec::is_empty")]
46    pub tags: Vec<String>,
47    /// Free-text notes about this host.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub notes: Option<String>,
50    /// Where this entry came from.
51    #[serde(default)]
52    pub source: HostSource,
53    /// Original host name from `~/.ssh/config` if this host was renamed.
54    /// Used to prevent duplicate entries when a SSH-config host is renamed.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub original_ssh_host: Option<String>,
57
58    // -----------------------------------------------------------------------
59    // Auto SSH Key Setup metadata
60    // -----------------------------------------------------------------------
61    /// Date when SSH key was configured by OmnySSH (ISO 8601 format).
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub key_setup_date: Option<String>,
64    /// Whether password authentication has been disabled on the server.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub password_auth_disabled: Option<bool>,
67}
68
69fn default_user() -> String {
70    std::env::var("USER")
71        .or_else(|_| std::env::var("LOGNAME"))
72        .unwrap_or_else(|_| String::from("root"))
73}
74
75fn default_port() -> u16 {
76    22
77}
78
79impl Default for Host {
80    fn default() -> Self {
81        Self {
82            name: String::new(),
83            hostname: String::new(),
84            user: default_user(),
85            port: default_port(),
86            identity_file: None,
87            password: None,
88            proxy_jump: None,
89            tags: Vec::new(),
90            notes: None,
91            source: HostSource::default(),
92            original_ssh_host: None,
93            key_setup_date: None,
94            password_auth_disabled: None,
95        }
96    }
97}
98
99/// Runtime connection status for a host. Never serialised to disk.
100#[derive(Debug, Clone, Default, PartialEq)]
101pub enum ConnectionStatus {
102    /// No connection attempt has been made yet.
103    #[default]
104    Unknown,
105    /// A connection attempt is currently in progress.
106    Connecting,
107    /// The host is connected.
108    Connected,
109    /// The last connection attempt failed with the given message.
110    Failed(String),
111}