use anyhow::{bail, Context};
use derivative::Derivative;
use reqwest::{Certificate, Client};
use crate::secret_string::SecretString;
pub mod clock;
pub mod dhcp;
pub mod ethernet;
mod identity;
pub mod interface;
pub mod resource;
#[derive(Derivative, Clone)]
#[derivative(Debug)]
pub struct MikrotikClient {
#[derivative(Debug = "ignore")]
client: reqwest::Client,
#[derivative(Debug(format_with = "std::fmt::Display::fmt"))]
endpoint: url::Url,
username: String,
pass: SecretString,
}
#[derive(serde::Deserialize, Clone, Debug, Default)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum CertValidation {
#[default]
SystemRoots,
Insecure,
ManualCert {
#[serde(deserialize_with = "crate::certificate_serialization::deserialize_pem_string")]
cert: Certificate,
},
}
impl MikrotikClient {
pub fn new(
endpoint: url::Url,
username: String,
pass: SecretString,
cert_validation: CertValidation,
connect_timeout: std::time::Duration,
total_timeout: std::time::Duration,
) -> anyhow::Result<Self> {
let client = Client::builder()
.tls_built_in_root_certs(false)
.connect_timeout(connect_timeout)
.timeout(total_timeout);
let client = match cert_validation {
CertValidation::SystemRoots => client.tls_built_in_root_certs(true),
CertValidation::Insecure => client.danger_accept_invalid_certs(true),
CertValidation::ManualCert { cert } => client.add_root_certificate(cert),
};
let client = client
.build()
.with_context(|| "Failed to construct reqwest::Client")?;
Ok(Self {
client,
endpoint,
username,
pass,
})
}
#[must_use]
pub fn endpoint(&self) -> &url::Url {
&self.endpoint
}
async fn get_all<T>(&self, path: &str) -> anyhow::Result<T>
where
T: for<'a> serde::Deserialize<'a>,
{
let mut url = self.endpoint.clone();
url.set_path(path);
let response = self
.client
.get(url)
.basic_auth(&self.username, Some(&self.pass.0))
.send()
.await?;
let status = response.status();
if !status.is_success() {
let text = response.text().await.unwrap_or_default();
bail!(
"Failed to fetch {}: {status} | {text}",
std::any::type_name::<T>()
.rsplit_once("::")
.map_or(std::any::type_name::<T>(), |(_, r)| r.trim_end_matches('>'))
)
}
Ok(response.json().await?)
}
}
fn deserialize_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s: &str = serde::de::Deserialize::deserialize(deserializer)?;
match s {
"true" => Ok(true),
"false" => Ok(false),
_ => Err(serde::de::Error::unknown_variant(s, &["true", "false"])),
}
}
#[derive(Debug, Clone, Copy)]
pub enum AutoOrNumber {
Auto,
Value(u64),
}
impl<'de> serde::Deserialize<'de> for AutoOrNumber {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Ok(if s == "auto" {
Self::Auto
} else {
Self::Value(s.parse().map_err(|_| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&s),
&"unsigned integer or 'auto'",
)
})?)
})
}
}