misp_client/
client.rs

1//! Low-level HTTP client for MISP API requests.
2
3use reqwest::{header, Client, Method};
4use serde_json::Value;
5use std::time::Duration;
6use tracing::{debug, error};
7
8use crate::error::MispError;
9
10#[derive(Clone)]
11pub struct MispClient {
12    base_url: String,
13    api_key: String,
14    http: Client,
15}
16
17impl MispClient {
18    pub fn new(base_url: impl Into<String>, api_key: impl Into<String>, verify_ssl: bool) -> Self {
19        let base_url = base_url.into().trim_end_matches('/').to_string();
20
21        let http = Client::builder()
22            .danger_accept_invalid_certs(!verify_ssl)
23            .timeout(Duration::from_secs(30))
24            .build()
25            .expect("failed to create HTTP client");
26
27        Self {
28            base_url,
29            api_key: api_key.into(),
30            http,
31        }
32    }
33
34    pub fn with_timeout(
35        base_url: impl Into<String>,
36        api_key: impl Into<String>,
37        verify_ssl: bool,
38        timeout: Duration,
39    ) -> Self {
40        let base_url = base_url.into().trim_end_matches('/').to_string();
41
42        let http = Client::builder()
43            .danger_accept_invalid_certs(!verify_ssl)
44            .timeout(timeout)
45            .build()
46            .expect("failed to create HTTP client");
47
48        Self {
49            base_url,
50            api_key: api_key.into(),
51            http,
52        }
53    }
54
55    pub async fn get(&self, endpoint: &str) -> Result<Value, MispError> {
56        self.request(Method::GET, endpoint, None).await
57    }
58
59    pub async fn post(&self, endpoint: &str, body: Option<Value>) -> Result<Value, MispError> {
60        self.request(Method::POST, endpoint, body).await
61    }
62
63    pub async fn delete(&self, endpoint: &str) -> Result<Value, MispError> {
64        self.request(Method::DELETE, endpoint, None).await
65    }
66
67    async fn request(
68        &self,
69        method: Method,
70        endpoint: &str,
71        body: Option<Value>,
72    ) -> Result<Value, MispError> {
73        let url = format!("{}{}", self.base_url, endpoint);
74        debug!(%method, %url, "MISP API request");
75
76        let mut req = self
77            .http
78            .request(method, &url)
79            .header(header::AUTHORIZATION, &self.api_key)
80            .header(header::ACCEPT, "application/json")
81            .header(header::CONTENT_TYPE, "application/json");
82
83        if let Some(json) = body {
84            req = req.json(&json);
85        }
86
87        let resp = req.send().await?;
88        let status = resp.status();
89
90        if status == 401 || status == 403 {
91            return Err(MispError::Authentication(format!(
92                "API key rejected ({})",
93                status
94            )));
95        }
96
97        let text = resp.text().await?;
98        debug!(response_len = text.len(), "MISP API response received");
99
100        if !status.is_success() {
101            error!(%url, %status, "MISP API error");
102            return Err(MispError::Api {
103                status: status.as_u16(),
104                message: text,
105            });
106        }
107
108        serde_json::from_str(&text).map_err(|e| {
109            error!(%url, "Failed to parse MISP response");
110            MispError::Parse(e)
111        })
112    }
113}
114
115impl std::fmt::Debug for MispClient {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("MispClient")
118            .field("base_url", &self.base_url)
119            .field("api_key", &"[redacted]")
120            .finish()
121    }
122}