emixnet 0.6.0

Higher-level HTTP, mail, and VPN helpers layered on EssentialMix core utilities.
Documentation
use execute::Execute;
use std::process::{Command, Stdio};

use crate::Result;
use emix::Error;

#[derive(Default, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ExpressVPNStatus {
    #[default]
    Unknown,
    NotActivated,
    Connected(Option<String>),
    Disconnected,
    Error(String),
}

pub struct ExpressVPN;

impl ExpressVPN {
    const CMD: &'static str = "expressvpn";

    pub fn version(&self) -> Result<String> {
        let output = Command::new(Self::CMD)
            .arg("--version")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .execute_output()
            .map_err(|e| Error::from_std_error(e))?;
        let ret = match output.status.code() {
            Some(ret) => ret,
            None => {
                let stderr =
                    String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
                return Err(Error::Command(-1, stderr));
            }
        };

        if ret != 0 {
            let stderr = String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
            return Err(Error::Command(ret, stderr));
        }

        let text = String::from_utf8(output.stdout).map_err(|e| Error::from_std_error(e))?;
        let n = text
            .chars()
            .enumerate()
            .find(|(_, c)| c.is_ascii_digit())
            .map(|(i, _)| i)
            .ok_or_else(|| Error::Parse("Unable to find version number in output".to_string()))?;
        let text = text[n..].trim().to_string();
        Ok(text)
    }

    pub fn status(&self) -> Result<ExpressVPNStatus> {
        let output = Command::new(Self::CMD)
            .arg("status")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .execute_output()
            .map_err(|e| Error::from_std_error(e))?;
        let ret = match output.status.code() {
            Some(ret) => ret,
            None => {
                let stderr =
                    String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
                return Err(Error::Command(-1, stderr));
            }
        };

        if ret != 0 {
            let stderr = String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
            return Err(Error::Command(ret, stderr));
        }

        let text = String::from_utf8(output.stdout)
            .map_err(|e| Error::from_std_error(e))?
            .trim()
            .to_string();

        if text.contains("Not Activated") {
            return Ok(ExpressVPNStatus::NotActivated);
        }

        if text.contains("Not connected") {
            return Ok(ExpressVPNStatus::Disconnected);
        }

        if let Some(n) = text.find("Connected to") {
            let l = text[n..].find('\n').unwrap_or(text.len());
            let text = text[n..n + l].to_string();
            return Ok(ExpressVPNStatus::Connected(Some(text)));
        }

        if text.contains("connected") {
            return Ok(ExpressVPNStatus::Connected(None));
        }

        if text.contains("error") || text.contains("Oops!") || text.contains("problem") {
            return Ok(ExpressVPNStatus::Error(text));
        }

        Err(Error::Session(format!("Unknown VPN response: {}", text)))
    }

    pub fn connect(&self) -> Result<ExpressVPNStatus> {
        self.disconnect()?;
        self._connect(None)
    }

    pub fn connect_target(&self, value: &str) -> Result<ExpressVPNStatus> {
        self.disconnect()?;

        if value.is_empty() {
            self._connect(None)
        } else {
            self._connect(Some(value))
        }
    }

    fn _connect(&self, value: Option<&str>) -> Result<ExpressVPNStatus> {
        let mut command = Command::new(Self::CMD);
        command.arg("connect");

        if let Some(value) = value {
            if !value.is_empty() {
                command.arg(value);
            }
        }

        command.stdout(Stdio::piped()).stderr(Stdio::piped());

        let output = command.execute_output()?;
        let ret = match output.status.code() {
            Some(ret) => ret,
            None => {
                let stderr =
                    String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
                return Err(Error::Command(-1, stderr));
            }
        };

        if ret != 0 {
            let stderr = String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
            return Err(Error::Command(ret, stderr));
        }

        let text = String::from_utf8(output.stdout)
            .map_err(|e| Error::from_std_error(e))?
            .trim()
            .to_string();
        match text.find("Connected to") {
            Some(n) => {
                let l = text[n..].find('\n').unwrap_or(text.len());
                let text = text[n..n + l].to_string();
                Ok(ExpressVPNStatus::Connected(Some(text)))
            }
            None => {
                if text.contains("Canceled") {
                    return Err(Error::Canceled);
                }

                if text.contains("unexpectedly") {
                    return Err(Error::Network(text));
                }

                Err(Error::Session(format!("Unknown VPN response: {}", text)))
            }
        }
    }

    pub fn disconnect(&self) -> Result<ExpressVPNStatus> {
        let output = Command::new(Self::CMD)
            .arg("disconnect")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .execute_output()
            .map_err(|e| Error::from_std_error(e))?;
        let ret = match output.status.code() {
            Some(ret) => ret,
            None => {
                let stderr =
                    String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
                return Err(Error::Command(-1, stderr));
            }
        };

        if ret != 0 {
            let msg = String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;

            if msg.contains("Disconnected") {
                return Ok(ExpressVPNStatus::Disconnected);
            }

            return Err(Error::Command(ret, msg));
        }

        let text = String::from_utf8(output.stdout)
            .map_err(|e| Error::from_std_error(e))?
            .trim()
            .to_string();

        if text.contains("Disconnected") {
            return Ok(ExpressVPNStatus::Disconnected);
        }

        Err(Error::Session(format!("Unknown VPN response: {}", text)))
    }

    pub fn recent(&self) -> Result<Vec<String>> {
        let output = Command::new(Self::CMD)
            .arg("list")
            .arg("recent")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .execute_output()
            .map_err(|e| Error::from_std_error(e))?;
        let ret = match output.status.code() {
            Some(ret) => ret,
            None => {
                let stderr =
                    String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
                return Err(Error::Command(-1, stderr));
            }
        };

        if ret != 0 {
            let stderr = String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
            return Err(Error::Command(ret, stderr));
        }

        let text = String::from_utf8(output.stdout).map_err(|e| Error::from_std_error(e))?;
        Ok(self.locations_from_string(&text))
    }

    pub fn recommended(&self) -> Result<Vec<String>> {
        let output = Command::new(Self::CMD)
            .arg("list")
            .arg("recommended")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .execute_output()
            .map_err(|e| Error::from_std_error(e))?;
        let ret = match output.status.code() {
            Some(ret) => ret,
            None => {
                let stderr =
                    String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
                return Err(Error::Command(-1, stderr));
            }
        };

        if ret != 0 {
            let stderr = String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
            return Err(Error::Command(ret, stderr));
        }

        let text = String::from_utf8(output.stdout).map_err(|e| Error::from_std_error(e))?;
        Ok(self.locations_from_string(&text))
    }

    pub fn list(&self) -> Result<Vec<String>> {
        let output = Command::new(Self::CMD)
            .arg("list")
            .arg("all")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .execute_output()
            .map_err(|e| Error::from_std_error(e))?;
        let ret = match output.status.code() {
            Some(ret) => ret,
            None => {
                let stderr =
                    String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
                return Err(Error::Command(-1, stderr));
            }
        };

        if ret != 0 {
            let stderr = String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
            return Err(Error::Command(ret, stderr));
        }

        let text = String::from_utf8(output.stdout).map_err(|e| Error::from_std_error(e))?;
        Ok(self.locations_from_string(&text))
    }

    fn locations_from_string(&self, text: &str) -> Vec<String> {
        let mut locations = Vec::with_capacity(0);

        if text.is_empty() {
            return locations;
        }

        let n = text.find("-\n").unwrap_or(0);
        let l = text[n..].rfind("\n\n").unwrap_or(text.len());
        let text = text[n + 2..l].trim().to_string();

        for line in text.lines().filter(|line| !line.is_empty()) {
            let n = line.find(|c: char| c.is_whitespace()).unwrap_or(line.len());
            let line = line[..n].to_string();
            locations.push(line);
        }

        locations
    }

    pub fn refresh(&self) -> Result<()> {
        let output = Command::new(Self::CMD)
            .arg("refresh")
            .stderr(Stdio::piped())
            .execute_output()
            .map_err(|e| Error::from_std_error(e))?;
        let ret = match output.status.code() {
            Some(ret) => ret,
            None => {
                let stderr =
                    String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
                return Err(Error::Command(-1, stderr));
            }
        };

        if ret != 0 {
            let stderr = String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
            return Err(Error::Command(ret, stderr));
        }

        Ok(())
    }

    pub fn network_lock(&self, enable: bool) -> Result<()> {
        let output = Command::new(Self::CMD)
            .arg("preferences")
            .arg("set")
            .arg("network_lock")
            .arg(if enable { "on" } else { "off" })
            .stderr(Stdio::piped())
            .execute_output()
            .map_err(|e| Error::from_std_error(e))?;
        let ret = match output.status.code() {
            Some(ret) => ret,
            None => {
                let stderr =
                    String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
                return Err(Error::Command(-1, stderr));
            }
        };

        if ret != 0 {
            let stderr = String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
            return Err(Error::Command(ret, stderr));
        }

        Ok(())
    }

    pub fn block_trackers(&self, enable: bool) -> Result<()> {
        let output = Command::new(Self::CMD)
            .arg("preferences")
            .arg("set")
            .arg("block_trackers")
            .arg(if enable { "true" } else { "false" })
            .stderr(Stdio::piped())
            .execute_output()
            .map_err(|e| Error::from_std_error(e))?;
        let ret = match output.status.code() {
            Some(ret) => ret,
            None => {
                let stderr =
                    String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
                return Err(Error::Command(-1, stderr));
            }
        };

        if ret != 0 {
            let stderr = String::from_utf8(output.stderr).map_err(|e| Error::from_std_error(e))?;
            return Err(Error::Command(ret, stderr));
        }

        Ok(())
    }
}