add start of tailscale API client
Signed-off-by: Christine Dodrill <me@christine.website>
This commit is contained in:
parent
1b3d12435a
commit
5b2c73ba1f
|
@ -2,6 +2,25 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
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]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -79,6 +98,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-queue"
|
name = "crossbeam-queue"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -148,6 +176,18 @@ dependencies = [
|
||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -433,6 +473,9 @@ name = "ipnet"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
|
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
|
@ -544,6 +587,16 @@ version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
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]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.7.13"
|
version = "0.7.13"
|
||||||
|
@ -796,6 +849,7 @@ version = "0.11.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22"
|
checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-compression",
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
@ -814,9 +868,11 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustls",
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
|
"tokio-util",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
@ -1024,6 +1080,19 @@ dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tailscale-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"ipnet",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempdir"
|
name = "tempdir"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
|
|
|
@ -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"] }
|
|
@ -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<Rule>,
|
||||||
|
pub groups: BTreeMap<String, Vec<String>>,
|
||||||
|
#[serde(rename = "tagowners")]
|
||||||
|
pub tag_owners: BTreeMap<String, Vec<String>>,
|
||||||
|
pub hosts: BTreeMap<String, String>,
|
||||||
|
pub tests: Vec<Test>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
|
||||||
|
pub struct Rule {
|
||||||
|
pub action: String,
|
||||||
|
pub users: Vec<String>,
|
||||||
|
pub ports: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
|
||||||
|
pub struct Test {
|
||||||
|
pub users: Vec<String>,
|
||||||
|
pub allow: Option<Vec<String>>,
|
||||||
|
pub deny: Option<Vec<String>>,
|
||||||
|
}
|
|
@ -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<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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue