1use {
2 log::{debug, error},
3 reqwest::{RequestBuilder, Response, StatusCode},
4 serde::de::DeserializeOwned,
5 smbcloud_model::{
6 account::SmbAuthorization,
7 error_codes::{ErrorCode, ErrorResponse},
8 login::AccountStatus,
9 },
10};
11
12pub fn bearer_value(token: &str) -> String {
18 let token = token.trim();
19 let bare = match token.get(..7) {
20 Some(prefix) if prefix.eq_ignore_ascii_case("bearer ") => token[7..].trim_start(),
21 _ => token,
22 };
23 format!("Bearer {bare}")
24}
25
26#[cfg(debug_assertions)]
28const LOG_RESPONSE_BODY: bool = true; #[cfg(not(debug_assertions))]
30const LOG_RESPONSE_BODY: bool = false;
31
32#[cfg(not(target_arch = "wasm32"))]
38#[deprecated(
39 since = "1.1.0",
40 note = "the request itself reports NetworkError; this pinged a third-party host"
41)]
42pub async fn check_internet_connection() -> bool {
43 debug!("Checking internet connection");
44 let client = reqwest::Client::builder()
45 .build();
47
48 if let Err(e) = client {
49 error!("Failed to create client for connectivity check: {:?}", e);
50 return false;
51 }
52
53 if let Ok(client) = client {
54 match client.get("https://dns.google").send().await {
55 Ok(response) => {
56 debug!(
57 "Internet connection check successful: {}",
58 response.status()
59 );
60 response.status().is_success()
61 }
62 Err(e) => {
63 error!("Internet connection check failed: {:?}", e);
64 false
65 }
66 }
67 } else {
68 false
69 }
70}
71
72#[cfg(target_arch = "wasm32")]
73#[deprecated(
74 since = "1.1.0",
75 note = "the request itself reports NetworkError; this pinged a third-party host"
76)]
77pub async fn check_internet_connection() -> bool {
78 true
79}
80
81pub fn log_send_error(e: &reqwest::Error) {
84 #[cfg(not(target_arch = "wasm32"))]
85 let connect = e.is_connect();
86 #[cfg(target_arch = "wasm32")]
87 let connect = false;
88 let kind = if e.is_timeout() {
89 "timeout"
90 } else if connect {
91 "connect"
92 } else if e.is_builder() {
93 "builder"
94 } else {
95 "other"
96 };
97 debug!("Request failed to send ({kind}): {e:?}");
98}
99
100pub async fn parse_error_response<T: DeserializeOwned>(
101 response: Response,
102) -> Result<T, ErrorResponse> {
103 let error_response_body = match response.text().await {
104 Ok(body) => body,
105 Err(e) => {
106 error!("Failed to get response body: {:?}", e);
107 return Err(ErrorResponse::Error {
108 error_code: ErrorCode::NetworkError,
109 message: ErrorCode::NetworkError.message(None).to_string(),
110 });
111 }
112 };
113
114 if LOG_RESPONSE_BODY {
115 debug!(
116 "Parse Error >>>> {:?}",
117 serde_json::to_string_pretty(&error_response_body)
118 );
119 }
120
121 let error_response = match serde_json::from_str(&error_response_body) {
122 Ok(error_response) => error_response,
123 Err(e) => {
124 error!("Failed to parse error response: {:?}", e);
125 return Err(ErrorResponse::Error {
126 error_code: ErrorCode::ParseError,
127 message: ErrorCode::ParseError.message(None).to_string(),
128 });
129 }
130 };
131 Err(error_response)
134}
135
136pub async fn request_login(builder: RequestBuilder) -> Result<AccountStatus, ErrorResponse> {
137 let response = builder.send().await;
138 let response = match response {
139 Ok(response) => response,
140 Err(e) => {
141 error!("request_login: Failed to get response: {:?}", e);
142 return Err(ErrorResponse::Error {
143 error_code: ErrorCode::NetworkError,
144 message: ErrorCode::NetworkError.message(None).to_string(),
145 });
146 }
147 };
148
149 if LOG_RESPONSE_BODY {
150 debug!("request_login: Parse >>>> {:?}", response.status());
151 }
152
153 match (response.status(), response.headers().get("Authorization")) {
154 (StatusCode::OK, Some(token)) => {
155 let access_token = match token.to_str() {
157 Ok(token) => token.to_string(),
158 Err(_) => {
159 return Err(ErrorResponse::Error {
160 error_code: ErrorCode::NetworkError,
161 message: ErrorCode::NetworkError.message(None).to_string(),
162 });
163 }
164 };
165 Ok(AccountStatus::Ready { access_token })
166 }
167 (StatusCode::OK, None) => {
168 let error_response = match parse_error_response::<ErrorResponse>(response).await {
170 Ok(error) => error,
171 Err(_) => return Ok(AccountStatus::NotFound),
172 };
173 match error_response {
174 ErrorResponse::Error {
175 error_code,
176 message: _,
177 } => match error_code {
178 ErrorCode::EmailNotVerified => Ok(AccountStatus::Incomplete {
179 status: smbcloud_model::account::ErrorCode::EmailUnverified,
180 }),
181 ErrorCode::PasswordNotSet => Ok(AccountStatus::Incomplete {
182 status: smbcloud_model::account::ErrorCode::PasswordNotSet,
183 }),
184 ErrorCode::Unknown => Ok(AccountStatus::Ready {
185 access_token: "tokenization".to_string(),
186 }),
187 _ => Ok(AccountStatus::NotFound),
188 },
189 }
190 }
191 (StatusCode::NOT_FOUND, _) => {
192 Ok(AccountStatus::NotFound)
194 }
195 (StatusCode::UNPROCESSABLE_ENTITY, _) => {
196 let body_text = match response.text().await {
197 Ok(text) => text,
198 Err(_) => {
199 return Err(ErrorResponse::Error {
200 error_code: ErrorCode::NetworkError,
201 message: ErrorCode::NetworkError.message(None).to_string(),
202 });
203 }
204 };
205
206 let result: SmbAuthorization = match serde_json::from_str(&body_text) {
207 Ok(res) => res,
208 Err(_) => {
209 let message = serde_json::from_str::<serde_json::Value>(&body_text)
211 .ok()
212 .and_then(|v| {
213 v.get("message")
214 .and_then(serde_json::Value::as_str)
215 .map(ToOwned::to_owned)
216 })
217 .unwrap_or_else(|| ErrorCode::NetworkError.message(None).to_string());
218 return Err(ErrorResponse::Error {
219 error_code: ErrorCode::NetworkError,
220 message,
221 });
222 }
223 };
224
225 let error_code = match result.error_code {
226 Some(code) => code,
227 None => {
228 return Err(ErrorResponse::Error {
231 error_code: ErrorCode::NetworkError,
232 message: result.message,
233 });
234 }
235 };
236 Ok(AccountStatus::Incomplete { status: error_code })
237 }
238 (StatusCode::UNAUTHORIZED, _) => {
239 let body_text = match response.text().await {
240 Ok(text) => text,
241 Err(_) => {
242 return Err(ErrorResponse::Error {
243 error_code: ErrorCode::NetworkError,
244 message: ErrorCode::NetworkError.message(None).to_string(),
245 });
246 }
247 };
248
249 if let Ok(result) = serde_json::from_str::<SmbAuthorization>(&body_text) {
251 let error_code = match result.error_code {
252 Some(code) => code,
253 None => {
254 return Err(ErrorResponse::Error {
256 error_code: ErrorCode::Unauthorized,
257 message: result.message,
258 });
259 }
260 };
261 return Err(ErrorResponse::Error {
262 error_code: ErrorCode::Unauthorized,
263 message: error_code.to_string(),
264 });
265 }
266
267 if let Ok(generic) = serde_json::from_str::<serde_json::Value>(&body_text) {
269 let extracted = generic
270 .get("message")
271 .or_else(|| generic.get("error"))
272 .and_then(serde_json::Value::as_str)
273 .unwrap_or("Sign in failed.");
274 return Err(ErrorResponse::Error {
275 error_code: ErrorCode::Unauthorized,
276 message: extracted.to_string(),
277 });
278 }
279
280 Err(ErrorResponse::Error {
281 error_code: ErrorCode::Unauthorized,
282 message: "Sign in failed.".to_string(),
283 })
284 }
285 (status, _) => parse_error_response(response)
286 .await
287 .map_err(|_| ErrorResponse::Error {
288 error_code: ErrorCode::NetworkError,
289 message: format!("Unexpected login response status: {}", status),
290 }),
291 }
292}
293
294pub async fn request<R: DeserializeOwned>(builder: RequestBuilder) -> Result<R, ErrorResponse> {
295 let response = builder.send().await;
296 let response = match response {
297 Ok(response) => response,
298 Err(e) => {
299 error!("Failed to get response: {:?}", e);
300 log_send_error(&e);
301 return Err(ErrorResponse::Error {
302 error_code: ErrorCode::NetworkError,
303 message: ErrorCode::NetworkError.message(None).to_string(),
304 });
305 }
306 };
307 let response = match response.status() {
308 reqwest::StatusCode::OK | reqwest::StatusCode::CREATED => response,
309 status => {
310 error!("Failed to get response: {:?}", status);
311 return parse_error_response(response).await;
313 }
314 };
315
316 let response_body = match response.text().await {
317 Ok(body) => body,
318 Err(e) => {
319 error!("Failed to get response body: {:?}", e);
320 return Err(ErrorResponse::Error {
321 error_code: ErrorCode::NetworkError,
322 message: ErrorCode::NetworkError.message(None).to_string(),
323 });
324 }
325 };
326
327 if LOG_RESPONSE_BODY {
328 debug!(
329 "Parse >>>> {:?}",
330 serde_json::to_string_pretty(&response_body)
331 );
332 }
333
334 let response = match serde_json::from_str::<R>(&response_body) {
335 Ok(response) => response,
336 Err(e) => {
337 error!("Failed to parse response: {:?}", e);
338 return Err(ErrorResponse::Error {
339 error_code: ErrorCode::ParseError,
340 message: e.to_string(),
341 });
342 }
343 };
344
345 Ok(response)
346}
347
348#[cfg(all(test, not(target_arch = "wasm32")))]
349mod tests {
350 use super::*;
351 use serde_json::Value;
352 use std::time::{Duration, Instant};
353
354 #[tokio::test]
355 async fn unreachable_host_reports_network_error_quickly() {
356 let started = Instant::now();
357 let builder = reqwest::Client::new().get("http://127.0.0.1:1");
358 let result = request::<Value>(builder).await;
359 match result {
360 Err(ErrorResponse::Error { error_code, .. }) => {
361 assert!(matches!(error_code, ErrorCode::NetworkError));
362 }
363 other => panic!("expected NetworkError, got {other:?}"),
364 }
365 assert!(started.elapsed() < Duration::from_secs(5));
366 }
367
368 #[test]
369 fn workspace_does_not_call_the_connectivity_preflight() {
370 let crates = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
371 let mut callers = Vec::new();
372 let mut stack = vec![crates];
373 while let Some(dir) = stack.pop() {
374 for entry in std::fs::read_dir(&dir).unwrap().flatten() {
375 let path = entry.path();
376 if path.is_dir() {
377 stack.push(path);
378 } else if path.extension().is_some_and(|e| e == "rs")
379 && !path.ends_with("smbcloud-network/src/network.rs")
380 && std::fs::read_to_string(&path)
381 .unwrap()
382 .contains("check_internet_connection")
383 {
384 callers.push(path);
385 }
386 }
387 }
388 assert!(
389 callers.is_empty(),
390 "preflight still called from {callers:?}"
391 );
392 }
393}
394
395#[cfg(test)]
396mod bearer_tests {
397 use super::bearer_value;
398
399 #[test]
400 fn adds_one_prefix() {
401 assert_eq!(bearer_value("Bearer x"), "Bearer x");
402 assert_eq!(bearer_value("bearer x"), "Bearer x");
403 assert_eq!(bearer_value("x"), "Bearer x");
404 assert_eq!(bearer_value("smb_deploy_abc"), "Bearer smb_deploy_abc");
405 }
406}