tailscale_api/
lib.rs

1/*!
2 * A rust library for interacting with the Tailscale API.
3 *
4 * For more information, the Tailscale API is still in beta. The docs are
5 * here: https://github.com/tailscale/tailscale/blob/main/api.md
6 *
7 * Example:
8 *
9 * ```
10 * use serde::{Deserialize, Serialize};
11 * use tailscale_api::Tailscale;
12 *
13 * async fn get_devices() {
14 *     // Initialize the Tailscale client.
15 *     let tailscale = Tailscale::new_from_env();
16 *
17 *     // List the devices.
18 *     let devices = tailscale.list_devices().await.unwrap();
19 *
20 *     println!("{:?}", devices);
21 * }
22 * ```
23 */
24#![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
35/// Endpoint for the Tailscale API.
36const ENDPOINT: &str = "https://api.tailscale.com/api/v2/";
37
38/// Entrypoint for interacting with the Tailscale API.
39pub struct Tailscale {
40    key: String,
41    domain: String,
42
43    client: Arc<Client>,
44}
45
46impl Tailscale {
47    /// Create a new Tailscale client struct. It takes a type that can convert into
48    /// an &str (`String` or `Vec<u8>` for example). As long as the function is
49    /// given a valid API key your requests will work.
50    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    /// Create a new Tailscale client struct from environment variables. It
68    /// takes a type that can convert into
69    /// an &str (`String` or `Vec<u8>` for example). As long as the function is
70    /// given a valid API key and domain and your requests will work.
71    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        // Set the default headers.
86        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        // Add the body, this is to ensure our GET and DELETE calls succeed.
99        if method != Method::GET && method != Method::DELETE {
100            rb = rb.json(&body);
101        }
102
103        // Build the request.
104        rb.build().unwrap()
105    }
106
107    /// List devices.
108    pub async fn list_devices(&self) -> Result<Vec<Device>, APIError> {
109        // Build the request.
110        // TODO: paginate.
111        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    /// List devices.
130    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
148/// Error type returned by our library.
149pub 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
166// This is important for other errors to wrap this one.
167impl error::Error for APIError {
168    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
169        // Generic error, underlying cause isn't tracked.
170        None
171    }
172}
173
174/// The data type for an API response.
175#[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/// The data type for a device.
182#[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}