devops_cli/
tunnelblick.rs1#[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}