generalbots/src/basic/keywords/http_operations.rs
Rodrigo Rodriguez (Pragmatismo) c67aaa677a feat(security): Complete security infrastructure implementation
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
2025-12-28 19:29:18 -03:00

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(&params);
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, &params);
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)
}
}
}