rustssh/
lib.rs

1//! # rustssh
2//! rust ssh sudo password scp
3//! 
4//! ## Usage
5//! 
6//! #### 1. add dependencies
7//! ```
8//! # Cargo.toml
9//! [dependencies]
10//! rustssh = "0.1.2"
11//! ```
12//! 
13//! #### 2. edit code
14//! ```
15//! # src/main.rs
16//! 
17//! use rustssh::{Connection, Auth, SudoOptions};
18//! 
19//! fn main() {
20//! 
21//!     // create connection
22//!     let mut conn = Connection::new("1.1.1.1".to_string(), 22, "username".to_string(), Auth::Password("password".to_string())).unwrap();
23//! 
24//!     // Execute remote commands as the connected user
25//!     let (stdout, stderr, status) = conn.run("ls").unwrap();
26//!     println!("The output of run is: {}, The outerr of run is: {}, The status code of run is: {}, end.", stdout, stderr, status);
27//! 
28//!     // Execute remote commands as root user
29//!     let (stdout, stderr, status) = conn.sudo("ls -l /root/").unwrap();
30//!     println!("The output of sudo is: {}, The outerr of sudo is: {}, The status code of sudo is: {}, end.", stdout, stderr, status);
31//! 
32//!     // Execute remote commands as user user01
33//!     // SudoOptions::new() Specify three parameters: 1. The username to sudo, 2. The password that needs to be entered when sudo, 3. Specify the sudo prompt
34//!     let opt = SudoOptions::new("user01", "", "");
35//!     let (stdout, stderr, status) = conn.sudo_with_options("ls -l /home/user01/", Some(opt)).unwrap();
36//!     println!("The output of sudo is: {}, The outerr of sudo is: {}, The status code of sudo is: {}, end.", stdout, stderr, status);
37//! 
38//!     // scp copies local files to remote
39//!     conn.scp("/home/xxx/x.xml", "/home/xxx/xx.xml").unwrap();
40//! }
41//! ```
42//! 
43//! ## Features (see future work needs)
44//! 
45//! - conn.scp() can copy directories
46//! - is_exists()
47//! - conn.is_dir()
48//! - conn.is_empty()
49//! - ...
50
51use ssh2::{self, Session};
52use std::error::Error;
53use std::fs;
54use std::io::{Read, Write};
55use std::net::TcpStream;
56use std::path::Path;
57// use ssh2::PtyModes;
58
59struct Watcher {
60    pattern: String,  // 要捕获字符串
61    response: String, // 捕获到匹配的字符串出现后, 要输入的内容
62    // sentinel: String, // TODO 应答后返回的信息与之匹配, 说明应答失败. 还未实现
63    case_sensitive: bool, // 是否区别大小写
64}
65
66pub struct RunOptions {
67    watchers: Vec<Watcher>,
68}
69
70impl RunOptions {
71    pub fn new() -> RunOptions {
72        RunOptions { watchers: Vec::new() }
73    }
74
75    pub fn set_watcher(&mut self, pattern: &str, response: &str, case_sensitive: bool) {
76        self.watchers.push(Watcher { pattern: pattern.to_string(), response: response.to_string(), case_sensitive });
77    }
78}
79
80pub struct SudoOptions {
81    sudo_user: String,
82    sudo_password: String,
83    sudo_pattern: String,
84    run: RunOptions,
85}
86
87impl SudoOptions {
88    pub fn new(sudo_user: &str, sudo_password: &str, sudo_pattern: &str) -> SudoOptions {
89        SudoOptions {
90            sudo_user: sudo_user.to_string(),
91            sudo_password: sudo_password.to_string(),
92            sudo_pattern: sudo_pattern.to_string(),
93            run: RunOptions { watchers: Vec::new() },
94        }
95    }
96
97    pub fn set_watcher(&mut self, pattern: &str, response: &str, case_sensitive: bool) {
98        self.run.watchers.push(Watcher { pattern: pattern.to_string(), response: response.to_string(), case_sensitive });
99    }
100}
101
102pub enum Auth {
103    Password(String),
104    Privatekey(String),
105    Privatekeyfile(String),
106}
107
108impl Clone for Auth {
109    fn clone(&self) -> Self {
110        match self {
111            Self::Password(arg0) => Self::Password(arg0.clone()),
112            Self::Privatekey(arg0) => Self::Privatekey(arg0.clone()),
113            Self::Privatekeyfile(arg0) => Self::Privatekeyfile(arg0.clone()),
114        }
115    }
116}
117
118pub struct Connection {
119    host: String,
120    port: u16,
121    user: String,
122    auth: Auth,
123    timeout: u32,
124    session: Session,
125}
126
127impl Connection {
128    pub fn new(host: String, port: u16, user: String, auth: Auth) -> Result<Connection, Box<dyn Error>> {
129        let timeout: u32 = 60000;
130
131        let addr = format!("{}:{}", host, port);
132        let tcp = TcpStream::connect(addr)?;
133
134        let mut session = Session::new().unwrap();
135        session.set_tcp_stream(tcp);
136        session.handshake().unwrap();
137        session.set_timeout(timeout);
138
139        let mut conn = Connection {
140            host,
141            port,
142            user,
143            auth,
144            timeout,
145            session,
146        };
147        conn.authenticated()?;
148        conn.session.authenticated();
149
150        Ok(conn)
151    }
152
153    fn authenticated(&mut self) -> Result<(), ssh2::Error> {
154        match &self.auth {
155            Auth::Password(password) => self.session.userauth_password(&self.user, &password),
156            Auth::Privatekey(privatekey) => self.session.userauth_pubkey_memory(&self.user, None, &privatekey, None),
157            Auth::Privatekeyfile(privatekey_file) => {
158                let privatekey_path = Path::new(&privatekey_file);
159                self.session.userauth_pubkey_file(&self.user, None, privatekey_path, None)
160            }
161        }
162    }
163
164    pub fn get_host(&mut self) -> String {
165        self.host.to_string()
166    }
167
168    pub fn get_port(&mut self) -> u16 {
169        self.port
170    }
171
172    pub fn get_user(&mut self) -> String {
173        self.user.to_string()
174    }
175
176    pub fn get_auth(&mut self) -> Auth {
177        self.auth.clone()
178    }
179
180    pub fn get_timeout(&mut self) -> u32 {
181        self.timeout
182    }
183
184    pub fn set_timeout(&mut self, timeout: u32) {
185        self.timeout = timeout;
186        self.session.set_timeout(self.timeout);
187    }
188
189    fn run_watcher(&mut self, channel: &mut ssh2::Channel, watchers: Vec<Watcher>, stdout: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
190        loop {
191            let mut buf = [0; 1024];
192
193            let n = channel.read(&mut buf)?;
194            if n == 0 {
195                break;
196            }
197
198            let slice = &buf[0..n];
199            stdout.extend_from_slice(slice);
200
201            let s = String::from_utf8_lossy(slice);
202
203            for w in watchers.iter() {
204                if w.case_sensitive {
205                    if s.contains(&w.pattern) {
206                        channel.write_all(format!("{}\n", w.response).as_bytes())?;
207                        channel.flush()?;
208                    }
209                    
210                } else {
211                    if s.to_uppercase().contains(&w.pattern.to_uppercase()) {
212                        channel.write_all(format!("{}\n", w.response).as_bytes())?;
213                        channel.flush()?;
214                    }
215                }
216            }
217        }
218        Ok(())
219    }
220
221    pub fn run_with_options(&mut self, cmd: &str, options: Option<RunOptions>) -> Result<(String, String, i32), Box<dyn Error>> {
222        let command = format!("PATH=$PATH:/usr/bin:/usr/sbin {}", cmd);
223
224        let mut channel = self.session.channel_session()?;
225
226        // let mut modes = PtyModes::new();
227        // modes.set_boolean(ssh2::PtyModeOpcode::TCSANOW, false);
228        // modes.set_boolean(ssh2::PtyModeOpcode::ECHO, false);
229        // modes.set_u32(ssh2::PtyModeOpcode::TTY_OP_ISPEED, 144000);
230        // modes.set_u32(ssh2::PtyModeOpcode::TTY_OP_OSPEED, 144000);
231        // channel.request_pty("xterm", Some(modes),Some((80, 40, 0, 0))).unwrap();
232
233        channel.handle_extended_data(ssh2::ExtendedData::Merge)?;
234        channel.exec(&command)?;
235
236        let mut stdout = Vec::new();
237
238        match options {
239            Some(opt) => {
240                if opt.watchers.len() != 0 {
241                    self.run_watcher(&mut channel, opt.watchers, &mut stdout)?
242                }
243            }
244            None => (),
245        }
246
247        let mut stdout = String::from_utf8_lossy(&stdout);
248
249        // 对于没有 watchers 的情况, 需要在这里读取输出内容
250        let mut out = String::new();
251        channel.read_to_string(&mut out)?;
252        stdout.to_mut().push_str(&out);
253
254        let mut stderr = String::new();
255        channel.stderr().read_to_string(&mut stderr).unwrap();
256
257        channel.wait_close().unwrap();
258
259        let status: i32 = channel.exit_status()?;
260
261        Ok((stdout.to_string(), stderr.to_string(), status))
262    }
263
264    pub fn sudo_with_options(&mut self, cmd: &str, options: Option<SudoOptions>) -> Result<(String, String, i32), Box<dyn Error>> {
265
266        let mut opt = match options {
267            Some(options) => options,
268            None => SudoOptions::new("root", "", "[sudo] password:"),
269        };
270
271        if opt.sudo_user == "" {
272            opt.sudo_user = "root".to_string();
273        }
274    
275        if opt.sudo_password == "" {
276            match self.get_auth() {
277                Auth::Password(pwd) => opt.sudo_password = pwd.to_string(),
278                Auth::Privatekey(_) => {},
279                Auth::Privatekeyfile(_) => {},
280            }
281        }
282    
283        if opt.sudo_pattern == "" {
284            opt.sudo_pattern = "[sudo] password:".to_string()
285        }
286
287        opt.set_watcher(&opt.sudo_pattern.clone(), &opt.sudo_password.clone(), true);
288
289        let cmd = format!("sudo -S -p '{}' -H -u {} /bin/bash -l -c \"cd; {}\"", opt.sudo_pattern, opt.sudo_user, cmd);
290
291        self.run_with_options(&cmd, Some(opt.run))
292
293    }
294
295    pub fn run(&mut self, cmd: &str) -> Result<(String, String, i32), Box<dyn Error>> {
296        self.run_with_options(cmd, None)
297    }
298
299    pub fn sudo(&mut self, cmd: &str) -> Result<(String, String, i32), Box<dyn Error>> {
300        self.sudo_with_options(cmd, None)
301    }
302
303    pub fn scp(&mut self, source: &str, target: &str) -> Result<(), Box<dyn Error>> {
304        // 上传文件
305        let source_file = fs::read(source)?;
306        let mut target_file = self.session.scp_send(Path::new(&target), 0o755, source_file.len() as u64, None)?;
307
308        target_file.write(&source_file)?;
309
310        Ok(())
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use std::env;
317
318    use super::*;
319
320    #[test]
321    fn it_works() {
322        let myhost = env::var("MYHOST").expect("$MYHOST is not defined");
323        let myport = env::var("MYPORT").expect("$MYPORT is not defined");
324        let myusername = env::var("MYUSERNAME").expect("$MYUSERNAME is not defined");
325        let mypassword = env::var("MYPASSWORD").expect("$MYPASSWORD is not defined");
326
327        let port: u16 = myport.parse().unwrap();
328
329        let mut conn = Connection::new(myhost, port, myusername, Auth::Password(mypassword)).unwrap();
330
331        let (stdout, stderr, status) = conn.run("ls").unwrap();
332        println!("The output of run is: {}, The outerr of run is: {}, The status code of run is: {}, end.", stdout, stderr, status);
333
334        let (stdout, stderr, status) = conn.sudo("ls -l /root/").unwrap();
335        println!("The output of sudo is: {}, The outerr of sudo is: {}, The status code of sudo is: {}, end.", stdout, stderr, status);
336
337        conn.scp("/home/xxx/x.xml", "/home/xxx/xx.xml").unwrap();
338    }
339}