183 lines
4.4 KiB
Rust
183 lines
4.4 KiB
Rust
use chrono::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::env::VarError;
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum Error {
|
|
#[error("serde error: {0}")]
|
|
Serde(#[from] serde_json::Error),
|
|
|
|
#[error("http error: {0}")]
|
|
Reqwest(#[from] reqwest::Error),
|
|
|
|
#[error("no such envvar: {0}")]
|
|
NoSuchEnvvar(#[from] VarError),
|
|
}
|
|
|
|
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 {
|
|
use super::*;
|
|
use std::env::var as envvar;
|
|
|
|
fn client() -> Result<Client> {
|
|
Client::new(
|
|
envvar("TAILSCALE_TAILNET")?,
|
|
envvar("TAILSCALE_API_KEY")?,
|
|
"tailscale_api::test".into(),
|
|
)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn basic_tests() {
|
|
async fn inner() -> Result {
|
|
let cli = client()?;
|
|
|
|
cli.devices().await?;
|
|
cli.get_nameservers().await?;
|
|
//cli.set_nameservers(ns).await?;
|
|
cli.get_acl().await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
if let Err(why) = inner().await {
|
|
println!("error running tests: {}", why)
|
|
}
|
|
}
|
|
}
|