1use crate::http::error::{HttpError, HttpResult};
5use sms_types::gnss::{FixStatus, PositionReport};
6use sms_types::http::{
7 HttpModemBatteryLevelResponse, HttpModemNetworkOperatorResponse,
8 HttpModemNetworkStatusResponse, HttpModemSignalStrengthResponse, HttpPaginationOptions,
9 HttpSmsDeviceInfoResponse, HttpSmsSendResponse, LatestNumberFriendlyNamePair,
10};
11use sms_types::sms::{SmsDeliveryReport, SmsOutgoingMessage};
12
13pub mod error;
14pub mod paginator;
15
16async fn read_http_response<T>(response: reqwest::Response) -> HttpResult<T>
19where
20 T: serde::de::DeserializeOwned,
21{
22 let is_json = response
23 .headers()
24 .get(reqwest::header::CONTENT_TYPE)
25 .and_then(|ct| ct.to_str().ok())
26 .is_some_and(|ct| ct.contains("application/json"));
27
28 if is_json {
29 let json: serde_json::Value = response.json().await?;
31 let success = json
32 .get("success")
33 .and_then(serde_json::Value::as_bool)
34 .unwrap_or(false);
35
36 if !success {
37 let message = json
38 .get("error")
39 .and_then(|v| v.as_str())
40 .unwrap_or("Unknown API error!")
41 .to_string();
42
43 return Err(HttpError::ApiError(message));
44 }
45
46 let response_value = json
48 .get("response")
49 .ok_or(HttpError::MissingResponseField)?;
50
51 return serde_json::from_value(response_value.clone()).map_err(HttpError::JsonError);
52 }
53
54 let status = response.status();
56 if !status.is_success() {
57 let error_text = response
58 .text()
59 .await
60 .unwrap_or_else(|_| "Unknown error!".to_string());
61
62 return Err(HttpError::HttpStatus {
63 status: status.as_u16(),
64 message: error_text,
65 });
66 }
67
68 Err(HttpError::MissingResponseField)
69}
70
71fn client_builder(config: Option<&crate::config::TLSConfig>) -> HttpResult<reqwest::ClientBuilder> {
73 let builder = reqwest::Client::builder();
74 let Some(tls_config) = config.as_ref() else {
75 return Ok(builder);
76 };
77
78 #[cfg(not(any(feature = "http-tls-rustls", feature = "http-tls-native")))]
79 {
80 let _ = tls_config; Err(HttpError::TLSError(
82 "TLS configuration provided but no TLS features enabled. Enable either 'http-tls-rustls' or 'http-tls-native' feature".to_string()
83 ))
84 }
85
86 #[cfg(any(feature = "http-tls-rustls", feature = "http-tls-native"))]
87 {
88 let mut builder = builder;
89
90 #[cfg(feature = "http-tls-rustls")]
92 {
93 builder = builder.use_rustls_tls();
94 }
95
96 #[cfg(feature = "http-tls-native")]
97 {
98 builder = builder.use_native_tls();
99 }
100
101 let certificate = load_certificate(&tls_config.certificate)?;
103 Ok(builder.add_root_certificate(certificate))
104 }
105}
106
107#[cfg(any(feature = "http-tls-rustls", feature = "http-tls-native"))]
109fn load_certificate(cert_path: &std::path::Path) -> HttpResult<reqwest::tls::Certificate> {
110 let cert_data = std::fs::read(cert_path).map_err(HttpError::IOError)?;
111
112 if let Some(ext) = cert_path.extension().and_then(|s| s.to_str()) {
114 match ext {
115 "pem" => return Ok(reqwest::tls::Certificate::from_pem(&cert_data)?),
116 "der" => return Ok(reqwest::tls::Certificate::from_der(&cert_data)?),
117 "crt" => {
118 if cert_data.starts_with(b"-----BEGIN") {
119 return Ok(reqwest::tls::Certificate::from_pem(&cert_data)?);
120 } else {
121 return Ok(reqwest::tls::Certificate::from_der(&cert_data)?);
122 }
123 }
124 _ => {} }
126 }
127
128 reqwest::tls::Certificate::from_pem(&cert_data)
130 .or_else(|_| reqwest::tls::Certificate::from_der(&cert_data))
131 .map_err(Into::into)
132}
133
134#[derive(Debug)]
136pub struct HttpClient {
137 base_url: reqwest::Url,
138 authorization: Option<String>,
139 modem_timeout: Option<std::time::Duration>,
140 client: reqwest::Client,
141}
142impl HttpClient {
143 pub fn new(
145 config: crate::config::HttpConfig,
146 tls: Option<&crate::config::TLSConfig>,
147 ) -> HttpResult<Self> {
148 let client = client_builder(tls)?.timeout(config.base_timeout).build()?;
149
150 Ok(Self {
151 base_url: reqwest::Url::parse(config.url.as_str())?,
152 authorization: config.authorization,
153 modem_timeout: config.modem_timeout,
154 client,
155 })
156 }
157
158 pub async fn set_friendly_name(
160 &self,
161 phone_number: impl Into<String>,
162 friendly_name: Option<impl Into<String>>,
163 ) -> HttpResult<bool> {
164 let body = serde_json::json!({
165 "phone_number": phone_number.into(),
166 "friendly_name": friendly_name.map(Into::into)
167 });
168
169 let url = self.base_url.join("/db/friendly-names/set")?;
170 let response = self
171 .setup_request(false, self.client.post(url))
172 .json(&body)
173 .send()
174 .await?;
175
176 read_http_response(response).await
177 }
178
179 pub async fn get_friendly_name(
181 &self,
182 phone_number: impl Into<String>,
183 ) -> HttpResult<Option<String>> {
184 let body = serde_json::json!({
185 "phone_number": phone_number.into()
186 });
187
188 let url = self.base_url.join("/db/friendly-names/get")?;
189 let response = self
190 .setup_request(false, self.client.post(url))
191 .json(&body)
192 .send()
193 .await?;
194
195 read_http_response(response).await
196 }
197
198 pub async fn get_messages(
201 &self,
202 phone_number: impl Into<String>,
203 pagination: Option<HttpPaginationOptions>,
204 ) -> HttpResult<Vec<sms_types::sms::SmsMessage>> {
205 let mut body = serde_json::json!({
206 "phone_number": phone_number.into()
207 });
208 if let Some(pagination) = pagination {
209 pagination.add_to_body(&mut body);
210 }
211
212 let url = self.base_url.join("/db/messages")?;
213 let response = self
214 .setup_request(false, self.client.post(url))
215 .json(&body)
216 .send()
217 .await?;
218
219 read_http_response(response).await
220 }
221
222 pub async fn get_latest_numbers(
225 &self,
226 pagination: Option<HttpPaginationOptions>,
227 ) -> HttpResult<Vec<LatestNumberFriendlyNamePair>> {
228 let url = self.base_url.join("/db/latest-numbers")?;
229 let mut request = self.setup_request(false, self.client.post(url));
230
231 if let Some(pagination) = pagination {
233 request = request.json(&pagination);
234 }
235
236 let response = request.send().await?;
237 read_http_response(response).await
238 }
239
240 pub async fn get_delivery_reports(
243 &self,
244 message_id: i64,
245 pagination: Option<HttpPaginationOptions>,
246 ) -> HttpResult<Vec<SmsDeliveryReport>> {
247 let mut body = serde_json::json!({
248 "message_id": message_id
249 });
250 if let Some(pagination) = pagination {
251 pagination.add_to_body(&mut body);
252 }
253
254 let url = self.base_url.join("/db/delivery-reports")?;
255 let response = self
256 .setup_request(false, self.client.post(url))
257 .json(&body)
258 .send()
259 .await?;
260
261 read_http_response(response).await
262 }
263
264 pub async fn send_sms(&self, message: &SmsOutgoingMessage) -> HttpResult<HttpSmsSendResponse> {
268 let url = self.base_url.join("/sms/send")?;
269
270 let mut request = self.setup_request(true, self.client.post(url));
273 if let Some(timeout) = message.timeout {
274 request = request.timeout(std::time::Duration::from_secs(u64::from(timeout) + 5));
275 }
276
277 let response = request.json(message).send().await?;
278 read_http_response(response).await
279 }
280
281 pub async fn get_network_status(&self) -> HttpResult<HttpModemNetworkStatusResponse> {
283 self.modem_request("/sms/modem-status").await
284 }
285
286 pub async fn get_signal_strength(&self) -> HttpResult<HttpModemSignalStrengthResponse> {
288 self.modem_request("/sms/signal-strength").await
289 }
290
291 pub async fn get_network_operator(&self) -> HttpResult<HttpModemNetworkOperatorResponse> {
294 self.modem_request("/sms/network-operator").await
295 }
296
297 pub async fn get_service_provider(&self) -> HttpResult<String> {
300 self.modem_request("/sms/service-provider").await
301 }
302
303 pub async fn get_battery_level(&self) -> HttpResult<HttpModemBatteryLevelResponse> {
305 self.modem_request("/sms/battery-level").await
306 }
307
308 pub async fn get_gnss_status(&self) -> HttpResult<FixStatus> {
311 self.modem_request("/gnss/status").await
312 }
313
314 pub async fn get_gnss_location(&self) -> HttpResult<PositionReport> {
318 self.modem_request("/gnss/location").await
319 }
320
321 pub async fn get_device_info(&self) -> HttpResult<HttpSmsDeviceInfoResponse> {
323 let url = self.base_url.join("/sms/device-info")?;
324 let response = self
325 .setup_request(true, self.client.get(url))
326 .send()
327 .await?;
328
329 read_http_response(response).await
330 }
331
332 pub async fn get_phone_number(&self) -> HttpResult<Option<String>> {
335 let url = self.base_url.join("/sys/phone-number")?;
336 let response = self
337 .setup_request(false, self.client.get(url))
338 .send()
339 .await?;
340
341 read_http_response(response).await
342 }
343
344 pub async fn get_version(&self) -> HttpResult<String> {
347 let url = self.base_url.join("/sys/version")?;
348 let response = self
349 .setup_request(false, self.client.get(url))
350 .send()
351 .await?;
352
353 read_http_response(response).await
354 }
355
356 pub async fn set_log_level(&self, level: impl Into<String>) -> HttpResult<bool> {
358 let body = serde_json::json!({
359 "level": level.into()
360 });
361
362 let url = self.base_url.join("/sys/set-log-level")?;
363 let response = self
364 .setup_request(false, self.client.post(url))
365 .json(&body)
366 .send()
367 .await?;
368
369 read_http_response(response).await
370 }
371
372 async fn modem_request<T>(&self, route: &str) -> HttpResult<T>
374 where
375 T: serde::de::DeserializeOwned,
376 {
377 let url = self.base_url.join(route)?;
378 let response = self
379 .setup_request(true, self.client.get(url))
380 .send()
381 .await?;
382
383 read_http_response::<T>(response).await
384 }
385
386 fn setup_request(
389 &self,
390 is_modem: bool,
391 builder: reqwest::RequestBuilder,
392 ) -> reqwest::RequestBuilder {
393 let builder = if is_modem && let Some(timeout) = &self.modem_timeout {
394 builder.timeout(*timeout)
395 } else {
396 builder
397 };
398 if let Some(auth) = &self.authorization {
399 builder.header("authorization", auth)
400 } else {
401 builder
402 }
403 }
404}