commit
						cfcbf638d6
					
				
				 3 changed files with 422 additions and 0 deletions
			
			
		@ -0,0 +1,12 @@ | 
				
			|||||||
 | 
					[package] | 
				
			||||||
 | 
					name = "sockser" | 
				
			||||||
 | 
					version = "0.1.0" | 
				
			||||||
 | 
					authors = ["plazmoid <kronos44@mail.ru>"] | 
				
			||||||
 | 
					edition = "2018" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies] | 
				
			||||||
 | 
					nix = "*" | 
				
			||||||
 | 
					reqwest = { version = "*", features = ["blocking", "socks"]} | 
				
			||||||
 | 
					once_cell = "*" | 
				
			||||||
@ -0,0 +1,290 @@ | 
				
			|||||||
 | 
					use nix::{ | 
				
			||||||
 | 
					    sys::signal::{signal, SigHandler, Signal}, | 
				
			||||||
 | 
					    unistd::{close as fdclose, fork, getppid, setsid, ForkResult}, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					use reqwest::{blocking::Client, header, Proxy}; | 
				
			||||||
 | 
					use std::{ | 
				
			||||||
 | 
					    env, | 
				
			||||||
 | 
					    io::Result as IOResult, | 
				
			||||||
 | 
					    panic, | 
				
			||||||
 | 
					    process::{exit, Child, Command, Stdio}, | 
				
			||||||
 | 
					    sync::{Mutex, MutexGuard}, | 
				
			||||||
 | 
					    thread, time, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use once_cell::sync::OnceCell; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod ssh_config; | 
				
			||||||
 | 
					use ssh_config::TinySSHConfig; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const HOST: &str = "aws-tun"; | 
				
			||||||
 | 
					const BASE_CONFIG: &str = "/home/plazmoid/.ssh/config"; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static mut DAEMONIZED: bool = false; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct ThresholdCaller { | 
				
			||||||
 | 
					    // call Fn once after threshold is exceeded
 | 
				
			||||||
 | 
					    cmd: Box<dyn Fn(&str) + Send + 'static>, | 
				
			||||||
 | 
					    arg: Option<String>, | 
				
			||||||
 | 
					    triggered: bool, | 
				
			||||||
 | 
					    retries: usize, | 
				
			||||||
 | 
					    max_retries: usize, | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ThresholdCaller { | 
				
			||||||
 | 
					    fn new(cmd: impl Fn(&str) + Send + 'static, max_retries: usize) -> Self { | 
				
			||||||
 | 
					        ThresholdCaller { | 
				
			||||||
 | 
					            cmd: Box::new(cmd), | 
				
			||||||
 | 
					            arg: None, | 
				
			||||||
 | 
					            triggered: false, | 
				
			||||||
 | 
					            retries: 0, | 
				
			||||||
 | 
					            max_retries, | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get<'m>() -> MutexGuard<'m, Self> { | 
				
			||||||
 | 
					        static FAIL_NOTIFIER: OnceCell<Mutex<ThresholdCaller>> = OnceCell::new(); | 
				
			||||||
 | 
					        FAIL_NOTIFIER | 
				
			||||||
 | 
					            .get_or_init(|| { | 
				
			||||||
 | 
					                Mutex::new(ThresholdCaller::new( | 
				
			||||||
 | 
					                    |info| notify(format!("Can't setup socks proxy: {}", info), true), | 
				
			||||||
 | 
					                    5, | 
				
			||||||
 | 
					                )) | 
				
			||||||
 | 
					            }) | 
				
			||||||
 | 
					            .lock() | 
				
			||||||
 | 
					            .unwrap() | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn call_once(&mut self, info: Option<&String>) { | 
				
			||||||
 | 
					        //dbg!(self.retries, self.triggered);
 | 
				
			||||||
 | 
					        if !self.triggered && self.retries >= self.max_retries { | 
				
			||||||
 | 
					            let arg = info.or(self.arg.as_ref()); | 
				
			||||||
 | 
					            (self.cmd)(arg.unwrap_or(&String::new())); | 
				
			||||||
 | 
					            self.triggered = true; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn set_arg(&mut self, arg: Option<String>) { | 
				
			||||||
 | 
					        self.arg = arg; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn append_fail<S: Into<String>>(&mut self, msg: Option<S>) { | 
				
			||||||
 | 
					        self.inc(); | 
				
			||||||
 | 
					        if let Some(m) = msg { | 
				
			||||||
 | 
					            let msg = m.into(); | 
				
			||||||
 | 
					            unsafe { | 
				
			||||||
 | 
					                if !DAEMONIZED { | 
				
			||||||
 | 
					                    eprintln!("{}", &msg); | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            self.set_arg(Some(msg)); | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn was_triggered(&self) -> bool { | 
				
			||||||
 | 
					        self.triggered | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn reset(&mut self) { | 
				
			||||||
 | 
					        self.retries = 0; | 
				
			||||||
 | 
					        self.triggered = false; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn inc(&mut self) { | 
				
			||||||
 | 
					        self.retries += 1; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn daemonize() -> Result<i32, String> { | 
				
			||||||
 | 
					    if getppid().as_raw() != 1 { | 
				
			||||||
 | 
					        setsig(Signal::SIGTTOU, SigHandler::SigIgn); | 
				
			||||||
 | 
					        setsig(Signal::SIGTTIN, SigHandler::SigIgn); | 
				
			||||||
 | 
					        setsig(Signal::SIGTSTP, SigHandler::SigIgn); | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    for fd in 0..=2 { | 
				
			||||||
 | 
					        fdclose(fd).ok(); | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    unsafe { | 
				
			||||||
 | 
					        match fork() { | 
				
			||||||
 | 
					            Ok(ForkResult::Parent { .. }) => { | 
				
			||||||
 | 
					                exit(0); | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            Ok(ForkResult::Child) => match setsid() { | 
				
			||||||
 | 
					                Ok(pid) => Ok(pid.as_raw()), | 
				
			||||||
 | 
					                Err(e) => Err(e.to_string()), | 
				
			||||||
 | 
					            }, | 
				
			||||||
 | 
					            Err(_) => exit(255), | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn setsig(sig: Signal, hnd: SigHandler) { | 
				
			||||||
 | 
					    unsafe { | 
				
			||||||
 | 
					        signal(sig, hnd).unwrap(); | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn spawn(cmd: &str, args: &[&str]) -> IOResult<Child> { | 
				
			||||||
 | 
					    Command::new(cmd).args(args).spawn() | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn _dbg() { | 
				
			||||||
 | 
					    spawn("touch", &["/tmp/asdqwe"]).ok(); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn notify<S: Into<String>>(msg: S, critical: bool) { | 
				
			||||||
 | 
					    let urg = if critical { "critical" } else { "normal" }; | 
				
			||||||
 | 
					    spawn( | 
				
			||||||
 | 
					        "notify-send", | 
				
			||||||
 | 
					        &["-t", "5000", "-u", urg, &format!("sockser: {}", msg.into())], | 
				
			||||||
 | 
					    ) | 
				
			||||||
 | 
					    .unwrap() | 
				
			||||||
 | 
					    .wait() | 
				
			||||||
 | 
					    .unwrap(); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn run_proxy(host: &str) -> IOResult<Child> { | 
				
			||||||
 | 
					    Command::new("/usr/bin/ssh") | 
				
			||||||
 | 
					        .args(&["-f", "-N", host, "-o", "ConnectTimeout=3"]) | 
				
			||||||
 | 
					        .stderr(Stdio::piped()) | 
				
			||||||
 | 
					        .spawn() | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn pgrep(cmd: &str) -> Option<i32> { | 
				
			||||||
 | 
					    let grep = Command::new("pgrep").args(&["-f", cmd]).output().unwrap(); | 
				
			||||||
 | 
					    if grep.status.success() { | 
				
			||||||
 | 
					        let output = String::from_utf8(grep.stdout).unwrap(); | 
				
			||||||
 | 
					        output.lines().next().unwrap().parse().ok() | 
				
			||||||
 | 
					    } else { | 
				
			||||||
 | 
					        None | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn process_present(cmd: &str) -> bool { | 
				
			||||||
 | 
					    pgrep(cmd).is_some() | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn kill_by_cmd(cmd: &str) { | 
				
			||||||
 | 
					    if let Some(pid) = pgrep(cmd) { | 
				
			||||||
 | 
					        Command::new("kill") | 
				
			||||||
 | 
					            .args(&["-9", &pid.to_string()]) | 
				
			||||||
 | 
					            .status() | 
				
			||||||
 | 
					            .ok(); | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn monitor_connection(ssh_host: &str) -> ! { | 
				
			||||||
 | 
					    let config = TinySSHConfig::parse(BASE_CONFIG).expect("ssh config file has wrong format"); | 
				
			||||||
 | 
					    let host = config.get_host(ssh_host).expect("no host found in config"); | 
				
			||||||
 | 
					    assert!(host.can_be_proxy()); | 
				
			||||||
 | 
					    let proxy = Proxy::all(&format!( | 
				
			||||||
 | 
					        "socks5://127.0.0.1:{}", | 
				
			||||||
 | 
					        host.get("DynamicForward").unwrap() | 
				
			||||||
 | 
					    )) | 
				
			||||||
 | 
					    .unwrap(); | 
				
			||||||
 | 
					    let mut headers = header::HeaderMap::new(); | 
				
			||||||
 | 
					    headers.insert( | 
				
			||||||
 | 
					        header::USER_AGENT, | 
				
			||||||
 | 
					        header::HeaderValue::from_static("curl/7.74.0"), | 
				
			||||||
 | 
					    ); | 
				
			||||||
 | 
					    let client = Client::builder() | 
				
			||||||
 | 
					        .proxy(proxy) | 
				
			||||||
 | 
					        .timeout(time::Duration::from_secs(10)) | 
				
			||||||
 | 
					        .default_headers(headers) | 
				
			||||||
 | 
					        .build() | 
				
			||||||
 | 
					        .unwrap(); | 
				
			||||||
 | 
					    let proxy_ip = host.get("HostName").unwrap(); | 
				
			||||||
 | 
					    let killer = |msg| { | 
				
			||||||
 | 
					        ThresholdCaller::get().append_fail(Some(msg)); | 
				
			||||||
 | 
					        kill_by_cmd(ssh_host); | 
				
			||||||
 | 
					    }; | 
				
			||||||
 | 
					    loop { | 
				
			||||||
 | 
					        if process_present(HOST) { | 
				
			||||||
 | 
					            match client.get("https://2ip.ru/").send() { | 
				
			||||||
 | 
					                Ok(resp) => match resp.text() { | 
				
			||||||
 | 
					                    Ok(received_ip) => { | 
				
			||||||
 | 
					                        if received_ip.trim() != *proxy_ip { | 
				
			||||||
 | 
					                            killer("wrong ip"); | 
				
			||||||
 | 
					                        } else { | 
				
			||||||
 | 
					                            if ThresholdCaller::get().was_triggered() { | 
				
			||||||
 | 
					                                notify("reconnected", false); | 
				
			||||||
 | 
					                            } | 
				
			||||||
 | 
					                            ThresholdCaller::get().reset(); | 
				
			||||||
 | 
					                        } | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                    Err(_) => { | 
				
			||||||
 | 
					                        killer("can't receive response"); | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                }, | 
				
			||||||
 | 
					                Err(_) => { | 
				
			||||||
 | 
					                    killer("can't send request"); | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        thread::sleep(time::Duration::from_secs(10)); | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn monitor_process() -> ! { | 
				
			||||||
 | 
					    loop { | 
				
			||||||
 | 
					        if !process_present(HOST) { | 
				
			||||||
 | 
					            match run_proxy(HOST) { | 
				
			||||||
 | 
					                Ok(result) => { | 
				
			||||||
 | 
					                    let exited = result.wait_with_output().unwrap(); | 
				
			||||||
 | 
					                    if !exited.status.success() { | 
				
			||||||
 | 
					                        ThresholdCaller::get() | 
				
			||||||
 | 
					                            .append_fail(Some(String::from_utf8(exited.stderr).unwrap())); | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					                Err(e) => { | 
				
			||||||
 | 
					                    ThresholdCaller::get().append_fail(Some(e.to_string())); | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            ThresholdCaller::get().call_once(None); | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        thread::sleep(time::Duration::from_secs(1)); | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn main() { | 
				
			||||||
 | 
					    panic::set_hook(Box::new(|info| { | 
				
			||||||
 | 
					        notify(info.to_string(), true); | 
				
			||||||
 | 
					        exit(1); | 
				
			||||||
 | 
					    })); | 
				
			||||||
 | 
					    let mut args = env::args(); | 
				
			||||||
 | 
					    if let Some(arg) = args.nth(1) { | 
				
			||||||
 | 
					        if arg == "-d" { | 
				
			||||||
 | 
					            daemonize().unwrap(); | 
				
			||||||
 | 
					            unsafe { | 
				
			||||||
 | 
					                DAEMONIZED = true; | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    thread::spawn(|| monitor_connection(HOST)); | 
				
			||||||
 | 
					    monitor_process(); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)] | 
				
			||||||
 | 
					mod tests { | 
				
			||||||
 | 
					    use super::*; | 
				
			||||||
 | 
					    use std::error::Error; | 
				
			||||||
 | 
					    type TestResult = Result<(), Box<dyn Error>>; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test] | 
				
			||||||
 | 
					    fn test_ssh_config_parser() -> TestResult { | 
				
			||||||
 | 
					        let config = TinySSHConfig::parse(BASE_CONFIG)?; | 
				
			||||||
 | 
					        assert_eq!( | 
				
			||||||
 | 
					            config.get_host("aws2").unwrap().get("HostName").unwrap(), | 
				
			||||||
 | 
					            "35.177.229.131" | 
				
			||||||
 | 
					        ); | 
				
			||||||
 | 
					        Ok(()) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test] | 
				
			||||||
 | 
					    fn test_ssh_host_can_be_proxy() -> TestResult { | 
				
			||||||
 | 
					        let config = TinySSHConfig::parse(BASE_CONFIG)?; | 
				
			||||||
 | 
					        assert!(config.get_host("aws-tun").unwrap().can_be_proxy()); | 
				
			||||||
 | 
					        Ok(()) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,120 @@ | 
				
			|||||||
 | 
					use std::collections::HashMap; | 
				
			||||||
 | 
					use std::fs::read_to_string; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct LineLexer { | 
				
			||||||
 | 
					    linenum: usize, | 
				
			||||||
 | 
					    content: Vec<String>, | 
				
			||||||
 | 
					    current: String, | 
				
			||||||
 | 
					    previous: String, | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl LineLexer { | 
				
			||||||
 | 
					    pub fn new(data: String) -> Self { | 
				
			||||||
 | 
					        Self { | 
				
			||||||
 | 
					            linenum: 0, | 
				
			||||||
 | 
					            content: data.lines().map(String::from).collect(), | 
				
			||||||
 | 
					            current: String::new(), | 
				
			||||||
 | 
					            previous: String::new(), | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn next(&mut self) -> Option<String> { | 
				
			||||||
 | 
					        let line = self.content.get(self.linenum); | 
				
			||||||
 | 
					        if line.is_some() { | 
				
			||||||
 | 
					            self.previous = self.current.clone(); | 
				
			||||||
 | 
					            self.current = line.unwrap().trim().to_string(); | 
				
			||||||
 | 
					            self.linenum += 1; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        line.map(|s| s.clone()) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn next_if_nonempty(&mut self) -> Option<String> { | 
				
			||||||
 | 
					        let l = self.next(); | 
				
			||||||
 | 
					        if l.is_none() || l.as_ref().unwrap().len() > 0 { | 
				
			||||||
 | 
					            l | 
				
			||||||
 | 
					        } else { | 
				
			||||||
 | 
					            None | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn next_nonempty(&mut self) -> Option<String> { | 
				
			||||||
 | 
					        loop { | 
				
			||||||
 | 
					            let l = self.next(); | 
				
			||||||
 | 
					            if l.is_none() { | 
				
			||||||
 | 
					                return l; | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            if l.as_ref().unwrap().len() > 0 { | 
				
			||||||
 | 
					                return l; | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)] | 
				
			||||||
 | 
					pub struct TinySSHHost { | 
				
			||||||
 | 
					    hostname: String, | 
				
			||||||
 | 
					    entries: HashMap<String, String>, | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TinySSHHost { | 
				
			||||||
 | 
					    pub fn new(hostname: String) -> Self { | 
				
			||||||
 | 
					        TinySSHHost { | 
				
			||||||
 | 
					            hostname, | 
				
			||||||
 | 
					            entries: HashMap::new(), | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get(&self, k: &str) -> Option<&String> { | 
				
			||||||
 | 
					        self.entries.get(&k.to_string()) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn can_be_proxy(&self) -> bool { | 
				
			||||||
 | 
					        self.get("DynamicForward").is_some() | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct TinySSHConfig { | 
				
			||||||
 | 
					    hosts: Vec<TinySSHHost>, | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TinySSHConfig { | 
				
			||||||
 | 
					    pub fn parse<S: Into<String>>(filename: S) -> Result<Self, String> { | 
				
			||||||
 | 
					        let data = read_to_string(filename.into()).map_err(|e| e.to_string())?; | 
				
			||||||
 | 
					        let mut tsc = Self { hosts: vec![] }; | 
				
			||||||
 | 
					        let mut lexer = LineLexer::new(data); | 
				
			||||||
 | 
					        loop { | 
				
			||||||
 | 
					            let line = lexer.next_nonempty(); | 
				
			||||||
 | 
					            if line.is_none() { | 
				
			||||||
 | 
					                break; | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            let mut host = if lexer.current.starts_with("Host") { | 
				
			||||||
 | 
					                match lexer.current.split_whitespace().nth(1) { | 
				
			||||||
 | 
					                    Some(h) => TinySSHHost::new(h.trim().to_string()), | 
				
			||||||
 | 
					                    None => return Err("No hostname found".to_string()), | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } else { | 
				
			||||||
 | 
					                return Err("Wrong section order".to_string()); | 
				
			||||||
 | 
					            }; | 
				
			||||||
 | 
					            loop { | 
				
			||||||
 | 
					                let l = lexer.next_if_nonempty(); | 
				
			||||||
 | 
					                if l.is_none() { | 
				
			||||||
 | 
					                    break; | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					                let l = l.unwrap(); | 
				
			||||||
 | 
					                let mut param: Vec<String> = l.trim().split(' ').map(String::from).collect(); | 
				
			||||||
 | 
					                if param.len() != 2 { | 
				
			||||||
 | 
					                    return Err(format!("Wrong param format: {}", l)); | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					                let v = param.pop().unwrap(); | 
				
			||||||
 | 
					                let k = param.pop().unwrap(); | 
				
			||||||
 | 
					                host.entries.insert(k, v); | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            tsc.hosts.push(host); | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        Ok(tsc) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_host(&self, host: &str) -> Option<&TinySSHHost> { | 
				
			||||||
 | 
					        self.hosts.iter().find(|h| h.hostname == host) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue