i3status_rs/blocks/vpn/
nordvpn.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
use regex::Regex;
use std::process::Stdio;
use tokio::process::Command;

use crate::blocks::prelude::*;
use crate::util::country_flag_from_iso_code;

use super::{Driver, Status};

pub struct NordVpnDriver {
    regex_country_code: Regex,
}

impl NordVpnDriver {
    pub async fn new() -> NordVpnDriver {
        NordVpnDriver {
            regex_country_code: Regex::new("^.*Hostname:\\s+([a-z]{2}).*$").unwrap(),
        }
    }

    async fn run_network_command(arg: &str) -> Result<()> {
        Command::new("nordvpn")
            .args([arg])
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .spawn()
            .error(format!("Problem running nordvpn command: {arg}"))?
            .wait()
            .await
            .error(format!("Problem running nordvpn command: {arg}"))?;
        Ok(())
    }

    async fn find_line(stdout: &str, needle: &str) -> Option<String> {
        stdout
            .lines()
            .find(|s| s.contains(needle))
            .map(|s| s.to_owned())
    }
}

#[async_trait]
impl Driver for NordVpnDriver {
    async fn get_status(&self) -> Result<Status> {
        let stdout = Command::new("nordvpn")
            .args(["status"])
            .output()
            .await
            .error("Problem running nordvpn command")?
            .stdout;

        let stdout = String::from_utf8(stdout).error("nordvpn produced non-UTF8 output")?;
        let line_status = Self::find_line(&stdout, "Status:").await;
        let line_country = Self::find_line(&stdout, "Country:").await;
        let line_country_flag = Self::find_line(&stdout, "Hostname:").await;
        if line_status.is_none() {
            return Ok(Status::Error);
        }
        let line_status = line_status.unwrap();

        if line_status.ends_with("Disconnected") {
            return Ok(Status::Disconnected);
        } else if line_status.ends_with("Connected") {
            let country = match line_country {
                Some(country_line) => country_line.rsplit(": ").next().unwrap().to_string(),
                None => String::default(),
            };
            let country_flag = match line_country_flag {
                Some(country_line_flag) => self
                    .regex_country_code
                    .captures_iter(&country_line_flag)
                    .last()
                    .map(|capture| capture[1].to_owned())
                    .map(|code| code.to_uppercase())
                    .map(|code| country_flag_from_iso_code(&code))
                    .unwrap_or_default(),
                None => String::default(),
            };
            return Ok(Status::Connected {
                country,
                country_flag,
            });
        }
        Ok(Status::Error)
    }

    async fn toggle_connection(&self, status: &Status) -> Result<()> {
        match status {
            Status::Connected { .. } => Self::run_network_command("disconnect").await?,
            Status::Disconnected => Self::run_network_command("connect").await?,
            Status::Error => (),
        }
        Ok(())
    }
}