#![allow(clippy::field_reassign_with_default)]
use std::env;
use std::error;
use std::fmt;
use std::sync::Arc;
use chrono::offset::Utc;
use chrono::DateTime;
use reqwest::{header, Client, Method, Request, StatusCode, Url};
use serde::{Deserialize, Serialize};
const ENDPOINT: &str = "https://api.tailscale.com/api/v2/";
pub struct Tailscale {
key: String,
domain: String,
client: Arc<Client>,
}
impl Tailscale {
pub fn new<K, D>(key: K, domain: D) -> Self
where
K: ToString,
D: ToString,
{
let client = Client::builder().build();
match client {
Ok(c) => Self {
key: key.to_string(),
domain: domain.to_string(),
client: Arc::new(c),
},
Err(e) => panic!("creating client failed: {:?}", e),
}
}
pub fn new_from_env() -> Self {
let key = env::var("TAILSCALE_API_KEY").unwrap();
let domain = env::var("TAILSCALE_DOMAIN").unwrap();
Tailscale::new(key, domain)
}
fn request<B>(&self, method: Method, path: &str, body: B, query: Option<Vec<(&str, String)>>) -> Request
where
B: Serialize,
{
let base = Url::parse(ENDPOINT).unwrap();
let url = base.join(path).unwrap();
let mut headers = header::HeaderMap::new();
headers.append(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"));
let mut rb = self.client.request(method.clone(), url).headers(headers).basic_auth(&self.key, Some(""));
match query {
None => (),
Some(val) => {
rb = rb.query(&val);
}
}
if method != Method::GET && method != Method::DELETE {
rb = rb.json(&body);
}
rb.build().unwrap()
}
pub async fn list_devices(&self) -> Result<Vec<Device>, APIError> {
let request = self.request(Method::GET, &format!("domain/{}/devices", self.domain), (), None);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
let r: APIResponse = resp.json().await.unwrap();
Ok(r.devices)
}
pub async fn delete_device(&self, device_id: &str) -> Result<(), APIError> {
let request = self.request(Method::DELETE, &format!("device/{}", device_id), (), None);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
Ok(())
}
}
pub struct APIError {
pub status_code: StatusCode,
pub body: String,
}
impl fmt::Display for APIError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "APIError: status code -> {}, body -> {}", self.status_code.to_string(), self.body)
}
}
impl fmt::Debug for APIError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "APIError: status code -> {}, body -> {}", self.status_code.to_string(), self.body)
}
}
impl error::Error for APIError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
None
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct APIResponse {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub devices: Vec<Device>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Device {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub addresses: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty", rename = "allowedIPs")]
pub allowed_ips: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty", rename = "extraIPs")]
pub extra_ips: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub endpoints: Vec<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub derp: String,
#[serde(default, skip_serializing_if = "String::is_empty", rename = "clientVersion")]
pub client_version: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub os: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub name: String,
pub created: DateTime<Utc>,
#[serde(rename = "lastSeen")]
pub last_seen: DateTime<Utc>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub hostname: String,
#[serde(default, skip_serializing_if = "String::is_empty", rename = "machineKey")]
pub machine_key: String,
#[serde(default, skip_serializing_if = "String::is_empty", rename = "nodeKey")]
pub node_key: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub id: String,
#[serde(default, skip_serializing_if = "String::is_empty", rename = "displayNodeKey")]
pub display_node_key: String,
#[serde(default, skip_serializing_if = "String::is_empty", rename = "logID")]
pub log_id: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub user: String,
pub expires: DateTime<Utc>,
#[serde(default, rename = "neverExpires")]
pub never_expires: bool,
#[serde(default)]
pub authorized: bool,
#[serde(default, rename = "isExternal")]
pub is_external: bool,
#[serde(default, rename = "updateAvailable")]
pub update_available: bool,
#[serde(default, rename = "routeAll")]
pub route_all: bool,
#[serde(default, rename = "hasSubnet")]
pub has_subnet: bool,
}