use chrono::prelude::*; use serde::{Deserialize, Serialize}; #[derive(thiserror::Error, Debug)] pub enum Error { #[error("serde error: {0}")] Serde(#[from] serde_json::Error), #[error("http error: {0}")] Reqwest(#[from] reqwest::Error), } pub type Result = std::result::Result; pub mod acl; static USER_AGENT_BASE: &str = concat!( "library", "/", env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"), "(+https://tulpa.dev/cadey/rebterlai)", ); pub struct Client { client: reqwest::Client, base_url: String, domain: String, api_key: String, } impl Client { pub fn new(domain: String, api_key: String, user_agent: String) -> Result { let client = reqwest::Client::builder() .use_rustls_tls() .user_agent(format!("{} {}", user_agent, USER_AGENT_BASE)) .gzip(true) .build()?; Ok(Self { client, base_url: "https://api.tailscale.com".to_string(), domain, api_key, }) } pub async fn devices(&self) -> Result> { #[derive(Deserialize)] struct DevicesResp { devices: Vec, } let result: DevicesResp = self .client .get(&format!( "{}/api/v2/tailnet/{}/devices", self.base_url, self.domain )) .basic_auth(&self.api_key, None::) .send() .await? .error_for_status()? .json() .await?; Ok(result.devices) } pub async fn get_acl(&self) -> Result { Ok(self .client .get(&format!( "{}/api/v2/tailnet/{}/acl", self.base_url, self.domain )) .basic_auth(&self.api_key, None::) .header("Accept", "application/json") .send() .await? .error_for_status()? .json() .await?) } pub async fn get_nameservers(&self) -> Result> { #[derive(Deserialize)] struct NameserverResp { dns: Vec, } let result: NameserverResp = self .client .get(&format!( "{}/api/v2/tailnet/{}/dns/nameservers", self.base_url, self.domain )) .basic_auth(&self.api_key, None::) .send() .await? .error_for_status()? .json() .await?; Ok(result.dns) } pub async fn set_nameservers(&self, servers: Vec) -> Result { #[derive(Serialize)] struct NameserverReq { dns: Vec, } self.client .post(&format!( "{}/api/v2/tailnet/{}/dns/nameservers", self.base_url, self.domain )) .json(&NameserverReq { dns: servers }) .send() .await? .error_for_status()?; Ok(()) } } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Device { pub addresses: Vec, pub authorized: bool, pub blocks_incoming_connections: bool, pub client_version: String, // pub created: DateTime, pub expires: DateTime, pub hostname: String, pub id: String, pub is_external: bool, pub key_expiry_disabled: bool, pub last_seen: DateTime, pub machine_key: String, pub name: String, pub node_key: String, pub os: String, pub update_available: bool, pub user: String, } #[cfg(test)] mod tests { #[tokio::test] async fn it_works() { assert_eq!(2 + 2, 4); } }