devops_cli/
tunnelblick.rs

1#[cfg(target_os = "macos")]
2use osascript::{Error, JavaScript};
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6pub const TUNNELBLICK_CONFIG: &str = "tunnelblick";
7
8type Result<T> = std::result::Result<T, TunnelblickError>;
9
10#[cfg(target_os = "macos")]
11pub fn get_status() -> Result<Vec<Vpn>> {
12    let script = JavaScript::new(
13        r##"
14var tblk = Application('Tunnelblick')
15var configs = []
16
17var cfg = tblk.configurations().length
18for(let i = 0;i<cfg;i++) {
19  let c = tblk.configurations[i];
20  configs.push({name: c.name(),  state: c.state()})
21}
22return configs
23    "##,
24    );
25
26    Ok(script.execute()?)
27}
28
29#[cfg(not(target_os = "macos"))]
30pub fn get_status() -> Result<Vec<Vpn>> {
31    Err(TunnelblickError::UnsupportedPlatform)
32}
33
34#[cfg(target_os = "macos")]
35pub fn connect(vpn_name: &str) -> Result<ChangeResult> {
36    let result = JavaScript::new(
37        r##"var changed = Application('Tunnelblick').connect($params);return {changed: changed};"##,
38    )
39    .execute_with_params(vpn_name)?;
40
41    Ok(result)
42}
43
44#[cfg(not(target_os = "macos"))]
45pub fn connect(_vpn_name: &str) -> Result<ChangeResult> {
46    Err(TunnelblickError::UnsupportedPlatform)
47}
48
49#[cfg(target_os = "macos")]
50pub fn disconnect(vpn_name: &str) -> Result<ChangeResult> {
51    let result =
52        JavaScript::new(r##"var changed = Application('Tunnelblick').disconnect($params);return {changed: changed};"##)
53            .execute_with_params(vpn_name)?;
54
55    Ok(result)
56}
57
58#[cfg(not(target_os = "macos"))]
59pub fn disconnect(_vpn_name: &str) -> Result<ChangeResult> {
60    Err(TunnelblickError::UnsupportedPlatform)
61}
62
63#[cfg(target_os = "macos")]
64pub fn disconnect_all() -> Result<DisconnectResult> {
65    let result = JavaScript::new(
66        r##"var count = Application("Tunnelblick").disconnectAll();return {count: count};"##,
67    )
68    .execute()?;
69
70    Ok(result)
71}
72
73#[cfg(not(target_os = "macos"))]
74pub fn disconnect_all() -> Result<DisconnectResult> {
75    Err(TunnelblickError::UnsupportedPlatform)
76}
77
78#[derive(Deserialize)]
79pub struct ChangeResult {
80    pub changed: bool,
81}
82
83#[derive(Deserialize)]
84pub struct DisconnectResult {
85    pub count: i32,
86}
87
88#[derive(Deserialize, Serialize, Eq, PartialEq)]
89pub struct Vpn {
90    pub name: String,
91    pub state: State,
92}
93
94#[derive(Deserialize, Serialize, Eq, PartialEq, Debug)]
95#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
96pub enum State {
97    Connected,
98    Auth,
99    GetConfig,
100    Exiting,
101    Disconnecting,
102    #[serde(other)]
103    Unknown,
104}
105
106#[derive(Error, Debug)]
107pub enum TunnelblickError {
108    #[cfg(target_os = "macos")]
109    #[error("Unable to parse response from tunnelblick")]
110    ScriptResponseError(#[source] osascript::Error),
111
112    #[cfg(target_os = "macos")]
113    #[error("Unable to run osascript to control tunnelblick")]
114    ScriptExecutionError(#[source] osascript::Error),
115
116    #[cfg(target_os = "macos")]
117    #[error("The script to control tunnelblick is not compatible with your version")]
118    ScriptNotCompatible(#[source] osascript::Error),
119
120    #[error("Tunnelblick is only supported on macOS")]
121    UnsupportedPlatform,
122}
123
124#[cfg(target_os = "macos")]
125impl From<osascript::Error> for TunnelblickError {
126    fn from(e: Error) -> Self {
127        match e {
128            Error::Io(_) => TunnelblickError::ScriptExecutionError(e),
129            Error::Json(_) => TunnelblickError::ScriptResponseError(e),
130            Error::Script(_) => TunnelblickError::ScriptNotCompatible(e),
131        }
132    }
133}
134
135pub async fn wait_for_state<F>(
136    wait: std::time::Duration,
137    retries: u32,
138    f: F,
139) -> anyhow::Result<bool>
140where
141    F: Fn(Vec<Vpn>) -> anyhow::Result<bool>,
142{
143    for _ in 1..=retries {
144        let status = get_status()?;
145        match f(status) {
146            Ok(false) => tokio::time::sleep(wait).await,
147            failure_or_success => return failure_or_success,
148        }
149    }
150
151    Ok(false)
152}