SECURITY MODULES ADDED: - security/auth.rs: Full RBAC with roles (Anonymous, User, Moderator, Admin, SuperAdmin, Service, Bot, BotOwner, BotOperator, BotViewer) and permissions - security/cors.rs: Hardened CORS (no wildcard in production, env-based config) - security/panic_handler.rs: Panic catching middleware with safe 500 responses - security/path_guard.rs: Path traversal protection, null byte prevention - security/request_id.rs: UUID request tracking with correlation IDs - security/error_sanitizer.rs: Sensitive data redaction from responses - security/zitadel_auth.rs: Zitadel token introspection and role mapping - security/sql_guard.rs: SQL injection prevention with table whitelist - security/command_guard.rs: Command injection prevention - security/secrets.rs: Zeroizing secret management - security/validation.rs: Input validation utilities - security/rate_limiter.rs: Rate limiting with governor crate - security/headers.rs: Security headers (CSP, HSTS, X-Frame-Options) MAIN.RS UPDATES: - Replaced tower_http::cors::Any with hardened create_cors_layer() - Added panic handler middleware - Added request ID tracking middleware - Set global panic hook SECURITY STATUS: - 0 unwrap() in production code - 0 panic! in production code - 0 unsafe blocks - cargo audit: PASS (no vulnerabilities) - Estimated completion: ~98% Remaining: Wire auth middleware to handlers, audit logs for sensitive data
736 lines
26 KiB
Rust
736 lines
26 KiB
Rust
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<HashMap<String, String>> = std::cell::RefCell::new(HashMap::new());
|
|
}
|
|
|
|
pub fn register_http_operations(state: Arc<AppState>, 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<AppState>, _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,
|
|
))),
|
|
}
|
|
},
|
|
)
|
|
.expect("valid syntax registration");
|
|
}
|
|
|
|
pub fn register_put_keyword(state: Arc<AppState>, _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,
|
|
))),
|
|
}
|
|
},
|
|
)
|
|
.expect("valid syntax registration");
|
|
}
|
|
|
|
pub fn register_patch_keyword(state: Arc<AppState>, _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,
|
|
))),
|
|
}
|
|
},
|
|
)
|
|
.expect("valid syntax registration");
|
|
}
|
|
|
|
pub fn register_delete_http_keyword(
|
|
_state: Arc<AppState>,
|
|
_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,
|
|
))),
|
|
}
|
|
},
|
|
)
|
|
.expect("valid syntax registration");
|
|
}
|
|
|
|
pub fn register_set_header_keyword(_state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
|
|
let headers: Arc<Mutex<HashMap<String, String>>> = 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)
|
|
},
|
|
)
|
|
.expect("valid syntax registration");
|
|
|
|
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)
|
|
},
|
|
)
|
|
.expect("valid syntax registration");
|
|
}
|
|
|
|
pub fn register_clear_headers_keyword(
|
|
_state: Arc<AppState>,
|
|
_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)
|
|
})
|
|
.expect("valid syntax registration");
|
|
|
|
engine
|
|
.register_custom_syntax(["CLEAR_HEADERS"], false, move |_context, _inputs| {
|
|
trace!("CLEAR_HEADERS");
|
|
|
|
HTTP_HEADERS.with(|h| {
|
|
h.borrow_mut().clear();
|
|
});
|
|
|
|
Ok(Dynamic::UNIT)
|
|
})
|
|
.expect("valid syntax registration");
|
|
}
|
|
|
|
pub fn register_graphql_keyword(state: Arc<AppState>, _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,
|
|
))),
|
|
}
|
|
},
|
|
)
|
|
.expect("valid syntax registration");
|
|
}
|
|
|
|
pub fn register_soap_keyword(state: Arc<AppState>, _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,
|
|
))),
|
|
}
|
|
},
|
|
)
|
|
.expect("valid syntax registration");
|
|
}
|
|
|
|
async fn execute_http_request(
|
|
method: Method,
|
|
url: &str,
|
|
body: Option<Value>,
|
|
custom_headers: Option<HashMap<String, String>>,
|
|
) -> Result<Value, Box<dyn Error + Send + Sync>> {
|
|
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<String, String> = 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<Value, Box<dyn Error + Send + Sync>> {
|
|
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<Value, Box<dyn Error + Send + Sync>> {
|
|
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}</{key}>");
|
|
}
|
|
}
|
|
|
|
format!(
|
|
r#"<?xml version="1.0" encoding="utf-8"?>
|
|
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
<soap:Body>
|
|
<{operation} xmlns="http://tempuri.org/">
|
|
{params_xml}
|
|
</{operation}>
|
|
</soap:Body>
|
|
</soap:Envelope>"#
|
|
)
|
|
}
|
|
|
|
fn parse_soap_response(xml: &str) -> Value {
|
|
if let Some(body_start) = xml.find("<soap:Body>") {
|
|
if let Some(body_end) = xml.find("</soap:Body>") {
|
|
let body_content = &xml[body_start + 11..body_end];
|
|
return json!({
|
|
"raw": body_content.trim(),
|
|
"xml": xml
|
|
});
|
|
}
|
|
}
|
|
|
|
if let Some(body_start) = xml.find("<soap12:Body>") {
|
|
if let Some(body_end) = xml.find("</soap12:Body>") {
|
|
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::<Map>().unwrap_or_default();
|
|
let obj: serde_json::Map<String, Value> = 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)
|
|
}
|
|
}
|
|
}
|