rebterlai/crates/tailscale-api/src/lib.rs

155 lines
3.8 KiB
Rust

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<T = ()> = std::result::Result<T, Error>;
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<Self> {
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<Vec<Device>> {
#[derive(Deserialize)]
struct DevicesResp {
devices: Vec<Device>,
}
let result: DevicesResp = self
.client
.get(&format!(
"{}/api/v2/tailnet/{}/devices",
self.base_url, self.domain
))
.basic_auth(&self.api_key, None::<String>)
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(result.devices)
}
pub async fn get_acl(&self) -> Result<acl::Acl> {
Ok(self
.client
.get(&format!(
"{}/api/v2/tailnet/{}/acl",
self.base_url, self.domain
))
.basic_auth(&self.api_key, None::<String>)
.header("Accept", "application/json")
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn get_nameservers(&self) -> Result<Vec<String>> {
#[derive(Deserialize)]
struct NameserverResp {
dns: Vec<String>,
}
let result: NameserverResp = self
.client
.get(&format!(
"{}/api/v2/tailnet/{}/dns/nameservers",
self.base_url, self.domain
))
.basic_auth(&self.api_key, None::<String>)
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(result.dns)
}
pub async fn set_nameservers(&self, servers: Vec<String>) -> Result {
#[derive(Serialize)]
struct NameserverReq {
dns: Vec<String>,
}
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<String>,
pub authorized: bool,
pub blocks_incoming_connections: bool,
pub client_version: String,
// pub created: DateTime<Utc>,
pub expires: DateTime<Utc>,
pub hostname: String,
pub id: String,
pub is_external: bool,
pub key_expiry_disabled: bool,
pub last_seen: DateTime<Utc>,
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);
}
}