diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7182cd7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ ++ `v0.8.0` + - Add SSH agent tunnel feature diff --git a/Cargo.toml b/Cargo.toml index 7e2b6d5..4a5886b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "supervisor-rs" -version = "0.7.1" +version = "0.8.0" authors = ["ccQpein"] edition = "2018" description = "Manage (Start/Stop/Restart/etc.) processings on server." @@ -12,6 +12,7 @@ readme = "README.md" yaml-rust = "0.4" chrono = { version = "0.4", features = ["serde"] } openssl = "0.10.26" +ssh2 = "0.9" [[bin]] name = "supervisor-rs-server" diff --git a/README.md b/README.md index fe02011..fcda575 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,33 @@ listener_addr: "::1" # only listen local ipv6 `ipv6` field only used when there is **no** `listener_addr` given, or `supervisor-rs` server side will ignore `ipv6`. If there is no `listener_addr` given, and `ipv6` is true, `supervisor-rs` will start with listen ipv6 address `::`. +### SSH-agent tunnel feature ### + +Defaultly, `supervisor-rs-server` listens `0.0.0.0` that means all servers those can reach `supervisor-rs-server`'s host can send command to `supervisor-rs-server`. Then we have [key-pair](#use-key-pairs-authenticate-clients) feature for encrypting/authorization of client's identity. + +If we are in inter-network, listen `0.0.0.0` or specific ip addresses doesn't that matter. Firewall and router table can do the job for us. Beside, [key-pair](#use-key-pairs-authenticate-clients) can make sure only some people can send `supervisor-rs-server`. + +However. if we deploy in some cloud services, our server open to the weird world. We may don't trust outside, and still want to ssh login host server and do our jobs. This is the reason that why this feature come. + +**How to** + +**Server Side:** + +Don't need any additional configs for turn on this feature. For me, I just change `listener_addr` from `0.0.0.0` to `127.0.0.1` for making sure all outside computer cannot talk to `supervisor-rs-server`. + +Also, make sure `supervisor-rs-client` in your server's PATH. If you install `supervisor-rs` by `cargo install supervisor-rs`, I guess you already have it. + +**Client Side:** + +Assume our `supervisor-rs-server` hosted on `192.168.3.3`. And we can ssh login that server with `ssh -i ~/.ssh/key user@192.168.3.3` + +Then: + +1. Tell ssh in your computer that `192.168.3.3` use key `~/.ssh/key` (by change `~/.ssh/config`). So you can ssh login without give which key you need to use, like `ssh user@192.168.3.3` +2. `ssh-add ~/.ssh/key` for adding key in ssh-agent. + +After these, you can run `supervisor-rs-client check on ssh://user@192.168.3.3`. Every usages are same, you just need to change host (ip address) field to `ssh://{username}@{hostip}`. + ### What if accident happens ### * if supervisor-rs be killed by `kill`, children won't stop, they will be taken by system. diff --git a/src/bin/client.rs b/src/bin/client.rs index 093d163..e8aba9b 100644 --- a/src/bin/client.rs +++ b/src/bin/client.rs @@ -1,13 +1,7 @@ use std::env; -use std::io::prelude::*; -use std::net::{IpAddr, SocketAddr, TcpStream}; -use std::time::Duration; -use supervisor_rs::client::{Command, Ops}; - -const CANNOT_REACH_SERVER_ERROR: &'static str = - "\nLooks like client cannot reach server side, make sure you start supervisor-rs-server on host you want to reach. \ -Maybe it is network problem, or even worse, server app terminated. \ -If server app terminated, all children were running become zombies. Check them out."; +use std::net::IpAddr; +use std::str::FromStr; +use supervisor_rs::client::*; fn main() { let arguments = env::args(); @@ -20,60 +14,40 @@ fn main() { } }; - //println!("this is command {:?}", cache_command); - if let Ops::Help = cache_command.get_ops() { println!("{}", help()); return; } // build streams, parse all host - let mut streams: Vec = { + let mut streams: Vec = { if let Some(pairs) = cache_command.prep_obj_pairs() { - //parse ip address - //only accept ip address + // parse ip address + // only accept ip address let ip_pair = pairs.iter().filter(|x| x.0.is_on()); - let addrs: Vec = { - // ip address format can be "127.0.0.1" or "127.0.0.1, 127.0.0.2" - // this part need collect all addressed and *flatten* it - let addresses = ip_pair - .map(|des| { - des.1 - .split(|x| x == ',' || x == ' ') - .filter(|x| *x != "") - .collect::>() - }) - .flatten() - .collect::>(); - - // change ip to UpAddr - let mut result: Vec = vec![]; - for a in addresses { - match a.parse::() { - Ok(ad) => result.push(ad), - Err(e) => { - println!("something wrong when parse des ip address {}: {}", a, e); - return; + // ip address format can be "127.0.0.1" or "127.0.0.1, 127.0.0.2" + // or "ssh://username@ipaddress" + // or "ssh://username@ipaddress ,ssh://username1@ipaddress1" + match ip_fields_parser(ip_pair) { + Ok(addrs) => { + let mut a = vec![]; + //creat socket + for addr in addrs { + match ConnectionStream::new(addr) { + Ok(s) => a.push(s), + Err(e) => { + println!("{}", e.to_string()); + return; + } } - }; - } - result - }; - - //dbg!(&addrs); - //creat socket - let mut _streams: Vec = vec![]; - for addr in addrs { - let sock = SocketAddr::new(addr, 33889); - match TcpStream::connect_timeout(&sock, Duration::new(5, 0)) { - Ok(s) => _streams.push(s), - Err(e) => { - println!("error of {}: {}; {}", addr, e, CANNOT_REACH_SERVER_ERROR); - return; } - }; + a + } + Err(e) => { + println!("{}", e.to_string()); + return; + } } - _streams } else { vec![] } @@ -81,17 +55,11 @@ fn main() { if streams.len() == 0 { // If don't have prep, give local address (ipv4) - let mut _streams: Vec = vec![]; - let sock = SocketAddr::new("127.0.0.1".parse::().unwrap(), 33889); - match TcpStream::connect_timeout(&sock, Duration::new(5, 0)) { - Ok(s) => _streams.push(s), - Err(e) => { - println!("error of 127.0.0.1: {}; {}", e, CANNOT_REACH_SERVER_ERROR); - println!("Maybe you are listening on ipv6? try to use ::1 as host"); - return; - } - } - streams = _streams + streams = + vec![ + ConnectionStream::new(IpFields::Normal(IpAddr::from_str("127.0.0.1").unwrap())) + .unwrap(), + ]; } // Here to check/make encrypt data @@ -103,29 +71,11 @@ fn main() { //send same commands to all servers for mut stream in streams { - let address = if let Ok(ad) = stream.peer_addr() { - ad.to_string() - } else { - String::from("Unknow address") - }; - - if let Err(e) = stream.write_all(&data_2_server) { - println!("Error from {}:\n {}", address, e); - return; - }; - - if let Err(e) = stream.flush() { - println!("Error from {}:\n {}", address, e); - return; - }; - - let mut response = String::new(); - if let Err(e) = stream.read_to_string(&mut response) { - println!("Error from {}:\n {}", address, e); - return; - }; - - print!("Server {} response:\n{}", address, response); + print!( + "Server {} response:\n{}", + stream.address().unwrap(), + stream.send_comm(&data_2_server).unwrap() + ); } } @@ -156,7 +106,6 @@ https://github.com/ccqpein/supervisor-rs#usage #[cfg(test)] mod tests { use super::*; - use supervisor_rs::client::*; #[test] fn ip_address_parse() { diff --git a/src/client.rs b/src/client.rs index 4aa7a83..3ccbfb4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,15 @@ use super::keys_handler::DataWrapper; +use ssh2::Session; +use std::io::prelude::*; use std::io::{Error, ErrorKind, Result}; +use std::net::{IpAddr, SocketAddr, TcpStream}; use std::str; +use std::time::Duration; + +pub const CANNOT_REACH_SERVER_ERROR: &'static str = + "Looks like client cannot reach server side, make sure you start supervisor-rs-server on host you want to reach. \ +Maybe it is network problem, or even worse, server app terminated. \ +If server app terminated, all children were running become zombies. Check them out."; /// Client operations /// @@ -254,7 +263,8 @@ impl Command { } } - // ops + ' ' + childname + /// ops + ' ' + childname + /// and there are no Prepositions and Objects inside pub fn as_bytes(&self) -> Vec { let mut cache = self.op.to_string().as_bytes().to_vec(); if self.child_name.is_some() { @@ -273,6 +283,114 @@ impl Command { } } +#[derive(Debug, PartialEq)] +pub enum IpFields<'a> { + Normal(IpAddr), + SshIp { username: &'a str, ipaddr: IpAddr }, +} + +/// ip address parser, support normal ip address and ssh protocol +pub fn ip_fields_parser<'a>( + ip_pair: impl Iterator, +) -> std::result::Result>, String> { + let cache = ip_pair + .map(|(_, ip)| ip.split(|x| x == ',' || x == ' ').filter(|x| *x != "")) + .flatten(); + + let mut result = vec![]; + for s in cache { + if s.starts_with("ssh://") { + result.push(ssh_address_parse(s)?); + } else { + result.push(IpFields::Normal( + s.parse::().map_err(|e| e.to_string())?, + )) + } + } + Ok(result) +} + +/// ssh address has to follow 'ssh://username@ipaddress' +fn ssh_address_parse(address: &str) -> std::result::Result, String> { + let mut ll = address + .split(|x| x == '/' || x == ':' || x == '@') + .filter(|s| *s != ""); + + Ok(IpFields::SshIp { + username: ll.nth(1).ok_or("Username parse wrong".to_string())?, + ipaddr: ll + .nth(0) + .ok_or("IP address wrong".to_string())? + .parse::() + .map_err(|e| e.to_string())?, + }) +} + +pub enum ConnectionStream { + Tcp(TcpStream), + Ssh(Session, String), +} + +impl ConnectionStream { + /// generate new connections by using IpFields + pub fn new(ip: IpFields<'_>) -> std::result::Result { + match ip { + IpFields::Normal(addr) => { + let sock = SocketAddr::new(addr, 33889); + Ok(Self::Tcp( + TcpStream::connect_timeout(&sock, Duration::new(5, 0)) + .map_err(|_| CANNOT_REACH_SERVER_ERROR)?, + )) + } + IpFields::SshIp { username, ipaddr } => { + let sock = SocketAddr::new(ipaddr, 22); + let tcp = TcpStream::connect_timeout(&sock, Duration::new(5, 0)) + .map_err(|_| CANNOT_REACH_SERVER_ERROR)?; + let mut sess = Session::new().unwrap(); + sess.set_tcp_stream(tcp); + sess.handshake().map_err(|e| e.to_string())?; + sess.userauth_agent(username).map_err(|e| e.to_string())?; + Ok(Self::Ssh(sess, ipaddr.to_string())) + } + } + } + + /// send command to server during the built streams + pub fn send_comm(&mut self, comm: &[u8]) -> Result { + match self { + ConnectionStream::Tcp(s) => { + s.write_all(comm)?; + + s.flush()?; + + let mut response = String::new(); + s.read_to_string(&mut response)?; + + Ok(response) + } + ConnectionStream::Ssh(s, _) => { + let mut channel = s.channel_session()?; + let mut head = "supervisor-rs-client ".to_string(); + head.push_str(str::from_utf8(comm).unwrap()); + channel.exec(head.as_str())?; + + let mut response = String::new(); + channel.read_to_string(&mut response)?; + + channel.wait_close()?; + Ok(response) + } + } + } + + pub fn address(&self) -> std::result::Result { + Ok(match self { + ConnectionStream::Tcp(s) => s.peer_addr().map_err(|e| e.to_string())?.to_string(), + ConnectionStream::Ssh(_, addr) => addr.clone(), + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -355,4 +473,58 @@ mod tests { assert_eq!(dw, DataWrapper::new("./test/public.pem", "check").unwrap()); Ok(()) } + + #[test] + fn test_ip_fields_parser() { + let test = vec![ + (Prepositions::On, "127.0.0.1".to_string()), + (Prepositions::On, "127.0.0.2, 127.0.0.3".to_string()), + (Prepositions::On, "ssh://hello@127.0.0.1".to_string()), + ( + Prepositions::On, + "ssh://hello@127.0.0.1, ssh://hello@127.0.0.2".to_string(), + ), + ( + Prepositions::On, + "ssh:/wrong@127.0.0.1, ssh://alsowrong127.0.0.2, sssh://jjj@oidde".to_string(), + ), + ]; + + let test0 = test.iter().map(|(ref a, ref b)| (a, b)).collect::>(); + + assert_eq!( + ip_fields_parser(vec![test0[0]].iter()), + Ok(vec![IpFields::Normal("127.0.0.1".parse().unwrap())]), + ); + + assert_eq!( + ip_fields_parser(vec![test0[1]].iter()), + Ok(vec![ + IpFields::Normal("127.0.0.2".parse().unwrap()), + IpFields::Normal("127.0.0.3".parse().unwrap()) + ]), + ); + + assert_eq!( + ip_fields_parser(vec![test0[2]].iter()), + Ok(vec![IpFields::SshIp { + username: "hello", + ipaddr: "127.0.0.1".parse().unwrap() + }]), + ); + assert_eq!( + ip_fields_parser(vec![test0[3]].iter()), + Ok(vec![ + IpFields::SshIp { + username: "hello", + ipaddr: "127.0.0.1".parse().unwrap() + }, + IpFields::SshIp { + username: "hello", + ipaddr: "127.0.0.2".parse().unwrap() + } + ]), + ); + assert!(ip_fields_parser(vec![test0[4]].iter()).is_err()); + } } diff --git a/src/server.rs b/src/server.rs index fd42146..89a6b52 100644 --- a/src/server.rs +++ b/src/server.rs @@ -519,7 +519,13 @@ pub fn start_deamon(safe_kg: Arc>, sd: Sender<(String, Strin }; // start TCP listener to receive client commands - let listener = TcpListener::bind((server_conf.listener_addr, 33889)).unwrap(); + let listener = TcpListener::bind((server_conf.listener_addr.clone(), 33889)).unwrap(); + println!( + "{} {}:{}", + logger::timelog("Server is listening on"), + server_conf.listener_addr, + 33889 + ); for stream in listener.incoming() { match stream {