use crate::shared::models::UserSession; use crate::shared::state::AppState; use log::{error, trace}; use reqwest::{header::HeaderMap, header::HeaderName, header::HeaderValue, Client, Method}; use rhai::{Dynamic, Engine, Map}; use serde_json::{json, Value}; use std::collections::HashMap; use std::error::Error; use std::sync::{Arc, Mutex}; use std::time::Duration; thread_local! { static HTTP_HEADERS: std::cell::RefCell> = std::cell::RefCell::new(HashMap::new()); } pub fn register_http_operations(state: Arc, user: UserSession, engine: &mut Engine) { register_post_keyword(Arc::clone(&state), user.clone(), engine); register_put_keyword(Arc::clone(&state), user.clone(), engine); register_patch_keyword(Arc::clone(&state), user.clone(), engine); register_delete_http_keyword(Arc::clone(&state), user.clone(), engine); register_set_header_keyword(Arc::clone(&state), user.clone(), engine); register_graphql_keyword(Arc::clone(&state), user.clone(), engine); register_soap_keyword(Arc::clone(&state), user.clone(), engine); register_clear_headers_keyword(state, user, engine); } pub fn register_post_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { let _state_clone = state; engine .register_custom_syntax( ["POST", "$expr$", ",", "$expr$"], false, move |context, inputs| { let url = context.eval_expression_tree(&inputs[0])?.to_string(); let data = context.eval_expression_tree(&inputs[1])?; trace!("POST request to: {url}"); let (tx, rx) = std::sync::mpsc::channel(); let url_clone = url; let data_clone = dynamic_to_json(&data); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build(); let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { execute_http_request(Method::POST, &url_clone, Some(data_clone), None) .await }); tx.send(result).err() } else { tx.send(Err("Failed to build tokio runtime".into())).err() }; if send_err.is_some() { error!("Failed to send POST result from thread"); } }); match rx.recv_timeout(std::time::Duration::from_secs(60)) { Ok(Ok(response)) => Ok(json_to_dynamic(&response)), Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("POST failed: {e}").into(), rhai::Position::NONE, ))), Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { Err(Box::new(rhai::EvalAltResult::ErrorRuntime( "POST request timed out".into(), rhai::Position::NONE, ))) } Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("POST thread failed: {e}").into(), rhai::Position::NONE, ))), } }, ) .unwrap(); } pub fn register_put_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { let _state_clone = state; engine .register_custom_syntax( ["PUT", "$expr$", ",", "$expr$"], false, move |context, inputs| { let url = context.eval_expression_tree(&inputs[0])?.to_string(); let data = context.eval_expression_tree(&inputs[1])?; trace!("PUT request to: {url}"); let (tx, rx) = std::sync::mpsc::channel(); let url_clone = url; let data_clone = dynamic_to_json(&data); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build(); let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { execute_http_request(Method::PUT, &url_clone, Some(data_clone), None) .await }); tx.send(result).err() } else { tx.send(Err("Failed to build tokio runtime".into())).err() }; if send_err.is_some() { error!("Failed to send PUT result from thread"); } }); match rx.recv_timeout(std::time::Duration::from_secs(60)) { Ok(Ok(response)) => Ok(json_to_dynamic(&response)), Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("PUT failed: {e}").into(), rhai::Position::NONE, ))), Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { Err(Box::new(rhai::EvalAltResult::ErrorRuntime( "PUT request timed out".into(), rhai::Position::NONE, ))) } Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("PUT thread failed: {e}").into(), rhai::Position::NONE, ))), } }, ) .unwrap(); } pub fn register_patch_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { let _state_clone = state; engine .register_custom_syntax( ["PATCH", "$expr$", ",", "$expr$"], false, move |context, inputs| { let url = context.eval_expression_tree(&inputs[0])?.to_string(); let data = context.eval_expression_tree(&inputs[1])?; trace!("PATCH request to: {url}"); let (tx, rx) = std::sync::mpsc::channel(); let url_clone = url; let data_clone = dynamic_to_json(&data); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build(); let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { execute_http_request(Method::PATCH, &url_clone, Some(data_clone), None) .await }); tx.send(result).err() } else { tx.send(Err("Failed to build tokio runtime".into())).err() }; if send_err.is_some() { error!("Failed to send PATCH result from thread"); } }); match rx.recv_timeout(std::time::Duration::from_secs(60)) { Ok(Ok(response)) => Ok(json_to_dynamic(&response)), Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("PATCH failed: {e}").into(), rhai::Position::NONE, ))), Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { Err(Box::new(rhai::EvalAltResult::ErrorRuntime( "PATCH request timed out".into(), rhai::Position::NONE, ))) } Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("PATCH thread failed: {e}").into(), rhai::Position::NONE, ))), } }, ) .unwrap(); } pub fn register_delete_http_keyword( _state: Arc, _user: UserSession, engine: &mut Engine, ) { engine .register_custom_syntax( ["DELETE", "HTTP", "$expr$"], false, move |context, inputs| { let url = context.eval_expression_tree(&inputs[0])?.to_string(); trace!("DELETE HTTP request to: {url}"); let (tx, rx) = std::sync::mpsc::channel(); let url_clone = url; std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build(); let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { execute_http_request(Method::DELETE, &url_clone, None, None).await }); tx.send(result).err() } else { tx.send(Err("Failed to build tokio runtime".into())).err() }; if send_err.is_some() { error!("Failed to send DELETE result from thread"); } }); match rx.recv_timeout(std::time::Duration::from_secs(60)) { Ok(Ok(response)) => Ok(json_to_dynamic(&response)), Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("DELETE failed: {e}").into(), rhai::Position::NONE, ))), Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { Err(Box::new(rhai::EvalAltResult::ErrorRuntime( "DELETE request timed out".into(), rhai::Position::NONE, ))) } Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("DELETE thread failed: {e}").into(), rhai::Position::NONE, ))), } }, ) .unwrap(); } pub fn register_set_header_keyword(_state: Arc, _user: UserSession, engine: &mut Engine) { let headers: Arc>> = Arc::new(Mutex::new(HashMap::new())); let headers_clone = Arc::clone(&headers); let headers_clone2 = Arc::clone(&headers); engine .register_custom_syntax( ["SET", "HEADER", "$expr$", ",", "$expr$"], false, move |context, inputs| { let name = context.eval_expression_tree(&inputs[0])?.to_string(); let value = context.eval_expression_tree(&inputs[1])?.to_string(); trace!("SET HEADER: {name} = {value}"); HTTP_HEADERS.with(|h| { h.borrow_mut().insert(name.clone(), value.clone()); }); if let Ok(mut h) = headers_clone.lock() { h.insert(name, value); } Ok(Dynamic::UNIT) }, ) .unwrap(); engine .register_custom_syntax( ["SET_HEADER", "$expr$", ",", "$expr$"], false, move |context, inputs| { let name = context.eval_expression_tree(&inputs[0])?.to_string(); let value = context.eval_expression_tree(&inputs[1])?.to_string(); trace!("SET_HEADER: {name} = {value}"); HTTP_HEADERS.with(|h| { h.borrow_mut().insert(name.clone(), value.clone()); }); if let Ok(mut h) = headers_clone2.lock() { h.insert(name, value); } Ok(Dynamic::UNIT) }, ) .unwrap(); } pub fn register_clear_headers_keyword( _state: Arc, _user: UserSession, engine: &mut Engine, ) { engine .register_custom_syntax(["CLEAR", "HEADERS"], false, move |_context, _inputs| { trace!("CLEAR HEADERS"); HTTP_HEADERS.with(|h| { h.borrow_mut().clear(); }); Ok(Dynamic::UNIT) }) .unwrap(); engine .register_custom_syntax(["CLEAR_HEADERS"], false, move |_context, _inputs| { trace!("CLEAR_HEADERS"); HTTP_HEADERS.with(|h| { h.borrow_mut().clear(); }); Ok(Dynamic::UNIT) }) .unwrap(); } pub fn register_graphql_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { let _state_clone = state; engine .register_custom_syntax( ["GRAPHQL", "$expr$", ",", "$expr$", ",", "$expr$"], false, move |context, inputs| { let endpoint = context.eval_expression_tree(&inputs[0])?.to_string(); let query = context.eval_expression_tree(&inputs[1])?.to_string(); let variables = context.eval_expression_tree(&inputs[2])?; trace!("GRAPHQL request to: {endpoint}"); let (tx, rx) = std::sync::mpsc::channel(); let endpoint_clone = endpoint; let query_clone = query; let variables_json = dynamic_to_json(&variables); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build(); let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { execute_graphql(&endpoint_clone, &query_clone, variables_json).await }); tx.send(result).err() } else { tx.send(Err("Failed to build tokio runtime".into())).err() }; if send_err.is_some() { error!("Failed to send GRAPHQL result from thread"); } }); match rx.recv_timeout(std::time::Duration::from_secs(60)) { Ok(Ok(response)) => Ok(json_to_dynamic(&response)), Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("GRAPHQL failed: {e}").into(), rhai::Position::NONE, ))), Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { Err(Box::new(rhai::EvalAltResult::ErrorRuntime( "GRAPHQL request timed out".into(), rhai::Position::NONE, ))) } Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("GRAPHQL thread failed: {e}").into(), rhai::Position::NONE, ))), } }, ) .unwrap(); } pub fn register_soap_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { let _state_clone = state; engine .register_custom_syntax( ["SOAP", "$expr$", ",", "$expr$", ",", "$expr$"], false, move |context, inputs| { let wsdl = context.eval_expression_tree(&inputs[0])?.to_string(); let operation = context.eval_expression_tree(&inputs[1])?.to_string(); let params = context.eval_expression_tree(&inputs[2])?; trace!("SOAP request to: {wsdl}, operation: {operation}"); let (tx, rx) = std::sync::mpsc::channel(); let wsdl_clone = wsdl; let operation_clone = operation; let params_json = dynamic_to_json(¶ms); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build(); let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { execute_soap(&wsdl_clone, &operation_clone, params_json).await }); tx.send(result).err() } else { tx.send(Err("Failed to build tokio runtime".into())).err() }; if send_err.is_some() { error!("Failed to send SOAP result from thread"); } }); match rx.recv_timeout(std::time::Duration::from_secs(120)) { Ok(Ok(response)) => Ok(json_to_dynamic(&response)), Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("SOAP failed: {e}").into(), rhai::Position::NONE, ))), Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { Err(Box::new(rhai::EvalAltResult::ErrorRuntime( "SOAP request timed out".into(), rhai::Position::NONE, ))) } Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("SOAP thread failed: {e}").into(), rhai::Position::NONE, ))), } }, ) .unwrap(); } async fn execute_http_request( method: Method, url: &str, body: Option, custom_headers: Option>, ) -> Result> { let client = Client::builder() .timeout(Duration::from_secs(60)) .connect_timeout(Duration::from_secs(10)) .build() .map_err(|e| { error!("Failed to build HTTP client: {e}"); e })?; let mut headers = HeaderMap::new(); HTTP_HEADERS.with(|h| { for (name, value) in h.borrow().iter() { if let (Ok(header_name), Ok(header_value)) = ( HeaderName::try_from(name.as_str()), HeaderValue::try_from(value.as_str()), ) { headers.insert(header_name, header_value); } } }); if let Some(custom) = custom_headers { for (name, value) in custom { if let (Ok(header_name), Ok(header_value)) = ( HeaderName::try_from(name.as_str()), HeaderValue::try_from(value.as_str()), ) { headers.insert(header_name, header_value); } } } if body.is_some() && !headers.contains_key("content-type") { headers.insert( reqwest::header::CONTENT_TYPE, HeaderValue::from_static("application/json"), ); } let mut request = client.request(method.clone(), url).headers(headers); if let Some(body_data) = body { request = request.json(&body_data); } let response = request.send().await.map_err(|e| { error!("HTTP {method} request failed for URL {url}: {e}"); e })?; let status = response.status(); let response_headers: HashMap = response .headers() .iter() .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) .collect(); let body_text = response.text().await.unwrap_or_default(); let body_value: Value = serde_json::from_str(&body_text).unwrap_or(Value::String(body_text)); trace!("HTTP {method} request to {url} completed with status: {status}"); Ok(json!({ "status": status.as_u16(), "statusText": status.canonical_reason().unwrap_or(""), "headers": response_headers, "data": body_value })) } async fn execute_graphql( endpoint: &str, query: &str, variables: Value, ) -> Result> { let graphql_body = json!({ "query": query, "variables": variables }); execute_http_request(Method::POST, endpoint, Some(graphql_body), None).await } async fn execute_soap( endpoint: &str, operation: &str, params: Value, ) -> Result> { use std::fmt::Write; let soap_body = build_soap_envelope(operation, ¶ms); let mut headers = HashMap::new(); headers.insert( "Content-Type".to_string(), "text/xml; charset=utf-8".to_string(), ); let mut soap_action = String::new(); let _ = write!(soap_action, "\"{operation}\""); headers.insert("SOAPAction".to_string(), soap_action); let client = Client::builder() .timeout(Duration::from_secs(120)) .connect_timeout(Duration::from_secs(10)) .build() .map_err(|e| { error!("Failed to build HTTP client: {e}"); e })?; let mut header_map = HeaderMap::new(); for (name, value) in headers { if let (Ok(header_name), Ok(header_value)) = ( HeaderName::try_from(name.as_str()), HeaderValue::try_from(value.as_str()), ) { header_map.insert(header_name, header_value); } } let response = client .post(endpoint) .headers(header_map) .body(soap_body) .send() .await .map_err(|e| { error!("SOAP request failed for endpoint {endpoint}: {e}"); e })?; let status = response.status(); let body_text = response.text().await.unwrap_or_default(); let parsed_response = parse_soap_response(&body_text); trace!("SOAP request to {endpoint} completed with status: {status}"); Ok(json!({ "status": status.as_u16(), "data": parsed_response })) } fn build_soap_envelope(operation: &str, params: &Value) -> String { use std::fmt::Write; let mut params_xml = String::new(); if let Value::Object(obj) = params { for (key, value) in obj { let value_str = match value { Value::String(s) => s.clone(), Value::Number(n) => n.to_string(), Value::Bool(b) => b.to_string(), _ => value.to_string(), }; let _ = write!(params_xml, "<{key}>{value_str}"); } } format!( r#" <{operation} xmlns="http://tempuri.org/"> {params_xml} "# ) } fn parse_soap_response(xml: &str) -> Value { if let Some(body_start) = xml.find("") { if let Some(body_end) = xml.find("") { let body_content = &xml[body_start + 11..body_end]; return json!({ "raw": body_content.trim(), "xml": xml }); } } if let Some(body_start) = xml.find("") { if let Some(body_end) = xml.find("") { let body_content = &xml[body_start + 13..body_end]; return json!({ "raw": body_content.trim(), "xml": xml }); } } json!({ "raw": xml, "xml": xml }) } fn dynamic_to_json(value: &Dynamic) -> Value { if value.is_unit() { Value::Null } else if value.is_bool() { Value::Bool(value.as_bool().unwrap_or(false)) } else if value.is_int() { Value::Number(value.as_int().unwrap_or(0).into()) } else if value.is_float() { if let Ok(f) = value.as_float() { serde_json::Number::from_f64(f) .map(Value::Number) .unwrap_or(Value::Null) } else { Value::Null } } else if value.is_string() { Value::String(value.to_string()) } else if value.is_array() { let arr = value.clone().into_array().unwrap_or_default(); Value::Array(arr.iter().map(dynamic_to_json).collect()) } else if value.is_map() { let map = value.clone().try_cast::().unwrap_or_default(); let obj: serde_json::Map = map .iter() .map(|(k, v)| (k.to_string(), dynamic_to_json(v))) .collect(); Value::Object(obj) } else { Value::String(value.to_string()) } } fn json_to_dynamic(value: &Value) -> Dynamic { match value { Value::Null => Dynamic::UNIT, Value::Bool(b) => Dynamic::from(*b), Value::Number(n) => { if let Some(i) = n.as_i64() { Dynamic::from(i) } else if let Some(f) = n.as_f64() { Dynamic::from(f) } else { Dynamic::UNIT } } Value::String(s) => Dynamic::from(s.clone()), Value::Array(arr) => { let rhai_arr: rhai::Array = arr.iter().map(json_to_dynamic).collect(); Dynamic::from(rhai_arr) } Value::Object(obj) => { let mut map = Map::new(); for (k, v) in obj { map.insert(k.clone().into(), json_to_dynamic(v)); } Dynamic::from(map) } } }