diff --git a/Cargo.lock b/Cargo.lock index b7be701..92ca33c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,25 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "async-compression" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443ccbb270374a2b1055fc72da40e1f237809cd6bb0e97e66d264cd138473a6" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -79,6 +98,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.2" @@ -148,6 +176,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "flate2" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80edafed416a46fb378521624fab1cfa2eb514784fd8921adbe8a8d8321da811" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -433,6 +473,9 @@ name = "ipnet" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" +dependencies = [ + "serde", +] [[package]] name = "itoa" @@ -544,6 +587,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.7.13" @@ -796,6 +849,7 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" dependencies = [ + "async-compression", "base64", "bytes", "encoding_rs", @@ -814,9 +868,11 @@ dependencies = [ "pin-project-lite", "rustls", "serde", + "serde_json", "serde_urlencoded", "tokio", "tokio-rustls", + "tokio-util", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1024,6 +1080,19 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tailscale-api" +version = "0.1.0" +dependencies = [ + "chrono", + "ipnet", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", +] + [[package]] name = "tempdir" version = "0.3.7" diff --git a/crates/tailscale-api/Cargo.toml b/crates/tailscale-api/Cargo.toml new file mode 100644 index 0000000..aa02715 --- /dev/null +++ b/crates/tailscale-api/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tailscale-api" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +ipnet = { version = "2", features = ["serde"] } +reqwest = { version = "0.11", default-features = false, features = [ "json", "rustls-tls", "gzip" ] } +thiserror = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[dev-dependencies] +tokio = { version = "1", features = ["full"] } diff --git a/crates/tailscale-api/src/acl.rs b/crates/tailscale-api/src/acl.rs new file mode 100644 index 0000000..cc0e258 --- /dev/null +++ b/crates/tailscale-api/src/acl.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(thiserror::Error, Clone, Debug)] +pub enum Error { + #[error("user {0} not found in any group")] + UserNotFound(String), +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub struct Acl { + pub acls: Vec, + pub groups: BTreeMap>, + #[serde(rename = "tagowners")] + pub tag_owners: BTreeMap>, + pub hosts: BTreeMap, + pub tests: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub struct Rule { + pub action: String, + pub users: Vec, + pub ports: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub struct Test { + pub users: Vec, + pub allow: Option>, + pub deny: Option>, +} diff --git a/crates/tailscale-api/src/lib.rs b/crates/tailscale-api/src/lib.rs new file mode 100644 index 0000000..c57f851 --- /dev/null +++ b/crates/tailscale-api/src/lib.rs @@ -0,0 +1,154 @@ +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 = std::result::Result; + +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 { + 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> { + #[derive(Deserialize)] + struct DevicesResp { + devices: Vec, + } + + let result: DevicesResp = self + .client + .get(&format!( + "{}/api/v2/tailnet/{}/devices", + self.base_url, self.domain + )) + .basic_auth(&self.api_key, None::) + .send() + .await? + .error_for_status()? + .json() + .await?; + Ok(result.devices) + } + + pub async fn get_acl(&self) -> Result { + Ok(self + .client + .get(&format!( + "{}/api/v2/tailnet/{}/acl", + self.base_url, self.domain + )) + .basic_auth(&self.api_key, None::) + .header("Accept", "application/json") + .send() + .await? + .error_for_status()? + .json() + .await?) + } + + pub async fn get_nameservers(&self) -> Result> { + #[derive(Deserialize)] + struct NameserverResp { + dns: Vec, + } + + let result: NameserverResp = self + .client + .get(&format!( + "{}/api/v2/tailnet/{}/dns/nameservers", + self.base_url, self.domain + )) + .basic_auth(&self.api_key, None::) + .send() + .await? + .error_for_status()? + .json() + .await?; + Ok(result.dns) + } + + pub async fn set_nameservers(&self, servers: Vec) -> Result { + #[derive(Serialize)] + struct NameserverReq { + dns: Vec, + } + + 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, + pub authorized: bool, + pub blocks_incoming_connections: bool, + pub client_version: String, + // pub created: DateTime, + pub expires: DateTime, + pub hostname: String, + pub id: String, + pub is_external: bool, + pub key_expiry_disabled: bool, + pub last_seen: DateTime, + 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); + } +}