extern crate hyper; extern crate serde; use std::{ffi::OsString, path::PathBuf, str}; use log::{error}; use hyper::{Body, Client, Method, Request, Uri}; use async_trait::async_trait; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use crate::{model::{Conversation, JwtToken}, APIInterface}; type HttpClient = Client; pub struct HTTPAPIClient { pub base_url: Uri, credentials: Option, auth_token: Option, client: HttpClient, } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Credentials { pub username: String, pub password: String, } #[derive(Debug)] pub enum Error { ClientError(String), HTTPError(hyper::Error), SerdeError(serde_json::Error), DecodeError, } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Error::HTTPError(ref err) => Some(err), _ => None, } } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } impl From for Error { fn from(err: hyper::Error) -> Error { Error::HTTPError(err) } } impl From for Error { fn from(err: serde_json::Error) -> Error { Error::SerdeError(err) } } trait AuthBuilder { fn with_auth(self, token: &Option) -> Self; } impl AuthBuilder for hyper::http::request::Builder { fn with_auth(self, token: &Option) -> Self { if let Some(token) = &token { self.header("Authorization", token.to_header_value()) } else { self } } } trait AuthSetting { fn authenticate(&mut self, token: &Option); } impl AuthSetting for hyper::http::Request { fn authenticate(&mut self, token: &Option) { if let Some(token) = &token { self.headers_mut().insert("Authorization", token.to_header_value()); } } } #[async_trait] impl APIInterface for HTTPAPIClient { type Error = Error; async fn get_version(&mut self) -> Result { let version: String = self.request("version", Method::GET).await?; Ok(version) } async fn get_conversations(&mut self) -> Result, Self::Error> { let conversations: Vec = self.request("conversations", Method::GET).await?; Ok(conversations) } async fn authenticate(&mut self, credentials: Credentials) -> Result { #[derive(Deserialize, Debug)] struct AuthResponse { jwt: String, } let body = || -> Body { serde_json::to_string(&credentials).unwrap().into() }; let token: AuthResponse = self.request_with_body_retry("authenticate", Method::POST, body, false).await?; let token = JwtToken::new(&token.jwt).map_err(|_| Error::DecodeError)?; self.auth_token = Some(token.clone()); Ok(token) } } impl HTTPAPIClient { pub fn new(base_url: Uri, credentials: Option) -> HTTPAPIClient { HTTPAPIClient { base_url, credentials, auth_token: Option::None, client: Client::new(), } } fn uri_for_endpoint(&self, endpoint: &str) -> Uri { let mut parts = self.base_url.clone().into_parts(); let root_path: PathBuf = parts.path_and_query.unwrap().path().into(); let path = root_path.join(endpoint); parts.path_and_query = Some(path.to_str().unwrap().parse().unwrap()); Uri::try_from(parts).unwrap() } async fn request(&mut self, endpoint: &str, method: Method) -> Result { self.request_with_body(endpoint, method, || { Body::empty() }).await } async fn request_with_body(&mut self, endpoint: &str, method: Method, body_fn: B) -> Result where T: DeserializeOwned, B: Fn() -> Body { self.request_with_body_retry(endpoint, method, body_fn, true).await } async fn request_with_body_retry( &mut self, endpoint: &str, method: Method, body_fn: B, retry_auth: bool) -> Result where T: DeserializeOwned, B: Fn() -> Body { use hyper::StatusCode; let uri = self.uri_for_endpoint(endpoint); log::debug!("Requesting {:?} {:?}", method, uri); let build_request = move |auth: &Option| { let body = body_fn(); Request::builder() .method(&method) .uri(&uri) .with_auth(auth) .body(body) .expect("Unable to build request") }; let request = build_request(&self.auth_token); let mut response = self.client.request(request).await?; log::debug!("-> Response: {:}", response.status()); match response.status() { StatusCode::OK => { /* cool */ }, // 401: Unauthorized. Token may have expired or is invalid. Attempt to renew. StatusCode::UNAUTHORIZED => { if !retry_auth { return Err(Error::ClientError("Unauthorized".into())); } if let Some(credentials) = &self.credentials { self.authenticate(credentials.clone()).await?; let request = build_request(&self.auth_token); response = self.client.request(request).await?; } }, // Other errors: bubble up. _ => { let message = format!("Request failed ({:})", response.status()); return Err(Error::ClientError(message)); } } // Read and parse response body let body = hyper::body::to_bytes(response.into_body()).await?; let parsed: T = match serde_json::from_slice(&body) { Ok(result) => Ok(result), Err(json_err) => { // If JSON deserialization fails, try to interpret it as plain text // Unfortunately the server does return things like this... let s = str::from_utf8(&body).map_err(|_| Error::DecodeError)?; serde_plain::from_str(s).map_err(|_| json_err) } }?; Ok(parsed) } } mod test { use super::*; fn local_mock_client() -> HTTPAPIClient { let base_url = "http://localhost:5738".parse().unwrap(); let credentials = Credentials { username: "test".to_string(), password: "test".to_string(), }; HTTPAPIClient::new(base_url, credentials.into()) } async fn mock_client_is_reachable() -> bool { let mut client = local_mock_client(); let version = client.get_version().await; match version { Ok(_) => true, Err(e) => { error!("Mock client error: {:?}", e); false } } } #[tokio::test] async fn test_version() { if !mock_client_is_reachable().await { log::warn!("Skipping http_client tests (mock server not reachable)"); return; } let mut client = local_mock_client(); let version = client.get_version().await.unwrap(); assert!(version.starts_with("KordophoneMock-")); } #[tokio::test] async fn test_conversations() { if !mock_client_is_reachable().await { log::warn!("Skipping http_client tests (mock server not reachable)"); return; } let mut client = local_mock_client(); let conversations = client.get_conversations().await.unwrap(); assert!(!conversations.is_empty()); } }