1use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
2use reqwest::{Client, Response};
3use serde::{Deserialize, Serialize};
4use std::error::Error;
5
6pub struct TailscaleClient {
8 pub base_url: String,
9 pub token: String,
10 client: Client,
11}
12
13impl TailscaleClient {
14 pub fn new(token: String) -> Self {
17 TailscaleClient {
18 base_url: "https://api.tailscale.com/api/v2".to_string(),
19 token,
20 client: Client::new(),
21 }
22 }
23
24 async fn get(&self, path: &str) -> Result<Response, Box<dyn Error>> {
26 let mut headers = HeaderMap::new();
27 let auth_value = format!("Bearer {}", self.token);
28 headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);
29
30 let url = format!("{}/{}", self.base_url, path);
31 let resp = self.client.get(url).headers(headers).send().await?;
32 Ok(resp)
33 }
34
35 pub async fn whoami(&self) -> Result<WhoAmIResponse, Box<dyn Error>> {
38 let resp = self.get("whoami").await?;
39 if resp.status().is_success() {
40 let data: WhoAmIResponse = resp.json().await?;
41 Ok(data)
42 } else {
43 let error_body = resp.text().await?;
44 Err(format!("Tailscale whoami endpoint error: {}", error_body).into())
45 }
46 }
47
48 pub async fn create_auth_key(
52 &self,
53 tailnet: &str,
54 all: bool,
55 req_body: &CreateAuthKeyRequest,
56 ) -> Result<CreateAuthKeyResponse, Box<dyn Error>> {
57 let mut headers = HeaderMap::new();
58 let auth_value = format!("Bearer {}", self.token);
59 headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);
60 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
61
62 let url = format!("{}/tailnet/{}/keys?all={}", self.base_url, tailnet, all);
63
64 let resp = self
65 .client
66 .post(url)
67 .headers(headers)
68 .json(req_body)
69 .send()
70 .await?;
71
72 if resp.status().is_success() {
73 let data = resp.json().await?;
74 Ok(data)
75 } else {
76 let error_body = resp.text().await?;
77 Err(format!("Tailscale create_auth_key error: {}", error_body).into())
78 }
79 }
80}
81
82#[derive(Debug, Deserialize)]
84pub struct WhoAmIResponse {
85 pub logged_in: bool,
86 #[serde(rename = "user")]
87 pub user_info: Option<UserInfo>,
88 #[serde(rename = "tailnet")]
89 pub tailnet_info: Option<TailnetInfo>,
90}
91
92#[derive(Debug, Deserialize)]
94pub struct UserInfo {
95 pub login_name: Option<String>,
96 pub display_name: Option<String>,
97 pub profile_pic_url: Option<String>,
98}
99
100#[derive(Debug, Deserialize)]
102pub struct TailnetInfo {
103 pub name: Option<String>,
104 pub magic_dns: Option<bool>,
105}
106
107#[derive(Debug, Serialize)]
110pub struct CreateAuthKeyRequest {
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub description: Option<String>,
113
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub expirySeconds: Option<u64>,
116
117 pub capabilities: Capabilities,
118}
119
120#[derive(Debug, Serialize)]
122pub struct Capabilities {
123 pub devices: Devices,
124}
125
126#[derive(Debug, Serialize)]
128pub struct Devices {
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub create: Option<CreateOpts>,
131}
132
133#[derive(Debug, Serialize)]
135pub struct CreateOpts {
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub reusable: Option<bool>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub ephemeral: Option<bool>,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub preauthorized: Option<bool>,
144
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub tags: Option<Vec<String>>,
147}
148
149#[derive(Debug, Deserialize)]
151pub struct CreateAuthKeyResponse {
152 pub id: Option<String>,
153 pub key: Option<String>,
154 pub created: Option<String>,
155 pub expires: Option<String>,
156 pub revoked: Option<String>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub capabilities: Option<AuthKeyCapabilities>,
160
161 pub description: Option<String>,
162 pub invalid: Option<bool>,
163 pub userId: Option<String>,
164}
165
166#[derive(Debug, Deserialize)]
168pub struct AuthKeyCapabilities {
169 pub devices: Option<AuthKeyDevices>,
170}
171
172#[derive(Debug, Deserialize)]
173pub struct AuthKeyDevices {
174 pub create: Option<AuthKeyCreate>,
175}
176
177#[derive(Debug, Deserialize)]
178pub struct AuthKeyCreate {
179 pub reusable: Option<bool>,
180 pub ephemeral: Option<bool>,
181 pub preauthorized: Option<bool>,
182 pub tags: Option<Vec<String>>,
183}
184
185fn main() {
187 println!(
188 "Run the async example or tests to see the client in action. \
189 This file defines a base Tailscale client with a create_auth_key method."
190 );
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use std::env;
197
198 #[tokio::test]
208 async fn test_create_auth_key_integration() -> Result<(), Box<dyn Error>> {
209 let token = env::var("TAILSCALE_API_KEY")
210 .expect("Please set env var TAILSCALE_API_KEY with a valid Tailscale API key");
211 let tailnet = env::var("TAILSCALE_TAILNET").unwrap_or_else(|_| "-".to_string());
212
213 let client = TailscaleClient::new(token);
215
216 let request_body = CreateAuthKeyRequest {
218 description: Some("Integration test auth key".to_string()),
219 expirySeconds: None, capabilities: Capabilities {
221 devices: Devices {
222 create: Some(CreateOpts {
223 reusable: Some(true),
224 ephemeral: Some(false),
225 preauthorized: Some(false),
226 tags: Some(vec!["tag:example".to_string()]),
227 }),
228 },
229 },
230 };
231
232 let response = client
234 .create_auth_key(&tailnet, true, &request_body)
235 .await?;
236
237 println!("Create Auth Key response: {:#?}", response);
238
239 assert!(
241 response.key.is_some(),
242 "Expected some auth key in the `key` field"
243 );
244
245 Ok(())
246 }
247}