1#![allow(clippy::field_reassign_with_default)]
25use std::env;
26use std::error;
27use std::fmt;
28use std::sync::Arc;
29
30use chrono::offset::Utc;
31use chrono::DateTime;
32use reqwest::{header, Client, Method, Request, StatusCode, Url};
33use serde::{Deserialize, Serialize};
34
35const ENDPOINT: &str = "https://api.tailscale.com/api/v2/";
37
38pub struct Tailscale {
40 key: String,
41 domain: String,
42
43 client: Arc<Client>,
44}
45
46impl Tailscale {
47 pub fn new<K, D>(key: K, domain: D) -> Self
51 where
52 K: ToString,
53 D: ToString,
54 {
55 let client = Client::builder().build();
56 match client {
57 Ok(c) => Self {
58 key: key.to_string(),
59 domain: domain.to_string(),
60
61 client: Arc::new(c),
62 },
63 Err(e) => panic!("creating client failed: {:?}", e),
64 }
65 }
66
67 pub fn new_from_env() -> Self {
72 let key = env::var("TAILSCALE_API_KEY").unwrap();
73 let domain = env::var("TAILSCALE_DOMAIN").unwrap();
74
75 Tailscale::new(key, domain)
76 }
77
78 fn request<B>(&self, method: Method, path: &str, body: B, query: Option<Vec<(&str, String)>>) -> Request
79 where
80 B: Serialize,
81 {
82 let base = Url::parse(ENDPOINT).unwrap();
83 let url = base.join(path).unwrap();
84
85 let mut headers = header::HeaderMap::new();
87 headers.append(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"));
88
89 let mut rb = self.client.request(method.clone(), url).headers(headers).basic_auth(&self.key, Some(""));
90
91 match query {
92 None => (),
93 Some(val) => {
94 rb = rb.query(&val);
95 }
96 }
97
98 if method != Method::GET && method != Method::DELETE {
100 rb = rb.json(&body);
101 }
102
103 rb.build().unwrap()
105 }
106
107 pub async fn list_devices(&self) -> Result<Vec<Device>, APIError> {
109 let request = self.request(Method::GET, &format!("domain/{}/devices", self.domain), (), None);
112
113 let resp = self.client.execute(request).await.unwrap();
114 match resp.status() {
115 StatusCode::OK => (),
116 s => {
117 return Err(APIError {
118 status_code: s,
119 body: resp.text().await.unwrap(),
120 })
121 }
122 };
123
124 let r: APIResponse = resp.json().await.unwrap();
125
126 Ok(r.devices)
127 }
128
129 pub async fn delete_device(&self, device_id: &str) -> Result<(), APIError> {
131 let request = self.request(Method::DELETE, &format!("device/{}", device_id), (), None);
132
133 let resp = self.client.execute(request).await.unwrap();
134 match resp.status() {
135 StatusCode::OK => (),
136 s => {
137 return Err(APIError {
138 status_code: s,
139 body: resp.text().await.unwrap(),
140 })
141 }
142 };
143
144 Ok(())
145 }
146}
147
148pub struct APIError {
150 pub status_code: StatusCode,
151 pub body: String,
152}
153
154impl fmt::Display for APIError {
155 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
156 write!(f, "APIError: status code -> {}, body -> {}", self.status_code.to_string(), self.body)
157 }
158}
159
160impl fmt::Debug for APIError {
161 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
162 write!(f, "APIError: status code -> {}, body -> {}", self.status_code.to_string(), self.body)
163 }
164}
165
166impl error::Error for APIError {
168 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
169 None
171 }
172}
173
174#[derive(Clone, Debug, Default, Serialize, Deserialize)]
176pub struct APIResponse {
177 #[serde(default, skip_serializing_if = "Vec::is_empty")]
178 pub devices: Vec<Device>,
179}
180
181#[derive(Clone, Debug, Serialize, Deserialize)]
183pub struct Device {
184 #[serde(default, skip_serializing_if = "Vec::is_empty")]
185 pub addresses: Vec<String>,
186 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "allowedIPs")]
187 pub allowed_ips: Vec<String>,
188 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "extraIPs")]
189 pub extra_ips: Vec<String>,
190 #[serde(default, skip_serializing_if = "Vec::is_empty")]
191 pub endpoints: Vec<String>,
192 #[serde(default, skip_serializing_if = "String::is_empty")]
193 pub derp: String,
194 #[serde(default, skip_serializing_if = "String::is_empty", rename = "clientVersion")]
195 pub client_version: String,
196 #[serde(default, skip_serializing_if = "String::is_empty")]
197 pub os: String,
198 #[serde(default, skip_serializing_if = "String::is_empty")]
199 pub name: String,
200 pub created: DateTime<Utc>,
201 #[serde(rename = "lastSeen")]
202 pub last_seen: DateTime<Utc>,
203 #[serde(default, skip_serializing_if = "String::is_empty")]
204 pub hostname: String,
205 #[serde(default, skip_serializing_if = "String::is_empty", rename = "machineKey")]
206 pub machine_key: String,
207 #[serde(default, skip_serializing_if = "String::is_empty", rename = "nodeKey")]
208 pub node_key: String,
209 #[serde(default, skip_serializing_if = "String::is_empty")]
210 pub id: String,
211 #[serde(default, skip_serializing_if = "String::is_empty", rename = "displayNodeKey")]
212 pub display_node_key: String,
213 #[serde(default, skip_serializing_if = "String::is_empty", rename = "logID")]
214 pub log_id: String,
215 #[serde(default, skip_serializing_if = "String::is_empty")]
216 pub user: String,
217 pub expires: DateTime<Utc>,
218 #[serde(default, rename = "neverExpires")]
219 pub never_expires: bool,
220 #[serde(default)]
221 pub authorized: bool,
222 #[serde(default, rename = "isExternal")]
223 pub is_external: bool,
224 #[serde(default, rename = "updateAvailable")]
225 pub update_available: bool,
226 #[serde(default, rename = "routeAll")]
227 pub route_all: bool,
228 #[serde(default, rename = "hasSubnet")]
229 pub has_subnet: bool,
230}