use reqwest::blocking::Client;
use semver::{Version, VersionReq};
use serde_json::Value;
use thiserror::Error;
use uuid::Uuid;
pub enum AuthType {
Token(String),
UsernamePassword(String, String),
}
pub enum ApiRequestParams {
Json(Value),
String(String),
}
impl From<Value> for ApiRequestParams {
fn from(v: Value) -> Self {
ApiRequestParams::Json(v)
}
}
impl From<&str> for ApiRequestParams {
fn from(s: &str) -> Self {
ApiRequestParams::String(s.to_string())
}
}
impl From<String> for ApiRequestParams {
fn from(s: String) -> Self {
ApiRequestParams::String(s)
}
}
#[derive(Error, Debug)]
pub enum ZabbixError {
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Version parse error: {0}")]
VersionParse(#[from] semver::Error),
#[error("Zabbix API Error: {message} {data}")]
ApiError { message: String, data: String },
#[error("Unknown error: {0}")]
Other(String),
}
pub struct ZabbixInstance {
id: String,
url: String,
token: String,
request_client: Client,
need_auth_in_body: bool,
version: String,
need_logout: bool,
}
impl ZabbixInstance {
pub fn builder(url: &str) -> ZabbixInstanceBuilder {
ZabbixInstanceBuilder::new(url)
}
}
pub struct ZabbixInstanceBuilder {
url: String,
accept_invalid_certs: bool,
client: Option<Client>,
need_auth_in_body: bool,
version: String,
}
impl ZabbixInstanceBuilder {
pub fn new(url: &str) -> Self {
Self {
url: url.to_string(),
accept_invalid_certs: false,
client: None,
need_auth_in_body: false,
version: "".to_string(),
}
}
pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
self.accept_invalid_certs = accept;
self
}
pub fn build(mut self) -> Result<Self, ZabbixError> {
let client = Client::builder()
.danger_accept_invalid_certs(self.accept_invalid_certs)
.build()?;
let v6_4_req = VersionReq::parse(">=6.4")?;
let version_str_raw = ZabbixInstance::zabbix_raw_request(
&client,
&self.url,
"apiinfo.version",
serde_json::json!([]),
"",
false,
)?;
let version_str = version_str_raw.trim_matches('"');
let current_v = Version::parse(version_str)?;
self.need_auth_in_body = !(v6_4_req.matches(¤t_v));
self.client = Some(client);
self.version = version_str.to_string();
Ok(self)
}
pub fn login(self, auth_type: AuthType) -> Result<ZabbixInstance, ZabbixError> {
match auth_type {
AuthType::Token(token) => self.login_with_token(token),
AuthType::UsernamePassword(username, password) => {
self.login_with_username_password(username, password)
}
}
}
fn login_with_token(self, token: String) -> Result<ZabbixInstance, ZabbixError> {
let client = self.client.ok_or_else(|| {
ZabbixError::Other("Client not initialized. Did you call build()?".to_string())
})?;
match ZabbixInstance::zabbix_raw_request(
&client,
&self.url,
"user.checkAuthentication",
serde_json::json!({"token": token}),
"",
self.need_auth_in_body,
) {
Ok(_) => {
return Ok(ZabbixInstance {
id: Uuid::new_v4().to_string(),
need_auth_in_body: self.need_auth_in_body,
token: token,
request_client: client,
url: self.url,
version: self.version,
need_logout: false,
});
}
Err(e) => {
return Err(ZabbixError::ApiError {
message: "Invalid token".to_string(),
data: e.to_string(),
});
}
}
}
fn login_with_username_password(
self,
username: String,
password: String,
) -> Result<ZabbixInstance, ZabbixError> {
let v5_2 = Version::parse("5.2.0")?;
let current_v = Version::parse(&self.version)?;
let user_param = if current_v <= v5_2 {
"user"
} else {
"username"
};
let client = self.client.ok_or_else(|| {
ZabbixError::Other("Client not initialized. Did you call build()?".to_string())
})?;
let token = ZabbixInstance::zabbix_raw_request(
&client,
&self.url,
"user.login",
serde_json::json!({user_param: username, "password": password}),
"",
self.need_auth_in_body,
)?;
Ok(ZabbixInstance {
id: Uuid::new_v4().to_string(),
need_auth_in_body: self.need_auth_in_body,
token: token,
request_client: client,
url: self.url,
version: self.version,
need_logout: true,
})
}
}
impl ZabbixInstance {
pub fn id(&self) -> &str {
&self.id
}
pub fn url(&self) -> &str {
&self.url
}
pub fn logout(&mut self) -> Result<&mut Self, ZabbixError> {
if !self.need_logout {
return Ok(self);
}
match Self::zabbix_raw_request(
&self.request_client,
&self.url,
"user.logout",
serde_json::json!([]),
self.token.as_ref(),
self.need_auth_in_body,
) {
Ok(_) => {
self.token = "".to_string();
self.need_logout = false;
Ok(self)
}
Err(e) => Err(e),
}
}
pub fn get_version(&self) -> Result<String, ZabbixError> {
let version_str = Self::zabbix_raw_request(
&self.request_client,
&self.url,
"apiinfo.version",
serde_json::json!([]),
"",
false,
)?;
Ok(version_str)
}
pub fn check_version(&self, version_req: &str) -> Result<bool, ZabbixError> {
let version_req = VersionReq::parse(version_req)?;
let current_v = Version::parse(&self.version)?;
Ok(version_req.matches(¤t_v))
}
pub fn zabbix_request<P: Into<ApiRequestParams>>(
&self,
method: &str,
params: P,
) -> Result<String, ZabbixError> {
let params_val = match params.into() {
ApiRequestParams::Json(val) => val,
ApiRequestParams::String(s) => serde_json::from_str(&s).map_err(ZabbixError::from)?,
};
Self::zabbix_raw_request(
&self.request_client,
&self.url,
method,
params_val,
&self.token,
self.need_auth_in_body,
)
}
fn zabbix_raw_request(
client: &Client,
url: &str,
method: &str,
params: Value,
token: &str,
need_auth_in_body: bool,
) -> Result<String, ZabbixError> {
let mut request_builder = client
.post(format!("{}/api_jsonrpc.php", url))
.header("Content-Type", "application/json-rpc");
let mut payload = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": Uuid::new_v4().to_string()
});
if token != "" {
if !need_auth_in_body {
request_builder =
request_builder.header("Authorization", format!("Bearer {}", token));
} else {
if let Some(obj) = payload.as_object_mut() {
obj.insert("auth".to_string(), Value::String(String::from(token)));
}
}
}
let response = request_builder.json(&payload).send()?;
if !response.status().is_success() {
return Err(ZabbixError::Other(format!(
"HTTP Error: {}",
response.status()
)));
}
let text = response.text()?;
let json: Value = serde_json::from_str(&text)?;
if let Some(error) = json.get("error") {
if error.is_object() {
let msg = error
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
let data = error.get("data").and_then(|v| v.as_str()).unwrap_or("");
return Err(ZabbixError::ApiError {
message: msg.to_string(),
data: data.to_string(),
});
}
return Err(ZabbixError::Other(error.to_string()));
}
if let Some(result) = json.get("result") {
if let Some(s) = result.as_str() {
return Ok(s.to_string());
}
return Ok(result.to_string());
}
Err(ZabbixError::Other("Unknown response format".to_string()))
}
}
impl Drop for ZabbixInstance {
fn drop(&mut self) {
if self.need_logout {
self.logout().ok();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockito::Server;
#[test]
fn test_login_with_token_success() {
let mut server = Server::new();
let url = server.url();
let mock_version = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
.create();
let mock_auth = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
.create();
let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
let result = builder.login(AuthType::Token("test_token".to_string()));
assert!(result.is_ok());
mock_version.assert();
mock_auth.assert();
}
#[test]
fn test_login_with_password_success() {
let mut server = Server::new();
let url = server.url();
let mock_version = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
.create();
let mock_auth = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
.create();
let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
let result = builder.login(AuthType::UsernamePassword(
"Admin".to_string(),
"zabbix".to_string(),
));
assert!(result.is_ok());
mock_version.assert();
mock_auth.assert();
}
#[test]
fn test_login_with_password_failure() {
let mut server = Server::new();
let url = server.url();
let mock_version = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
.create();
let mock_auth = server
.mock("POST", "/api_jsonrpc.php")
.with_status(401)
.with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"Invalid username or password"},"id":1}"#)
.create();
let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
let result = builder.login(AuthType::UsernamePassword(
"Admin".to_string(),
"zabbix".to_string(),
));
assert!(result.is_err());
mock_version.assert();
mock_auth.assert();
}
#[test]
fn test_login_with_token_failure() {
let mut server = Server::new();
let url = server.url();
let mock_version = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
.create();
let mock_auth = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"Token is invalid"},"id":1}"#)
.create();
let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
let result = builder.login(AuthType::Token("test_token".to_string()));
assert!(result.is_err());
mock_version.assert();
mock_auth.assert();
}
#[test]
fn test_request_json_success() {
let mut server = Server::new();
let url = server.url();
let mock_version = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
.create();
let mock_auth = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
.create();
let mock_request = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"dummy_result","id":1}"#)
.create();
let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
let result = builder.login(AuthType::Token("test_token".to_string()));
let host_get = result.unwrap().zabbix_request(
"host.get",
serde_json::json!({"output": ["host", "name"], "limit": 1}),
);
assert!(host_get.is_ok());
mock_version.assert();
mock_auth.assert();
mock_request.assert();
}
#[test]
fn test_request_json_failure() {
let mut server = Server::new();
let url = server.url();
let mock_version = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
.create();
let mock_auth = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
.create();
let mock_request = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"BlahBlahBlah"},"id":1}"#)
.create();
let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
let result = builder.login(AuthType::Token("test_token".to_string()));
let host_get = result.unwrap().zabbix_request(
"host.get",
serde_json::json!({"output": ["host", "name"], "limit": 1}),
);
assert!(host_get.is_err());
mock_version.assert();
mock_auth.assert();
mock_request.assert();
}
#[test]
fn test_request_string_success() {
let mut server = Server::new();
let url = server.url();
let mock_version = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
.create();
let mock_auth = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
.create();
let mock_request = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"dummy_result","id":1}"#)
.create();
let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
let result = builder.login(AuthType::Token("test_token".to_string()));
let host_get = result
.unwrap()
.zabbix_request("host.get", r#"{"output": ["host", "name"], "limit": 1}"#);
assert!(host_get.is_ok());
mock_version.assert();
mock_auth.assert();
mock_request.assert();
}
#[test]
fn test_request_string_failure() {
let mut server = Server::new();
let url = server.url();
let mock_version = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
.create();
let mock_auth = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
.create();
let mock_request = server
.mock("POST", "/api_jsonrpc.php")
.with_status(200)
.with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"BlahBlahBlah"},"id":1}"#)
.create();
let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
let result = builder.login(AuthType::Token("test_token".to_string()));
let host_get = result
.unwrap()
.zabbix_request("host.get", r#"{"output": ["host", "name"], "limit": 1}"#);
assert!(host_get.is_err());
mock_version.assert();
mock_auth.assert();
mock_request.assert();
}
}