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 body = serde_json::json!({
205 "jsonrpc": "2.0",
206 "method": "apiinfo.version",
207 "params": [],
208 "id": 1
209 });
210
211 let client = Client::builder()
212 .danger_accept_invalid_certs(self.accept_invalid_certs)
213 .build()?;
214
215 let v6_4_req = VersionReq::parse(">=6.4")?;
216
217 let version_str_raw =
218 ZabbixInstance::zabbix_raw_request(&client, &self.url, body, "", false)?;
219 let version_str = version_str_raw.trim_matches('"');
220
221 let current_v = Version::parse(version_str)?;
222
223 self.need_auth_in_body = !(v6_4_req.matches(¤t_v));
224
225 self.client = Some(client);
226
227 self.version = version_str.to_string();
228
229 Ok(self)
230 }
231
232 pub fn login(self, auth_type: AuthType) -> Result<ZabbixInstance, ZabbixError> {
264 match auth_type {
265 AuthType::Token(token) => self.login_with_token(token),
266 AuthType::UsernamePassword(username, password) => {
267 self.login_with_username_password(username, password)
268 }
269 }
270 }
271
272 fn login_with_token(self, token: String) -> Result<ZabbixInstance, ZabbixError> {
273 let body = serde_json::json!({
274 "jsonrpc": "2.0",
275 "method": "user.checkAuthentication",
276 "params": {
277 "token": token
278 },
279 "id": 1
280 });
281
282 let client = self.client.ok_or_else(|| {
283 ZabbixError::Other("Client not initialized. Did you call build()?".to_string())
284 })?;
285
286 match ZabbixInstance::zabbix_raw_request(
287 &client,
288 &self.url,
289 body,
290 "",
291 self.need_auth_in_body,
292 ) {
293 Ok(_) => {
294 return Ok(ZabbixInstance {
295 id: Uuid::new_v4().to_string(),
296 need_auth_in_body: self.need_auth_in_body,
297 token: token,
298 request_client: client,
299 url: self.url,
300 version: self.version,
301 need_logout: false,
302 });
303 }
304 Err(e) => {
305 return Err(ZabbixError::ApiError {
306 message: "Invalid token".to_string(),
307 data: e.to_string(),
308 });
309 }
310 }
311 }
312
313 fn login_with_username_password(
314 self,
315 username: String,
316 password: String,
317 ) -> Result<ZabbixInstance, ZabbixError> {
318 let v5_2 = Version::parse("5.2.0")?;
319 let current_v = Version::parse(&self.version)?;
320 let user_param = if current_v <= v5_2 {
321 "user"
322 } else {
323 "username"
324 };
325 let body = serde_json::json!({
326 "jsonrpc": "2.0",
327 "method": "user.login",
328 "params": {
329 user_param: username,
330 "password": password
331 },
332 "id": 1
333 });
334
335 let client = self.client.ok_or_else(|| {
336 ZabbixError::Other("Client not initialized. Did you call build()?".to_string())
337 })?;
338
339 let token = ZabbixInstance::zabbix_raw_request(
340 &client,
341 &self.url,
342 body,
343 "",
344 self.need_auth_in_body,
345 )?;
346
347 Ok(ZabbixInstance {
348 id: Uuid::new_v4().to_string(),
349 need_auth_in_body: self.need_auth_in_body,
350 token: token,
351 request_client: client,
352 url: self.url,
353 version: self.version,
354 need_logout: true,
355 })
356 }
357}
358
359impl ZabbixInstance {
360 pub fn id(&self) -> &str {
377 &self.id
378 }
379
380 pub fn url(&self) -> &str {
395 &self.url
396 }
397
398 pub fn logout(&mut self) -> Result<&mut Self, ZabbixError> {
415 if !self.need_logout {
416 return Ok(self);
417 }
418
419 let body = serde_json::json!({
420 "jsonrpc": "2.0",
421 "method": "user.logout",
422 "params": [],
423 "id": 1
424 });
425
426 match Self::zabbix_raw_request(
427 &self.request_client,
428 &self.url,
429 body,
430 self.token.as_ref(),
431 self.need_auth_in_body,
432 ) {
433 Ok(_) => {
434 self.token = "".to_string();
435 self.need_logout = false;
436 Ok(self)
437 }
438 Err(e) => Err(e),
439 }
440 }
441
442 pub fn get_version(&self) -> Result<String, ZabbixError> {
460 let body = serde_json::json!({
461 "jsonrpc": "2.0",
462 "method": "apiinfo.version",
463 "params": [],
464 "id": 1
465 });
466
467 let version_str =
468 Self::zabbix_raw_request(&self.request_client, &self.url, body, "", false)?;
469
470 Ok(version_str)
471 }
472
473 pub fn check_version(&self, version_req: &str) -> Result<bool, ZabbixError> {
491 let version_req = VersionReq::parse(version_req)?;
492 let current_v = Version::parse(&self.version)?;
493
494 Ok(version_req.matches(¤t_v))
495 }
496
497 pub fn zabbix_request<P: Into<ApiRequestParams>>(
516 &self,
517 method: &str,
518 params: P,
519 ) -> Result<String, ZabbixError> {
520 let params_val = match params.into() {
521 ApiRequestParams::Json(val) => val,
522 ApiRequestParams::String(s) => serde_json::from_str(&s).map_err(ZabbixError::from)?,
523 };
524
525 let body = serde_json::json!({
526 "jsonrpc": "2.0",
527 "method": method,
528 "params": params_val,
529 "id": Uuid::new_v4().to_string()
530 });
531
532 Self::zabbix_raw_request(
533 &self.request_client,
534 &self.url,
535 body,
536 &self.token,
537 self.need_auth_in_body,
538 )
539 }
540
541 fn zabbix_raw_request(
542 client: &Client,
543 url: &str,
544 mut payload: Value,
545 token: &str,
546 need_auth_in_body: bool,
547 ) -> Result<String, ZabbixError> {
548 let mut request_builder = client
549 .post(format!("{}/api_jsonrpc.php", url))
550 .header("Content-Type", "application/json-rpc");
551
552 if token != "" {
553 if !need_auth_in_body {
554 request_builder =
555 request_builder.header("Authorization", format!("Bearer {}", token));
556 } else {
557 if let Some(obj) = payload.as_object_mut() {
558 obj.insert("auth".to_string(), Value::String(String::from(token)));
559 }
560 }
561 }
562
563 let response = request_builder.json(&payload).send()?;
564
565 if !response.status().is_success() {
566 return Err(ZabbixError::Other(format!(
567 "HTTP Error: {}",
568 response.status()
569 )));
570 }
571
572 let text = response.text()?;
573
574 let json: Value = serde_json::from_str(&text)?;
575
576 if let Some(error) = json.get("error") {
577 if error.is_object() {
578 let msg = error
579 .get("message")
580 .and_then(|v| v.as_str())
581 .unwrap_or("Unknown error");
582 let data = error.get("data").and_then(|v| v.as_str()).unwrap_or("");
583 return Err(ZabbixError::ApiError {
584 message: msg.to_string(),
585 data: data.to_string(),
586 });
587 }
588 return Err(ZabbixError::Other(error.to_string()));
589 }
590
591 if let Some(result) = json.get("result") {
592 if let Some(s) = result.as_str() {
593 return Ok(s.to_string());
594 }
595 return Ok(result.to_string());
596 }
597
598 Err(ZabbixError::Other("Unknown response format".to_string()))
599 }
600}
601
602impl Drop for ZabbixInstance {
603 fn drop(&mut self) {
604 if self.need_logout {
605 self.logout().ok();
606 }
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use mockito::Server;
614
615 #[test]
616 fn test_login_with_token_success() {
617 let mut server = Server::new();
618 let url = server.url();
619
620 let mock_version = server
621 .mock("POST", "/api_jsonrpc.php")
622 .with_status(200)
623 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
624 .create();
625
626 let mock_auth = server
627 .mock("POST", "/api_jsonrpc.php")
628 .with_status(200)
629 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
630 .create();
631
632 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
633 let result = builder.login(AuthType::Token("test_token".to_string()));
634
635 assert!(result.is_ok());
636 mock_version.assert();
637 mock_auth.assert();
638 }
639
640 #[test]
641 fn test_login_with_password_success() {
642 let mut server = Server::new();
643 let url = server.url();
644
645 let mock_version = server
646 .mock("POST", "/api_jsonrpc.php")
647 .with_status(200)
648 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
649 .create();
650
651 let mock_auth = server
652 .mock("POST", "/api_jsonrpc.php")
653 .with_status(200)
654 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
655 .create();
656
657 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
658 let result = builder.login(AuthType::UsernamePassword(
659 "Admin".to_string(),
660 "zabbix".to_string(),
661 ));
662
663 assert!(result.is_ok());
664 mock_version.assert();
665 mock_auth.assert();
666 }
667
668 #[test]
669 fn test_login_with_password_failure() {
670 let mut server = Server::new();
671 let url = server.url();
672
673 let mock_version = server
674 .mock("POST", "/api_jsonrpc.php")
675 .with_status(200)
676 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
677 .create();
678
679 let mock_auth = server
680 .mock("POST", "/api_jsonrpc.php")
681 .with_status(401)
682 .with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"Invalid username or password"},"id":1}"#)
683 .create();
684
685 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
686 let result = builder.login(AuthType::UsernamePassword(
687 "Admin".to_string(),
688 "zabbix".to_string(),
689 ));
690
691 assert!(result.is_err());
692 mock_version.assert();
693 mock_auth.assert();
694 }
695
696 #[test]
697 fn test_login_with_token_failure() {
698 let mut server = Server::new();
699 let url = server.url();
700
701 let mock_version = server
702 .mock("POST", "/api_jsonrpc.php")
703 .with_status(200)
704 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
705 .create();
706
707 let mock_auth = server
708 .mock("POST", "/api_jsonrpc.php")
709 .with_status(200)
710 .with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"Token is invalid"},"id":1}"#)
711 .create();
712
713 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
714 let result = builder.login(AuthType::Token("test_token".to_string()));
715
716 assert!(result.is_err());
717 mock_version.assert();
718 mock_auth.assert();
719 }
720
721 #[test]
722 fn test_request_json_success() {
723 let mut server = Server::new();
724 let url = server.url();
725
726 let mock_version = server
727 .mock("POST", "/api_jsonrpc.php")
728 .with_status(200)
729 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
730 .create();
731
732 let mock_auth = server
733 .mock("POST", "/api_jsonrpc.php")
734 .with_status(200)
735 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
736 .create();
737
738 let mock_request = server
739 .mock("POST", "/api_jsonrpc.php")
740 .with_status(200)
741 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_result","id":1}"#)
742 .create();
743
744 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
745 let result = builder.login(AuthType::Token("test_token".to_string()));
746 let host_get = result.unwrap().zabbix_request(
747 "host.get",
748 serde_json::json!({"output": ["host", "name"], "limit": 1}),
749 );
750
751 assert!(host_get.is_ok());
752 mock_version.assert();
753 mock_auth.assert();
754 mock_request.assert();
755 }
756
757 #[test]
758 fn test_request_json_failure() {
759 let mut server = Server::new();
760 let url = server.url();
761
762 let mock_version = server
763 .mock("POST", "/api_jsonrpc.php")
764 .with_status(200)
765 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
766 .create();
767
768 let mock_auth = server
769 .mock("POST", "/api_jsonrpc.php")
770 .with_status(200)
771 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
772 .create();
773
774 let mock_request = server
775 .mock("POST", "/api_jsonrpc.php")
776 .with_status(200)
777 .with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"BlahBlahBlah"},"id":1}"#)
778 .create();
779
780 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
781 let result = builder.login(AuthType::Token("test_token".to_string()));
782 let host_get = result.unwrap().zabbix_request(
783 "host.get",
784 serde_json::json!({"output": ["host", "name"], "limit": 1}),
785 );
786
787 assert!(host_get.is_err());
788 mock_version.assert();
789 mock_auth.assert();
790 mock_request.assert();
791 }
792
793 #[test]
794 fn test_request_string_success() {
795 let mut server = Server::new();
796 let url = server.url();
797
798 let mock_version = server
799 .mock("POST", "/api_jsonrpc.php")
800 .with_status(200)
801 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
802 .create();
803
804 let mock_auth = server
805 .mock("POST", "/api_jsonrpc.php")
806 .with_status(200)
807 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
808 .create();
809
810 let mock_request = server
811 .mock("POST", "/api_jsonrpc.php")
812 .with_status(200)
813 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_result","id":1}"#)
814 .create();
815
816 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
817 let result = builder.login(AuthType::Token("test_token".to_string()));
818 let host_get = result
819 .unwrap()
820 .zabbix_request("host.get", r#"{"output": ["host", "name"], "limit": 1}"#);
821
822 assert!(host_get.is_ok());
823 mock_version.assert();
824 mock_auth.assert();
825 mock_request.assert();
826 }
827
828 #[test]
829 fn test_request_string_failure() {
830 let mut server = Server::new();
831 let url = server.url();
832
833 let mock_version = server
834 .mock("POST", "/api_jsonrpc.php")
835 .with_status(200)
836 .with_body(r#"{"jsonrpc":"2.0","result":"7.0.0","id":1}"#)
837 .create();
838
839 let mock_auth = server
840 .mock("POST", "/api_jsonrpc.php")
841 .with_status(200)
842 .with_body(r#"{"jsonrpc":"2.0","result":"dummy_token","id":1}"#)
843 .create();
844
845 let mock_request = server
846 .mock("POST", "/api_jsonrpc.php")
847 .with_status(200)
848 .with_body(r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params","data":"BlahBlahBlah"},"id":1}"#)
849 .create();
850
851 let builder = ZabbixInstanceBuilder::new(&url).build().unwrap();
852 let result = builder.login(AuthType::Token("test_token".to_string()));
853 let host_get = result
854 .unwrap()
855 .zabbix_request("host.get", r#"{"output": ["host", "name"], "limit": 1}"#);
856
857 assert!(host_get.is_err());
858 mock_version.assert();
859 mock_auth.assert();
860 mock_request.assert();
861 }
862}