pub mod ctype; pub mod url; use std::str::FromStr; use crate::{ auth::{Auth, BasicAuth, OAuth}, error::AppError, request::{ctype::ContentType, url::RequestUrl}, workspace::WorkspaceEntryBase, AppResult, }; use base64::{prelude::BASE64_STANDARD, Engine}; use reqwest::{ header::{self, HeaderMap, HeaderValue}, Body, Method, }; use serde::{Deserialize, Serialize}; use sqlx::prelude::FromRow; use tauri_plugin_log::log; pub const DEFAULT_HEADERS: &'static [(&'static str, &'static str)] = &[ ("user-agent", "rquest/0.0.1"), ("accept", "*/*"), ("accept-encoding", "gzip, defalte, br"), ]; pub async fn send(client: reqwest::Client, req: HttpRequestParameters) -> AppResult { let HttpRequestParameters { url, method, mut headers, body, } = req; fn insert_ct_if_missing(headers: &mut HeaderMap, value: &'static str) { if !headers.contains_key(header::CONTENT_TYPE) { headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(value)); } } let body = match body { Some(body) => { match body.ty { ContentType::Text => insert_ct_if_missing(&mut headers, "text/plain"), ContentType::Json => { insert_ct_if_missing(&mut headers, "application/json"); serde_json::from_str::(&body.path)?; } ContentType::Xml => { insert_ct_if_missing(&mut headers, "application/xml"); roxmltree::Document::parse(&body.path)?; } ContentType::FormUrlEncoded => { serde_urlencoded::from_str::>(&body.path) .map_err(|e| AppError::SerdeUrl(e.to_string()))?; } // Handled by reqwest ContentType::FormData => {} }; Some(Body::from(body.path)) } None => None, }; let mut req = client.request(method, url).headers(headers); if let Some(body) = body { req = req.body(body); } let req = req.build()?; dbg!(&req); let res = match client.execute(req).await { Ok(res) => { log::debug!( "{} {} {:?} {:#?}", res.remote_addr() .map(|addr| addr.to_string()) .unwrap_or(String::new()), res.status(), res.content_length(), res.headers() ); let status = res.status(); let headers = res.headers().clone(); let body = ResponseBody::try_from_response(res).await?; HttpResponse::new(status.as_u16() as usize, headers, body) } Err(e) => return Err(e.into()), }; Ok(res) } /// Load the request body into a vec, validating it beforehand to ensure no syntactic errors are /// present in bodies that need valid syntax. pub async fn get_valid_request_body(path: &str, ty: ContentType) -> AppResult { let body = tokio::fs::read_to_string(path).await?; match ty { ContentType::Text => {} ContentType::Json => { serde_json::from_str::(&body)?; } ContentType::Xml => { roxmltree::Document::parse(&body)?; } ContentType::FormUrlEncoded => { serde_urlencoded::from_str::>(&body) .map_err(|e| AppError::SerdeUrl(e.to_string()))?; } // Handled by reqwest ContentType::FormData => {} }; Ok(body) } #[derive(Debug, Serialize)] pub struct WorkspaceRequest { /// Workspace entry representing this request. pub entry: WorkspaceEntryBase, /// Request method. pub method: String, /// The request URL pub url: String, /// Request HTTP body. pub body: Option, /// HTTP header names => values. pub headers: Vec, /// URL path keys => values. pub path_params: Vec, } impl WorkspaceRequest { pub fn new(entry: WorkspaceEntryBase, method: String, url: String) -> Self { Self { entry, method, url, body: None, headers: vec![], path_params: vec![], } } pub fn from_params_and_headers( entry: WorkspaceEntryBase, params: RequestParams, headers: Vec, path_params: Vec, ) -> Self { let body = match (params.body_id, params.body, params.content_type) { (Some(id), Some(body), Some(content_type)) => Some(EntryRequestBody { id, body, content_type, }), (None, None, None) => None, _ => panic!("id, body and content_type must all be present"), }; WorkspaceRequest { entry, method: params.method, url: params.url, body, headers, path_params, } } pub fn resolve_auth(&mut self, auth: Auth) -> AppResult<()> { match auth { crate::auth::Auth::Token(token) => match token.placement { crate::auth::TokenPlacement::Query => { let mut url = RequestUrl::parse(&self.url)?; url.query_params.push((&token.name, &token.value)); self.url = url.to_string(); } crate::auth::TokenPlacement::Header => { self.headers.push(RequestHeader { id: -1, name: token.name, value: token.value, }); } }, crate::auth::Auth::Basic(BasicAuth { user, password }) => { let value = format!( "Basic {}", BASE64_STANDARD.encode(format!("{user}:{password}")) ); self.headers.push(RequestHeader { id: -1, name: "Authorization".to_string(), value, }); } crate::auth::Auth::OAuth(OAuth { token_name, callback_url, auth_url, token_url, refresh_url, client_id, client_secret, scope, state, grant_type, }) => { todo!() } } Ok(()) } } /// Finalized request parameters obtained from a [WorkspaceRequest]. #[derive(Debug)] pub struct HttpRequestParameters { pub url: String, pub method: reqwest::Method, pub headers: HeaderMap, pub body: Option, } impl TryFrom for HttpRequestParameters { type Error = String; fn try_from(value: WorkspaceRequest) -> Result { let method = match Method::from_str(&value.method) { Ok(m) => m, Err(e) => return Err(e.to_string()), }; let mut headers = HeaderMap::new(); for header in value.headers { headers.insert( reqwest::header::HeaderName::from_str(&header.name).map_err(|e| e.to_string())?, HeaderValue::from_str(&header.value).map_err(|e| e.to_string())?, ); } Ok(Self { url: value.url, method, headers, body: value.body.map(|body| RequestBody { path: body.body, ty: body.content_type, }), }) } } #[derive(Debug, Serialize)] pub struct HttpResponse { pub status: usize, // pub headers: HeaderMap, pub body: Option, } impl HttpResponse { pub fn new(status: usize, headers: HeaderMap, body: Option) -> Self { Self { status, // headers, body, } } } #[derive(Debug, Clone, Serialize)] pub enum ResponseBody { Text(String), /// A pretty printed JSON string Json(String), // TODO: // Xml(String), // HTML // Binary } impl ResponseBody { pub fn len(&self) -> usize { match self { ResponseBody::Text(t) => t.len(), ResponseBody::Json(t) => t.len(), } } pub async fn try_from_response(res: reqwest::Response) -> AppResult> { if res.content_length().is_none() { log::debug!("Response no content"); } let Some(ct) = res.headers().get(header::CONTENT_TYPE) else { log::warn!("Response does not contain content-type header, attempting to read as text"); return Ok(Some(Self::Text(res.text().await?))); }; let ct = match ct.to_str() { Ok(ct) => ct, Err(e) => { log::warn!("Unable to parse content-type header: {e}"); return Err(e.into()); } }; let ct: mime::Mime = ct.parse()?; if ct.subtype() == mime::JSON || ct.suffix().is_some_and(|s| s == mime::JSON) { log::debug!("reading body"); let json = serde_json::to_string_pretty(&res.json::().await?)?; log::debug!("body read"); return Ok(Some(Self::Json(json))); } if ct.type_() == mime::TEXT { log::debug!("reading body"); let text = res.text().await?; log::debug!("body read"); return Ok(Some(Self::Text(text))); } log::warn!("Body did not match anything!"); Ok(None) } } #[derive(Debug)] pub struct RequestParams { /// ID of the workspace entry representing this request. pub id: i64, pub method: String, pub url: String, pub content_type: Option, pub body: Option, pub body_id: Option, } #[derive(Debug, Deserialize, Serialize)] pub struct RequestPathParam { pub position: i64, pub name: String, pub value: String, } #[derive(Debug, Deserialize)] pub struct RequestPathUpdate { pub position: usize, pub name: String, pub value: Option, } #[derive(Debug, Serialize, FromRow)] pub struct RequestHeader { pub id: i64, pub name: String, pub value: String, } #[derive(Debug, Deserialize)] pub struct RequestHeaderInsert { pub name: String, pub value: String, } #[derive(Debug, Deserialize)] pub struct RequestHeaderUpdate { pub id: i64, pub name: Option, pub value: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct RequestBody { pub path: String, pub ty: ContentType, } #[derive(Debug, Serialize)] pub struct EntryRequestBody { pub id: i64, pub body: String, pub content_type: ContentType, }