use chrono::prelude::*; use serde::{Deserialize, Serialize}; static USER_AGENT_BASE: &str = concat!( "library", "/", env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"), " +https://tulpa.dev/cadey/tailscale-api", ); #[derive(thiserror::Error, Debug)] pub enum Error { #[error("io error: {0}")] IO(#[from] std::io::Error), #[error("serde error: {0}")] Serde(#[from] serde_json::Error), #[error("ureq error: {0}")] UReq(String), #[error("http unsuccessful: {0}")] HttpStatus(u16), } pub type Result = std::result::Result; pub struct Client { base_url: String, domain: String, api_key: String, user_agent: String, } impl Client { pub fn new(domain: String, api_key: String, user_agent: String) -> Self { Self { base_url: "https://api.tailscale.com".into(), domain: domain, api_key: api_key, user_agent: format!("{} {}", user_agent, USER_AGENT_BASE), } } pub fn get_nameservers(&self) -> Result { let resp = ureq::get(&format!( "{}/api/v2/domain/{}/dns/nameservers", self.base_url, self.domain )) .set("User-Agent", &self.user_agent) .auth(&self.api_key, "") .call(); if resp.ok() { Ok(resp.into_json_deserialize()?) } else { Err(match resp.synthetic_error() { Some(why) => Error::UReq(why.to_string()), None => Error::HttpStatus(resp.status()), }) } } pub fn set_nameservers(&self, list: Vec) -> Result { let data: Nameservers = list.into(); let val = serde_json::to_value(data)?; let resp = ureq::post(&format!( "{}/api/v2/domain/{}/dns/nameservers", self.base_url, self.domain, )) .set("User-Agent", &self.user_agent) .auth(&self.api_key, "") .send_json(val); if resp.ok() { Ok(resp.into_json_deserialize()?) } else { Err(match resp.synthetic_error() { Some(why) => Error::UReq(why.to_string()), None => Error::HttpStatus(resp.status()), }) } } pub fn get_search_paths(&self) -> Result { let resp = ureq::get(&format!( "{}/api/v2/domain/{}/dns/searchpaths", self.base_url, self.domain )) .set("User-Agent", &self.user_agent) .auth(&self.api_key, "") .call(); if resp.ok() { Ok(resp.into_json_deserialize()?) } else { Err(match resp.synthetic_error() { Some(why) => Error::UReq(why.to_string()), None => Error::HttpStatus(resp.status()), }) } } pub fn set_search_paths(&self, list: Vec) -> Result { let data: SearchPaths = list.into(); let val = serde_json::to_value(data)?; let resp = ureq::post(&format!( "{}/api/v2/domain/{}/dns/searchpaths", self.base_url, self.domain, )) .set("User-Agent", &self.user_agent) .auth(&self.api_key, "") .send_json(val); if resp.ok() { Ok(resp.into_json_deserialize()?) } else { Err(match resp.synthetic_error() { Some(why) => Error::UReq(why.to_string()), None => Error::HttpStatus(resp.status()), }) } } pub fn devices(&self) -> Result { let resp = ureq::get(&format!( "{}/api/v2/domain/{}/devices", self.base_url, self.domain )) .set("User-Agent", &self.user_agent) .auth(&self.api_key, "") .call(); if resp.ok() { Ok(resp.into_json_deserialize()?) } else { Err(match resp.synthetic_error() { Some(why) => Error::UReq(why.to_string()), None => Error::HttpStatus(resp.status()), }) } } } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Nameservers { pub dns: Vec, } impl From> for Nameservers { fn from(addrs: Vec) -> Self { Self { dns: addrs } } } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct MagicDNSSetting { #[serde(rename = "magicDNS")] pub magic_dns: bool, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SetNameserverResponse { #[serde(flatten)] pub ns: Nameservers, #[serde(flatten)] pub md: MagicDNSSetting, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SearchPaths { #[serde(rename = "searchPaths")] pub search_paths: Vec, } impl From> for SearchPaths { fn from(domains: Vec) -> Self { Self { search_paths: domains, } } } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Device { pub addresses: Vec, #[serde(rename = "allowedIPs")] pub allowed_ips: Vec, #[serde(rename = "extraIPs")] pub extra_ips: Vec, pub endpoints: Vec, pub derp: String, pub client_version: String, pub os: String, pub name: String, pub created: DateTime, pub last_seen: DateTime, pub hostname: String, pub machine_key: String, pub node_key: String, pub id: String, pub display_node_key: String, pub user: String, pub expires: DateTime, pub never_expires: bool, pub authorized: bool, pub is_external: bool, pub update_available: bool, pub route_all: bool, pub has_subnet: bool, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Devices { pub devices: Vec, }