Skip to main content

scouter_http/
lib.rs

1pub mod error;
2use crate::error::ClientError;
3use reqwest::blocking::{Client, Response};
4use reqwest::header::AUTHORIZATION;
5use reqwest::header::{HeaderMap, HeaderValue};
6use scouter_settings::http::HttpConfig;
7use scouter_types::http::{JwtToken, RequestType, Routes};
8use serde_json::Value;
9use std::sync::Arc;
10use std::sync::RwLock;
11use tracing::{debug, error, instrument};
12
13const TIMEOUT_SECS: u64 = 60;
14
15/// Create a new HTTP client that can be shared across different clients
16pub fn build_http_client(settings: &HttpConfig) -> Result<Client, ClientError> {
17    let mut headers = HeaderMap::new();
18
19    headers.insert("Username", HeaderValue::from_str(&settings.username)?);
20    headers.insert("Password", HeaderValue::from_str(&settings.password)?);
21
22    let client_builder = Client::builder().timeout(std::time::Duration::from_secs(TIMEOUT_SECS));
23    let client = client_builder.default_headers(headers).build()?;
24    Ok(client)
25}
26
27#[derive(Debug, Clone)]
28pub struct HttpClient {
29    client: Client,
30    base_path: String,
31    pub auth_token: Arc<RwLock<String>>,
32}
33
34impl HttpClient {
35    pub fn new(config: HttpConfig) -> Result<Self, ClientError> {
36        let client = build_http_client(&config)?;
37        debug!("HttpClient created with base path: {}", config.server_uri);
38
39        let api_client = HttpClient {
40            client,
41            auth_token: Arc::new(RwLock::new(String::new())),
42            base_path: format!("{}/{}", config.server_uri, "scouter"),
43        };
44
45        api_client.refresh_token()?;
46
47        debug!(
48            "HTTPClient initialized with base path: {}",
49            api_client.base_path
50        );
51        Ok(api_client)
52    }
53
54    #[instrument(skip_all)]
55    pub fn refresh_token(&self) -> Result<(), ClientError> {
56        let url = format!("{}/{}", self.base_path, Routes::AuthLogin.as_str());
57        debug!("Getting JWT token from {}", url);
58
59        let response = self.client.get(url).send()?;
60
61        // check if unauthorized
62        if response.status().is_client_error() {
63            error!("Unauthorized login request");
64            return Err(ClientError::Unauthorized);
65        }
66
67        // Try to parse as JSON
68        let token = response.json::<JwtToken>().map_err(|e| {
69            error!("Failed to parse response as JSON for http: {}", e);
70            ClientError::ParseJwtTokenError(e.to_string())
71        })?;
72
73        if let Ok(mut token_guard) = self.auth_token.write() {
74            *token_guard = token.token;
75        } else {
76            error!("Failed to acquire write lock for token update");
77            return Err(ClientError::UpdateAuthTokenError);
78        }
79
80        Ok(())
81    }
82
83    pub fn update_token_from_response(&self, response: &Response) {
84        if let Some(new_token) = response
85            .headers()
86            .get(AUTHORIZATION)
87            .and_then(|h| h.to_str().ok())
88            .and_then(|h| h.strip_prefix("Bearer "))
89        {
90            match self.auth_token.write() {
91                Ok(mut token_guard) => {
92                    *token_guard = new_token.to_string();
93                }
94                Err(e) => {
95                    error!("Failed to acquire write lock for jwt token update: {}", e);
96                }
97            }
98        }
99    }
100
101    pub fn get_current_token(&self) -> String {
102        match self.auth_token.read() {
103            Ok(token_guard) => token_guard.clone(),
104            Err(e) => {
105                error!("Failed to acquire read lock for token: {}", e);
106                "".to_string()
107            }
108        }
109    }
110
111    fn _request(
112        &self,
113        route: Routes,
114        request_type: RequestType,
115        body_params: Option<Value>,
116        query_string: Option<String>,
117        headers: Option<HeaderMap>,
118    ) -> Result<Response, ClientError> {
119        let headers = headers.unwrap_or_default();
120
121        let url = format!("{}/{}", self.base_path, route.as_str());
122        let response = match request_type {
123            RequestType::Get => {
124                let url = if let Some(query_string) = query_string {
125                    format!("{url}?{query_string}")
126                } else {
127                    url
128                };
129
130                self.client
131                    .get(url)
132                    .headers(headers)
133                    .bearer_auth(self.get_current_token())
134                    .send()?
135            }
136            RequestType::Post => self
137                .client
138                .post(url)
139                .headers(headers)
140                .json(&body_params)
141                .bearer_auth(self.get_current_token())
142                .send()?,
143            RequestType::Put => self
144                .client
145                .put(url)
146                .headers(headers)
147                .json(&body_params)
148                .bearer_auth(self.get_current_token())
149                .send()?,
150            RequestType::Delete => {
151                let url = if let Some(query_string) = query_string {
152                    format!("{url}?{query_string}")
153                } else {
154                    url
155                };
156                self.client
157                    .delete(url)
158                    .headers(headers)
159                    .bearer_auth(self.get_current_token())
160                    .send()?
161            }
162        };
163
164        Ok(response)
165    }
166
167    pub fn request(
168        &self,
169        route: Routes,
170        request_type: RequestType,
171        body_params: Option<Value>,
172        query_params: Option<String>,
173        headers: Option<HeaderMap>,
174    ) -> Result<Response, ClientError> {
175        let response = self._request(
176            route.clone(),
177            request_type,
178            body_params,
179            query_params,
180            headers,
181        )?;
182
183        // Check and update token if a new one was provided
184        self.update_token_from_response(&response);
185
186        Ok(response)
187    }
188}