220 lines
5.8 KiB
Rust
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>,
|
|
}
|