use std::fmt::Display; use std::marker::PhantomData; use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use log::trace; use reqwest::{Certificate, Request}; use reqwest::header::{HeaderName, HeaderValue}; use serde::de::DeserializeOwned; use tokio::sync::Semaphore; use crate::api::{DomainCollection, DomainExtention, DomainObject, DomainType}; use crate::{Error, Result}; pub struct Client(Arc); struct InnerClient { http: reqwest::Client, url: String, semaphore: Semaphore } #[derive(Default, PartialEq, Eq)] enum SslStrategy { NoSll, IngoreHostname, IgnoreCertificate, #[default] Strict } #[derive(Default)] pub struct RequiresLocation; #[derive(Default)] pub struct RequiresCredentials; #[derive(Default)] pub struct RequiresNothing; #[derive(Default)] pub struct ClientBuilder { hostname: String, sitename: String, username: String, password: String, ssl: SslStrategy, root_certificate: Option, resolve: Option, port: u16, _marker: PhantomData } impl ClientBuilder { fn new() -> ClientBuilder { Default::default() } } impl ClientBuilder { pub fn on_host_and_site(self, hostname: String, sitename: String) -> ClientBuilder { ClientBuilder:: { hostname, sitename, ..Default::default() } } } impl ClientBuilder { pub fn with_credentials(self, username: String, password: String) -> ClientBuilder { ClientBuilder { hostname: self.hostname, sitename: self.sitename, username, password, ..Default::default() } } } type ClientBuildResult = std::result::Result; #[derive(Debug, thiserror::Error)] pub enum ClientBuildError { #[error("failed to build client: {0}")] BuildClient(#[from] reqwest::Error), #[error("invalid credentials. thise can only consist of visibly ASCII characters (ie. (32-127))")] InvalidCredentials(#[from] reqwest::header::InvalidHeaderValue) } impl ClientBuilder { pub fn with_specific_port(mut self, port: u16) -> Self { self.port = port; self } pub fn without_ssl(mut self) -> Self { self.ssl = SslStrategy::NoSll; self } pub fn ignore_hostname_verification(mut self) -> Self { self.ssl = SslStrategy::IngoreHostname; self } pub fn ignore_certificate_veritifcation(mut self) -> Self { self.ssl = SslStrategy::IgnoreCertificate; self } pub fn with_root_certificate(mut self, certificate: Certificate) -> Self { let _ = self.root_certificate.insert(certificate); self } pub fn resolve(mut self, ipaddr: IpAddr) -> Self { let _ = self.resolve.insert(SocketAddr::new(ipaddr, 0)); self } fn protocol(&self) -> &str { if self.ssl == SslStrategy::NoSll { "http" } else { "https" } } fn basic_auth_header(&self) -> ClientBuildResult<(HeaderName, HeaderValue)> { let header = format!("Bearer {} {}", self.username, self.password); let mut header = HeaderValue::from_str(&header)?; header.set_sensitive(true); Ok((reqwest::header::AUTHORIZATION, header)) } pub fn build(self) -> Result { const API_BASE: &str = "check_mk/api/1.0"; let url = format!( "{}://{}:{}/{}/{API_BASE}", self.protocol(), self.hostname, self.port, self.sitename, ); trace!("checkmk url: {url}"); let http = { let headers = [self.basic_auth_header()?].into_iter().collect(); let mut builder = reqwest::ClientBuilder::new() .default_headers(headers) .danger_accept_invalid_certs(self.ssl == SslStrategy::IgnoreCertificate) .danger_accept_invalid_hostnames(self.ssl == SslStrategy::IngoreHostname); if let Some(sock) = self.resolve { builder = builder.resolve(&self.hostname, sock); } if let Some(certificate) = self.root_certificate { builder = builder.add_root_certificate(certificate); } builder.build() .map_err(ClientBuildError::BuildClient) .map_err(Error::BuildClient)? }; // checkmk api struggles with more than 10 requests at a time // not sure if this has been inproved since 2.2 let semaphore = Semaphore::new(10); Ok(Client(Arc::new(InnerClient { http, url, semaphore }))) } } impl Client { pub fn from_parts(url: String, client: reqwest::Client) -> Self { Self(Arc::new(InnerClient { http: client, semaphore: Semaphore::new(10), url })) } pub fn builder() -> ClientBuilder { Default::default() } } impl InnerClient { pub(crate) fn object_url(&self, domain_type: DomainType, id: impl Display) -> String { format!("{}/objects/{}/{}", self.url, domain_type, id) } pub(crate) fn collection_url(&self, domain_type: DomainType) -> String { format!("{}/domain-types/{}/collections/all", self.url, domain_type) } pub(crate) fn bulk_action_url(&self, domain_type: DomainType, action: BulkAction) -> String { format!("{}/domain-types/{}/actions/bulk-{}/invoke",self.url, domain_type, action) } pub(crate) async fn handle_request(&self, request: Request) -> Result { trace!("sending {}-request to {}", request.method(), request.url()); let response = self.http .execute(request) .await .map_err(Error::SendRequest)?; let status = response.status(); let body = response.text() .await .map_err(Error::ReceiveBody)?; if status.is_success() { Ok(body) } else { let cmkerror = serde_json::from_str(&body) .map_err(Error::DeserializeResponse)?; Err(Error::CheckmkError(cmkerror)) } } pub(crate) async fn invoke_api(&self, request: Request) -> Result<()> { let _response = self.handle_request(request).await?; Ok(()) } pub(crate) async fn query_api(&self, request: Request) -> Result { let response = self.handle_request(request).await?; serde_json::from_str(&response).map_err(Error::DeserializeResponse) } pub(crate) async fn get_etag(&self, object_url: &str) -> Result { let mut response = self.http .head(object_url) .send() .await .map_err(Error::SendRequest)?; if response.status().is_success() { response.headers_mut() .remove("ETag") .ok_or(Error::MissingHeader("ETag")) } else { let body = response.text() .await .map_err(Error::ReceiveBody)?; let cmkerror = serde_json::from_str(&body) .map_err(Error::DeserializeResponse)?; Err(Error::CheckmkError(cmkerror)) } } pub(crate) async fn create_domain_object( &self, body: &E::CreationRequest, query: &E::CreationQuery ) -> Result<()> { let request = self.http .post(self.collection_url(E::DOMAIN_TYPE)) .json(body) .query(query) .build() .unwrap(); self.invoke_api(request).await } pub(crate) async fn read_domain_object( &self, id: impl Display, query: &E::ReadQuery ) -> Result> { let request = self.http .get(self.object_url(E::DOMAIN_TYPE, id)) .query(query) .build() .unwrap(); self.query_api(request).await } pub(crate) async fn update_domain_object( &self, id: impl Display, request: &E::UpdateRequest, ) -> Result<()> { let url = self.object_url(E::DOMAIN_TYPE, id); let etag = self.get_etag(&url).await?; let request = self.http .put(url) .json(&request) .header(reqwest::header::IF_MATCH, etag) .build() .unwrap(); self.invoke_api(request).await } pub(crate) async fn delete_domain_object( &self, id: impl Display, params: &E::DeleteQuery, ) -> Result<()> { let request = self.http .delete(self.object_url(E::DOMAIN_TYPE, id)) .query(¶ms) .build() .unwrap(); self.invoke_api(request).await } pub(crate) async fn bulk_create_domain_objects( &self, body: &[E::CreationRequest], query: &E::CreationQuery ) -> Result<()> { let request = self.http .post(self.bulk_action_url(E::DOMAIN_TYPE, BulkAction::Create)) .json(body) .query(query) .build() .unwrap(); self.invoke_api(request).await } pub(crate) async fn bulk_read_domain_objects( &self, query: &E::ReadQuery, ) -> Result>> { let request = self.http .get(self.collection_url(E::DOMAIN_TYPE)) .query(query) .build() .unwrap(); let response: DomainCollection = self.query_api(request).await?; Ok(response.value) } pub(crate) async fn bulk_update_domain_objects( &self, body: &[E::UpdateRequest], ) -> Result<()> { let request = self.http .put(self.bulk_action_url(E::DOMAIN_TYPE, BulkAction::Update)) .json(body) .build() .unwrap(); self.invoke_api(request).await } pub(crate) async fn bulk_delete_domain_objects( &self, ids: &[String], ) -> Result<()> { let request = self.http .post(self.bulk_action_url(E::DOMAIN_TYPE, BulkAction::Delete)) .json(&ids) .build() .unwrap(); self.invoke_api(request).await } } pub(crate) enum BulkAction { Create, // Read, Update, Delete, } impl Display for BulkAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BulkAction::Create => write!(f, "create"), // BulkAction::Read => write!(f, "read"), BulkAction::Update => write!(f, "update"), BulkAction::Delete => write!(f, "delete"), } } }