tailscale-api/src/lib.rs

220 lines
5.8 KiB
Rust

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<T = ()> = std::result::Result<T, Error>;
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<Nameservers> {
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<String>) -> Result<SetNameserverResponse> {
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<SearchPaths> {
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<String>) -> Result<SearchPaths> {
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<Devices> {
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<String>,
}
impl From<Vec<String>> for Nameservers {
fn from(addrs: Vec<String>) -> 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<String>,
}
impl From<Vec<String>> for SearchPaths {
fn from(domains: Vec<String>) -> Self {
Self {
search_paths: domains,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Device {
pub addresses: Vec<String>,
#[serde(rename = "allowedIPs")]
pub allowed_ips: Vec<String>,
#[serde(rename = "extraIPs")]
pub extra_ips: Vec<String>,
pub endpoints: Vec<String>,
pub derp: String,
pub client_version: String,
pub os: String,
pub name: String,
pub created: DateTime<Utc>,
pub last_seen: DateTime<Utc>,
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<Utc>,
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<Device>,
}