Skip to main content

duxcore/connection/connectionmode/
ssh2mode.rs

1//! Most frequent case : reach host through SSHv2
2
3use crate::connection::specification::Credentials;
4use crate::error::Error;
5use crate::result::cmd::CmdResult;
6use pem::Pem;
7use serde::{Deserialize, Serialize};
8use ssh2::Session;
9use std::io::Read;
10use std::net::TcpStream;
11use std::path::PathBuf;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Ssh2ConnectionDetails {
15    pub hostaddress: String,
16    pub authmode: Ssh2AuthMode,
17}
18
19impl Ssh2ConnectionDetails {
20    pub fn from(hostaddress: String, authmode: Ssh2AuthMode) -> Ssh2ConnectionDetails {
21        Ssh2ConnectionDetails {
22            hostaddress,
23            authmode,
24        }
25    }
26}
27
28#[derive(Clone)]
29pub struct Ssh2HostHandler {
30    pub hostaddress: String,
31    pub sshsession: Session,
32    pub authmode: Ssh2AuthMode,
33}
34
35impl Ssh2HostHandler {
36    pub fn new() -> Ssh2HostHandler {
37        Ssh2HostHandler {
38            hostaddress: String::new(),
39            sshsession: Session::new().unwrap(),
40            authmode: Ssh2AuthMode::Unset,
41        }
42    }
43
44    pub fn none() -> Ssh2HostHandler {
45        Ssh2HostHandler {
46            hostaddress: String::from(""),
47            sshsession: Session::new().unwrap(), // TODO: remove this unnecessary construction
48            authmode: Ssh2AuthMode::Unset,
49        }
50    }
51
52    pub fn from(hostaddress: String, authmode: Ssh2AuthMode) -> Ssh2HostHandler {
53        Ssh2HostHandler {
54            hostaddress,
55            sshsession: Session::new().unwrap(),
56            authmode,
57        }
58    }
59
60    pub fn set_to(&mut self, hostaddress: String, authmode: Ssh2AuthMode) {
61        self.hostaddress = hostaddress;
62        self.authmode = authmode;
63    }
64
65    pub fn init(&mut self) -> Result<(), Error> {
66        if self.authmode == Ssh2AuthMode::Unset {
67            return Err(Error::MissingInitialization(
68                "SSH2 authentication mode is unset".to_string(),
69            ));
70        } else {
71            // TODO : add SSH custom port handling
72            match TcpStream::connect(format!("{}:22", self.hostaddress)) {
73                Ok(tcp) => {
74                    self.sshsession.set_tcp_stream(tcp);
75                    self.sshsession.handshake().unwrap();
76
77                    match &self.authmode {
78                        Ssh2AuthMode::UsernamePassword(credentials) => {
79                            self.sshsession
80                                .userauth_password(&credentials.username, &credentials.password)
81                                .unwrap();
82                            if self.sshsession.authenticated() {
83                                return Ok(());
84                            } else {
85                                return Err(Error::FailedInitialization(String::from(
86                                    "PLACEHOLDER",
87                                )));
88                            }
89                        }
90                        Ssh2AuthMode::KeyFile((username, privatekeypath)) => {
91                            self.sshsession
92                                .userauth_pubkey_file(
93                                    username.as_str(),
94                                    None,
95                                    &privatekeypath,
96                                    None,
97                                )
98                                .unwrap(); // TODO : add pubkey and passphrase support
99                            if self.sshsession.authenticated() {
100                                return Ok(());
101                            } else {
102                                return Err(Error::FailedInitialization(String::from(
103                                    "PLACEHOLDER",
104                                )));
105                            }
106                        }
107                        Ssh2AuthMode::KeyMemory((username, pem)) => {
108                            self.sshsession
109                                .userauth_pubkey_memory(
110                                    username.as_str(),
111                                    None,
112                                    pem.to_string().as_str(), // Pem struct doesn't implement directly '.as_str()' but accepts '.to_string()'
113                                    None,
114                                )
115                                .unwrap(); // TODO : add pubkey and passphrase support
116                            if self.sshsession.authenticated() {
117                                return Ok(());
118                            } else {
119                                return Err(Error::FailedInitialization(String::from(
120                                    "PLACEHOLDER",
121                                )));
122                            }
123                        }
124                        Ssh2AuthMode::Agent(_agent) => {
125                            return Ok(());
126                        } // TODO
127                        _ => return Err(Error::FailedInitialization(String::from("Other error"))),
128                    }
129                }
130                Err(e) => {
131                    return Err(Error::FailedTcpBinding(format!("{:?}", e)));
132                }
133            }
134        }
135    }
136
137    pub fn is_this_cmd_available(&self, cmd: &str) -> Result<bool, Error> {
138        let check_cmd_content = format!("command -v {}", cmd);
139        let check_cmd_result = self.run_cmd(check_cmd_content.as_str());
140
141        match check_cmd_result {
142            Ok(cmd_result) => {
143                if cmd_result.rc == 0 {
144                    return Ok(true);
145                } else {
146                    return Ok(false);
147                }
148            }
149            Err(e) => {
150                return Err(Error::FailureToRunCommand(format!("{:?}", e)));
151            }
152        }
153    }
154
155    pub fn run_cmd(&self, cmd: &str) -> Result<CmdResult, Error> {
156        if let Ssh2AuthMode::Unset = self.authmode {
157            return Err(Error::MissingInitialization(
158                "Can't run command on remote host : authentication unset".to_string(),
159            ));
160        }
161
162        match self.sshsession.channel_session() {
163            Ok(mut channel) => {
164                channel.exec(cmd).unwrap();
165                let mut s = String::new();
166                channel.read_to_string(&mut s).unwrap();
167                channel.wait_close().unwrap();
168
169                return Ok(CmdResult {
170                    rc: channel.exit_status().unwrap(),
171                    stdout: s,
172                });
173            }
174            Err(e) => {
175                return Err(Error::FailureToEstablishConnection(format!("{e}")));
176            }
177        }
178    }
179}
180
181#[derive(Clone, PartialEq, Serialize, Deserialize)]
182pub enum Ssh2AuthMode {
183    Unset,
184    UsernamePassword(Credentials),
185    KeyFile((String, PathBuf)), // (username, private key's path)
186    KeyMemory((String, Pem)),   // (username, PEM encoded key from memory)
187    Agent(String),              // Name of SSH agent
188}
189
190impl std::fmt::Debug for Ssh2AuthMode {
191    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
192        match self {
193            Ssh2AuthMode::Unset => {
194                write!(f, "Unset")
195            }
196            Ssh2AuthMode::UsernamePassword(creds) => {
197                write!(f, "UsernamePassword(Credentials {{ username: {:?}, password: \"HIDDEN PASSWORD\" }})", creds.username)
198            }
199            Ssh2AuthMode::KeyFile((username, key_path)) => {
200                write!(f, "KeyFile(({:?}, {:?}))", username, key_path)
201            }
202            Ssh2AuthMode::KeyMemory((username, _key_content)) => {
203                write!(f, "KeyMemory(({:?}, \"HIDDEN KEY CONTENT\"))", username)
204            }
205            Ssh2AuthMode::Agent(agent_name) => {
206                write!(f, "Agent({:?})", agent_name)
207            }
208        }
209    }
210}