1use reqwest::blocking::Client;
21use semver::{Version, VersionReq};
22use serde_json::Value;
23use thiserror::Error;
24use uuid::Uuid;
25
26pub enum AuthType {
42 Token(String),
45 UsernamePassword(String, String),
48}
49
50pub enum ApiRequestParams {
61 Json(Value),
63 String(String),
65}
66
67impl From<Value> for ApiRequestParams {
68 fn from(v: Value) -> Self {
69 ApiRequestParams::Json(v)
70 }
71}
72
73impl From<&str> for ApiRequestParams {
74 fn from(s: &str) -> Self {
75 ApiRequestParams::String(s.to_string())
76 }
77}
78
79impl From<String> for ApiRequestParams {
80 fn from(s: String) -> Self {
81 ApiRequestParams::String(s)
82 }
83}
84
85#[derive(Error, Debug)]
95pub enum ZabbixError {
96 #[error("Network error: {0}")]
97 Network(#[from] reqwest::Error),
98 #[error("JSON error: {0}")]
99 Json(#[from] serde_json::Error),
100 #[error("Version parse error: {0}")]
101 VersionParse(#[from] semver::Error),
102 #[error("Zabbix API Error: {message} {data}")]
103 ApiError { message: String, data: String },
104 #[error("Unknown error: {0}")]
105 Other(String),
106}
107
108pub struct ZabbixInstance {
110 id: String,
111 url: String,
112 token: String,
113 request_client: Client,
114 need_auth_in_body: bool,
115 version: String,
116 need_logout: bool,
117}
118
119impl ZabbixInstance {
120 pub fn builder(url: &str) -> ZabbixInstanceBuilder {
132 ZabbixInstanceBuilder::new(url)
133 }
134}
135
136pub struct ZabbixInstanceBuilder {
138 url: String,
139 accept_invalid_certs: bool,
140 client: Option<Client>,
141 need_auth_in_body: bool,
142 version: String,
143}
144
145impl ZabbixInstanceBuilder {
146 pub fn new(url: &str) -> Self {
158 Self {
159 url: url.to_string(),
160 accept_invalid_certs: false,
161 client: None,
162 need_auth_in_body: false,
163 version: "".to_string(),
164 }
165 }
166
167 pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
181 self.accept_invalid_certs = accept;
182 self
183 }
184
185 pub fn build(mut self) -> Result<Self, ZabbixError> {
204 let client = Client::builder()
205 .danger_accept_invalid_certs(self.accept_invalid_certs)
206 .build()?;
207
208 let v6_4_req = VersionReq::parse(">=6.4")?;
209
210 let version_str_raw = ZabbixInstance::zabbix_raw_request(
211 &client,
212 &self.url,
213 "apiinfo.version",
214 serde_json::json!([]),
215 "",
216 false,
217 )?;
218 let version_str = version_str_raw.trim_matches('"');
219
220 let current_v = Version::parse(version_str)?;
221
222 self.need_auth_in_body = !(v6_4_req.matches(¤t_v));
223
224 self.client = Some(client);
225
226 self.version = version_str.to_string();
227
228 Ok(self)
229 }
230
231 pub fn login(self, auth_type: AuthType) -> Result<ZabbixInstance, ZabbixError> {
263 match auth_type {
264 AuthType::Token(token) => self.login_with_token(token),
265 AuthType::UsernamePassword(username, password) => {
266 self.login_with_username_password(username, password)
267 }
268 }
269 }
270
271 fn login_with_token(self, token: String) -> Result<ZabbixInstance, ZabbixError> {
272 let client = self.client.ok_or_else(|| {
273 ZabbixError::Other("Client not initialized. Did you call build()?".to_string())
274 })?;
275
276 match ZabbixInstance::zabbix_raw_request(
277 &client,
278 &self.url,
279 "user.checkAuthentication",
280 serde_json::json!({"token": token}),
281 "",
282 self.need_auth_in_body,
283 ) {
284 Ok(_) => {
285 return Ok(ZabbixInstance {
286 id: Uuid::new_v4().to_string(),
287 need_auth_in_body: self.need_auth_in_body,
288 token: token,
289 request_client: client,
290 url: self.url,
291 version: self.version,
292 need_logout: false,
293 });
294 }
295 Err(e) => {
296 return Err(ZabbixError::ApiError {
297 message: "Invalid token".to_string(),
298 data: e.to_string(),
299 });
300 }
301 }
302 }
303
304 fn login_with_username_password(
305 self,
306 username: String,
307 password: String,
308 ) -> Result<ZabbixInstance, ZabbixError> {
309 let v5_2 = Version::parse("5.2.0")?;
310 let current_v = Version::parse(&self.version)?;
311 let user_param = if current_v <= v5_2 {
312 "user"
313 } else {
314 "username"
315 };
316
317 let client = self.client.ok_or_else(|| {
318 ZabbixError::Other("Client not initialized. Did you call build()?".to_string())
319 })?;
320
321 let token = ZabbixInstance::zabbix_raw_request(
322 &client,
323 &self.url,
324 "user.login",
325 serde_json::json!({user_param: username, "password": password}),
326 "",
327 self.need_auth_in_body,
328 )?;
329
330 Ok(ZabbixInstance {
331 id: Uuid::new_v4().to_string(),
332 need_auth_in_body: self.need_auth_in_body,
333 token: token,
334 request_client: client,
335 url: self.url,
336 version: self.version,
337 need_logout: true,
338 })
339 }
340}
341
342impl ZabbixInstance {
343 pub fn id(&self) -> &str {
360 &self.id
361 }
362
363 pub fn url(&self) -> &str {
378 &self.url
379 }
380
381 pub fn logout(&mut self) -> Result<&mut Self, ZabbixError> {
398 if !self.need_logout {
399 return Ok(self);
400 }
401
402 match Self::zabbix_raw_request(
403 &self.request_client,
404 &self.url,
405 "user.logout",
406 serde_json::json!([]),
407 self.token.as_ref(),
408 self.need_auth_in_body,
409 ) {
410 Ok(_) => {
411 self.token = "".to_string();
412 self.need_logout = false;
413 Ok(self)
414 }
415 Err(e) => Err(e),
416 }
417 }
418
419 pub fn get_version(&self) -> Result<String, ZabbixError> {
437 let version_str = Self::zabbix_raw_request(
438 &self.request_client,
439 &self.url,
440 "apiinfo.version",
441 serde_json::json!([]),
442 "",
443 false,
444 )?;
445
446 Ok(version_str)
447 }
448
449 pub fn check_version(&self, version_req: &str) -> Result<bool, ZabbixError> {
467 let version_req = VersionReq::parse(version_req)?;
468 let current_v = Version::parse(&self.version)?;
469
470 Ok(version_req.matches(¤t_v))
471 }
472
473 pub fn zabbix_request<P: Into<ApiRequestParams>>(
492 &self,
493 method: &str,
494 params: P,
495 ) -> Result<String, ZabbixError> {
496 let params_val = match params.into() {
497 ApiRequestParams::Json(val) => val,
498 ApiRequestParams::String(s) => serde_json::from_str(&s).map_err(ZabbixError::from)?,
499 };
500
501 Self::zabbix_raw_request(
502 &self.request_client,
503 &self.url,
504 method,
505 params_val,
506 &self.token,
507 self.need_auth_in_body,
508 )
509 }
510
511 fn zabbix_raw_request(
512 client: &Client,
513 url: &str,
514 method: &str,
515 params: Value,
516 token: &str,
517 need_auth_in_body: bool,
518 ) -> Result<String, ZabbixError> {
519 let mut request_builder = client
520 .post(format!("{}/api_jsonrpc.php", url))
521 .header("Content-Type", "application/json-rpc");
522
523 let mut payload = serde_json::json!({
524 "jsonrpc": "2.0",
525 "method": method,
526 "params": params,
527 "id": Uuid::new_v4().to_string()
528 });
529
530 if token != "" {
531 if !need_auth_in_body {
532 request_builder =
533 request_builder.header("Authorization", format!("Bearer {}", token));
534 } else {
535 if let Some(obj) = payload.as_object_mut() {
536 obj.insert("auth".to_string(), Value::String(String::from(token)));
537 }
538 }
539 }
540
541 let response = request_builder.json(&payload).send()?;
542
543 if !response.status().is_success() {
544 return Err(ZabbixError::Other(format!(
545 "HTTP Error: {}",
546 response.status()
547 )));
548 }
549
550 let text = response.text()?;
551
552 let json: Value = serde_json::from_str(&text)?;
553
554 if let Some(error) = json.get("error") {
555 if error.is_object() {
556 let msg = error
557 .get("message")
558 .and_then(|v| v.as_str())
559 .unwrap_or("Unknown error");
560 let data = error.get("data").and_then(|v| v.as_str()).unwrap_or("");
561 return Err(ZabbixError::ApiError {
562 message: msg.to_string(),
563 data: data.to_string(),
564 });
565 }
566 return Err(ZabbixError::Other(error.to_string()));
567 }
568
569 if let Some(result) = json.get("result") {
570 if let Some(s) = result.as_str() {
571 return Ok(s.to_string());
572 }
573 return Ok(result.to_string());
574 }
575
576 Err(ZabbixError::Other("Unknown response format".to_string()))
577 }
578}
579
580impl Drop for ZabbixInstance {
581 fn drop(&mut self) {
582 if self.need_logout {
583 self.logout().ok();
584 }
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use mockito::Server;
592
593 #[test]
594 fn test_login_with_token_success() {
595 let mut server = Server::new();
596 let url = server.url();
597
598 let mock_version = server
599 .mock("POST", "/api_jsonrpc.php")
600 .with_status(200)
601 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
602 .create();
603
604 let mock_auth = server
605 .mock("POST", "/api_jsonrpc.php")
606 .with_status(200)
607 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
608 .create();
609
610 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
611 let result = builder.login(AuthType::Token("test_token".to_string()));
612
613 assert!(result.is_ok());
614 mock_version.assert();
615 mock_auth.assert();
616 }
617
618 #[test]
619 fn test_login_with_password_success() {
620 let mut server = Server::new();
621 let url = server.url();
622
623 let mock_version = server
624 .mock("POST", "/api_jsonrpc.php")
625 .with_status(200)
626 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
627 .create();
628
629 let mock_auth = server
630 .mock("POST", "/api_jsonrpc.php")
631 .with_status(200)
632 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
633 .create();
634
635 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
636 let result = builder.login(AuthType::UsernamePassword(
637 "Admin".to_string(),
638 "zabbix".to_string(),
639 ));
640
641 assert!(result.is_ok());
642 mock_version.assert();
643 mock_auth.assert();
644 }
645
646 #[test]
647 fn test_login_with_password_failure() {
648 let mut server = Server::new();
649 let url = server.url();
650
651 let mock_version = server
652 .mock("POST", "/api_jsonrpc.php")
653 .with_status(200)
654 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
655 .create();
656
657 let mock_auth = server
658 .mock("POST", "/api_jsonrpc.php")
659 .with_status(401)
660 .with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"Invalid username or password"},"id":1}"#)
661 .create();
662
663 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
664 let result = builder.login(AuthType::UsernamePassword(
665 "Admin".to_string(),
666 "zabbix".to_string(),
667 ));
668
669 assert!(result.is_err());
670 mock_version.assert();
671 mock_auth.assert();
672 }
673
674 #[test]
675 fn test_login_with_token_failure() {
676 let mut server = Server::new();
677 let url = server.url();
678
679 let mock_version = server
680 .mock("POST", "/api_jsonrpc.php")
681 .with_status(200)
682 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
683 .create();
684
685 let mock_auth = server
686 .mock("POST", "/api_jsonrpc.php")
687 .with_status(200)
688 .with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"Token is invalid"},"id":1}"#)
689 .create();
690
691 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
692 let result = builder.login(AuthType::Token("test_token".to_string()));
693
694 assert!(result.is_err());
695 mock_version.assert();
696 mock_auth.assert();
697 }
698
699 #[test]
700 fn test_request_json_success() {
701 let mut server = Server::new();
702 let url = server.url();
703
704 let mock_version = server
705 .mock("POST", "/api_jsonrpc.php")
706 .with_status(200)
707 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
708 .create();
709
710 let mock_auth = server
711 .mock("POST", "/api_jsonrpc.php")
712 .with_status(200)
713 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
714 .create();
715
716 let mock_request = server
717 .mock("POST", "/api_jsonrpc.php")
718 .with_status(200)
719 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_result","id":1}"#)
720 .create();
721
722 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
723 let result = builder.login(AuthType::Token("test_token".to_string()));
724 let host_get = result.unwrap().zabbix_request(
725 "host.get",
726 serde_json::json!({"output": ["host", "name"], "limit": 1}),
727 );
728
729 assert!(host_get.is_ok());
730 mock_version.assert();
731 mock_auth.assert();
732 mock_request.assert();
733 }
734
735 #[test]
736 fn test_request_json_failure() {
737 let mut server = Server::new();
738 let url = server.url();
739
740 let mock_version = server
741 .mock("POST", "/api_jsonrpc.php")
742 .with_status(200)
743 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
744 .create();
745
746 let mock_auth = server
747 .mock("POST", "/api_jsonrpc.php")
748 .with_status(200)
749 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
750 .create();
751
752 let mock_request = server
753 .mock("POST", "/api_jsonrpc.php")
754 .with_status(200)
755 .with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"BlahBlahBlah"},"id":1}"#)
756 .create();
757
758 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
759 let result = builder.login(AuthType::Token("test_token".to_string()));
760 let host_get = result.unwrap().zabbix_request(
761 "host.get",
762 serde_json::json!({"output": ["host", "name"], "limit": 1}),
763 );
764
765 assert!(host_get.is_err());
766 mock_version.assert();
767 mock_auth.assert();
768 mock_request.assert();
769 }
770
771 #[test]
772 fn test_request_string_success() {
773 let mut server = Server::new();
774 let url = server.url();
775
776 let mock_version = server
777 .mock("POST", "/api_jsonrpc.php")
778 .with_status(200)
779 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
780 .create();
781
782 let mock_auth = server
783 .mock("POST", "/api_jsonrpc.php")
784 .with_status(200)
785 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
786 .create();
787
788 let mock_request = server
789 .mock("POST", "/api_jsonrpc.php")
790 .with_status(200)
791 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_result","id":1}"#)
792 .create();
793
794 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
795 let result = builder.login(AuthType::Token("test_token".to_string()));
796 let host_get = result
797 .unwrap()
798 .zabbix_request("host.get", r#"{"output": ["host", "name"], "limit": 1}"#);
799
800 assert!(host_get.is_ok());
801 mock_version.assert();
802 mock_auth.assert();
803 mock_request.assert();
804 }
805
806 #[test]
807 fn test_request_string_failure() {
808 let mut server = Server::new();
809 let url = server.url();
810
811 let mock_version = server
812 .mock("POST", "/api_jsonrpc.php")
813 .with_status(200)
814 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
815 .create();
816
817 let mock_auth = server
818 .mock("POST", "/api_jsonrpc.php")
819 .with_status(200)
820 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
821 .create();
822
823 let mock_request = server
824 .mock("POST", "/api_jsonrpc.php")
825 .with_status(200)
826 .with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"BlahBlahBlah"},"id":1}"#)
827 .create();
828
829 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
830 let result = builder.login(AuthType::Token("test_token".to_string()));
831 let host_get = result
832 .unwrap()
833 .zabbix_request("host.get", r#"{"output": ["host", "name"], "limit": 1}"#);
834
835 assert!(host_get.is_err());
836 mock_version.assert();
837 mock_auth.assert();
838 mock_request.assert();
839 }
840}