use actix_multipart::Multipart;
use actix_web::web::BytesMut;
use actix_web::{web, HttpRequest, Result};
use base64::{engine::general_purpose, Engine as _};
use futures_util::{StreamExt, TryStreamExt};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
use std::env;
use std::path::PathBuf;
use url::form_urlencoded;
use urlencoding;
#[derive(Serialize, Deserialize)]
pub struct RequestInfo {
pub args: BTreeMap<String, Value>,
pub data: String,
pub files: IndexMap<String, Value>,
pub form: IndexMap<String, Value>,
pub headers: IndexMap<String, String>,
pub json: Option<Value>,
pub method: String,
pub origin: String,
pub url: String,
}
#[derive(Serialize, Deserialize)]
pub struct GetRequestInfo {
pub args: BTreeMap<String, Value>,
pub headers: IndexMap<String, String>,
pub origin: String,
pub url: String,
}
#[derive(Serialize, Deserialize)]
pub struct HttpMethodsRequestInfo {
pub args: BTreeMap<String, Value>,
pub data: String,
pub files: IndexMap<String, Value>,
pub form: IndexMap<String, Value>,
pub headers: IndexMap<String, String>,
pub json: Option<Value>,
pub origin: String,
pub url: String,
}
fn is_text_content(data: &[u8]) -> bool {
if data.contains(&0) {
return false;
}
std::str::from_utf8(data).is_ok()
}
fn format_file_content(_filename: &str, data: &[u8]) -> String {
if is_text_content(data) {
match std::str::from_utf8(data) {
Ok(text) => text.to_string(),
Err(_) => "[Invalid UTF-8]".to_string(),
}
} else {
general_purpose::STANDARD.encode(data)
}
}
pub fn get_static_path() -> PathBuf {
let exe_path = env::current_exe().unwrap();
let exe_dir = exe_path.parent().unwrap();
let static_path = exe_dir.join("static");
if !static_path.exists() {
let current_dir_static = PathBuf::from("./static");
if current_dir_static.exists() {
return current_dir_static;
}
}
static_path
}
pub fn sort_hashmap(map: HashMap<String, String>) -> IndexMap<String, String> {
let mut keys: Vec<_> = map.keys().cloned().collect();
keys.sort();
let mut sorted_map = IndexMap::new();
for key in keys {
if let Some(value) = map.get(&key) {
sorted_map.insert(key, value.clone());
}
}
sorted_map
}
pub fn sort_hashmap_value(map: HashMap<String, Value>) -> IndexMap<String, Value> {
let mut keys: Vec<_> = map.keys().cloned().collect();
keys.sort();
let mut sorted_map = IndexMap::new();
for key in keys {
if let Some(value) = map.get(&key) {
sorted_map.insert(key, value.clone());
}
}
sorted_map
}
fn header_matches_pattern(header_name: &str, pattern: &str) -> bool {
let header_lower = header_name.to_lowercase();
let pattern_lower = pattern.to_lowercase();
if pattern_lower.ends_with('*') {
let prefix = &pattern_lower[..pattern_lower.len() - 1];
header_lower.starts_with(prefix)
} else {
header_lower == pattern_lower
}
}
pub fn filter_headers(
headers: HashMap<String, String>,
exclude_patterns: &[String],
) -> HashMap<String, String> {
let proxy_filtered = filter_proxy_headers(headers);
proxy_filtered
.into_iter()
.filter(|(name, _)| {
!exclude_patterns
.iter()
.any(|pattern| header_matches_pattern(name, pattern))
})
.collect()
}
pub fn filter_proxy_headers(headers: HashMap<String, String>) -> HashMap<String, String> {
let proxy_headers: Vec<&str> = vec![
"x-real-ip",
"x-forwarded-for",
"x-forwarded-proto",
"x-forwarded-host",
"x-forwarded-port",
"x-original-uri",
"x-original-url",
"x-forwarded-ssl",
"x-forwarded-scheme",
"x-nginx-proxy",
"cf-ray",
"cf-cache-status",
"cf-connecting-ip",
"cf-ipcountry",
"cf-visitor",
"cf-request-id",
"cf-worker",
"cf-warp-tag-id",
"cf-edge-cache",
"cf-cache-tag",
"cf-railgun",
"cdn-loop",
"cloudfront-viewer-address",
"cloudfront-viewer-asn",
"cloudfront-viewer-country",
"cloudfront-viewer-city",
"cloudfront-viewer-country-name",
"cloudfront-viewer-country-region",
"cloudfront-viewer-country-region-name",
"cloudfront-viewer-latitude",
"cloudfront-viewer-longitude",
"cloudfront-viewer-metro-code",
"cloudfront-viewer-postal-code",
"cloudfront-viewer-time-zone",
"cloudfront-viewer-header-order",
"cloudfront-viewer-header-count",
"cloudfront-forwarded-proto",
"cloudfront-is-android-viewer",
"cloudfront-is-desktop-viewer",
"cloudfront-is-ios-viewer",
"cloudfront-is-mobile-viewer",
"cloudfront-is-smarttv-viewer",
"cloudfront-is-tablet-viewer",
"x-amz-cf-id",
"x-amz-cf-pop",
"x-amz-cloudfront-id",
"x-amzn-trace-id",
"x-amzn-requestid",
"x-amzn-request-id",
"x-amz-request-id",
"x-amzn-elb-id",
"x-amzn-lb-id",
"x-cloud-trace-context",
"x-goog-trace",
"x-goog-request-id",
"x-google-trace",
"x-google-request-id",
"x-gfe-request-trace",
"x-gfe-response-code-details-trace",
"x-goog-iap-jwt-assertion",
"x-forwarded-for-original",
"x-appengine-city",
"x-appengine-citylatlong",
"x-appengine-country",
"x-appengine-region",
"x-appengine-request-id",
"x-appengine-datacenter",
"x-appengine-default-namespace",
"x-appengine-https",
"x-appengine-request-log-id",
"x-appengine-user-ip",
"x-appengine-user-id",
"x-appengine-user-email",
"x-appengine-user-nickname",
"x-appengine-auth-domain",
"x-appengine-cron",
"x-appengine-taskname",
"x-appengine-queuename",
"x-appengine-taskretrycount",
"x-appengine-taskexecutioncount",
"x-appengine-tasketa",
"x-azure-ref",
"x-azure-requestid",
"x-azure-request-id",
"x-ms-request-id",
"x-ms-correlation-request-id",
"x-ms-routing-request-id",
"x-ms-exchange-crosstenant-originalauthenticatedcontext",
"x-ms-exchange-crosstenant-fromentityheader",
"x-ms-exchange-crosstenant-id",
"x-azure-fdid",
"x-azure-socketip",
"x-fd-healthprobe",
"x-azure-clientip",
"x-azure-ref-originshield",
"x-cache-remote",
"x-p3p",
"x-msedge-ref",
"x-azure-appliedaccesspolicy",
"x-azure-appliedpolicy",
];
headers
.into_iter()
.filter(|(name, _)| {
let lowercase_name = name.to_lowercase();
!proxy_headers
.iter()
.any(|&proxy_header| lowercase_name == proxy_header)
})
.collect()
}
pub fn fix_request_info_url(req: &HttpRequest, request_info: &mut RequestInfo) {
let connection_info = req.connection_info();
let scheme = connection_info.scheme();
let host = connection_info.host();
let full_url = format!("{}://{}{}", scheme, host, req.uri());
request_info.url = full_url;
}
pub fn to_http_methods_format(request_info: RequestInfo) -> HttpMethodsRequestInfo {
HttpMethodsRequestInfo {
args: request_info.args,
data: request_info.data,
files: request_info.files,
form: request_info.form,
headers: request_info.headers,
json: request_info.json,
origin: request_info.origin,
url: request_info.url,
}
}
pub fn extract_get_request_info(req: &HttpRequest, exclude_patterns: &[String]) -> GetRequestInfo {
let headers: HashMap<String, String> = req
.headers()
.iter()
.map(|(name, value)| (name.to_string(), value.to_str().unwrap_or("").to_string()))
.collect();
let filtered_headers = filter_headers(headers, exclude_patterns);
let args = parse_multi_value_query_string(req.query_string());
let connection_info = req.connection_info();
let origin = connection_info
.realip_remote_addr()
.unwrap_or("127.0.0.1")
.to_string();
let scheme = connection_info.scheme();
let host = connection_info.host();
let full_url = format!("{}://{}{}", scheme, host, req.uri());
GetRequestInfo {
args,
headers: sort_hashmap(filtered_headers),
origin,
url: full_url,
}
}
fn parse_multi_value_query_string(query_string: &str) -> BTreeMap<String, Value> {
let mut params: BTreeMap<String, Vec<String>> = BTreeMap::new();
if query_string.is_empty() {
return BTreeMap::new();
}
for pair in query_string.split('&') {
if let Some((key, value)) = pair.split_once('=') {
let decoded_key = if key.contains('%') {
urlencoding::decode(key)
.unwrap_or_else(|_| key.into())
.to_string()
} else {
key.to_string()
};
let decoded_value = if value.contains('%') {
urlencoding::decode(value)
.unwrap_or_else(|_| value.into())
.to_string()
} else {
value.to_string()
};
params.entry(decoded_key).or_default().push(decoded_value);
} else if !pair.is_empty() {
let decoded_key = if pair.contains('%') {
urlencoding::decode(pair)
.unwrap_or_else(|_| pair.into())
.to_string()
} else {
pair.to_string()
};
params.entry(decoded_key).or_default().push(String::new());
}
}
params
.into_iter()
.map(|(key, values)| {
let value = if values.len() == 1 {
Value::String(values.into_iter().next().unwrap())
} else {
Value::Array(values.into_iter().map(Value::String).collect())
};
(key, value)
})
.collect()
}
fn parse_multi_value_form_data(form_data: &str) -> BTreeMap<String, Value> {
let mut params: BTreeMap<String, Vec<String>> = BTreeMap::new();
for (key, value) in form_urlencoded::parse(form_data.as_bytes()) {
params
.entry(key.to_string())
.or_default()
.push(value.to_string());
}
params
.into_iter()
.map(|(key, values)| {
let value = if values.len() == 1 {
Value::String(values.into_iter().next().unwrap())
} else {
Value::Array(values.into_iter().map(Value::String).collect())
};
(key, value)
})
.collect()
}
pub fn extract_request_info(
req: &HttpRequest,
body: Option<&str>,
exclude_patterns: &[String],
) -> RequestInfo {
let headers: HashMap<String, String> = req
.headers()
.iter()
.map(|(name, value)| (name.to_string(), value.to_str().unwrap_or("").to_string()))
.collect();
let filtered_headers = filter_headers(headers, exclude_patterns);
let args = parse_multi_value_query_string(req.query_string());
let connection_info = req.connection_info();
let origin = connection_info
.realip_remote_addr()
.unwrap_or("127.0.0.1")
.to_string();
let mut form_data_values: BTreeMap<String, Value> = BTreeMap::new();
let mut data_string = String::new();
if let Some(body_str) = body {
let content_type = req
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if content_type
.to_lowercase()
.starts_with("application/x-www-form-urlencoded")
{
form_data_values = parse_multi_value_form_data(body_str);
} else if content_type
.to_lowercase()
.starts_with("multipart/form-data")
{
data_string = body_str.to_string();
} else {
data_string = body_str.to_string();
}
}
RequestInfo {
args,
data: data_string,
files: IndexMap::new(),
form: sort_hashmap_value(form_data_values.into_iter().collect()),
headers: sort_hashmap(filtered_headers),
json: body.and_then(|b| {
if let Some(content_type) = req
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
{
if content_type.starts_with("application/json") {
serde_json::from_str(b).ok()
} else {
None
}
} else {
None
}
}),
method: req.method().to_string(),
origin,
url: req.uri().to_string(),
}
}
pub async fn extract_request_info_multipart(
req: &HttpRequest,
mut payload: Multipart,
exclude_patterns: &[String],
) -> Result<RequestInfo> {
let headers: HashMap<String, String> = req
.headers()
.iter()
.map(|(name, value)| (name.to_string(), value.to_str().unwrap_or("").to_string()))
.collect();
let filtered_headers = filter_headers(headers, exclude_patterns);
let args = parse_multi_value_query_string(req.query_string());
let origin = req
.connection_info()
.realip_remote_addr()
.unwrap_or("127.0.0.1")
.to_string();
let mut form_data: HashMap<String, Vec<String>> = HashMap::new();
let mut files: HashMap<String, Vec<String>> = HashMap::new();
while let Some(mut field) = payload.try_next().await? {
let content_disposition = field.content_disposition();
let field_name = content_disposition.get_name().map(|s| s.to_string());
let filename = content_disposition.get_filename().map(|s| s.to_string());
if let Some(name) = field_name {
let mut data = Vec::new();
while let Some(chunk) = field.try_next().await? {
data.extend_from_slice(&chunk);
}
if let Some(filename) = filename {
let file_content = format_file_content(&filename, &data);
files.entry(name).or_default().push(file_content);
} else {
if let Ok(value) = String::from_utf8(data) {
form_data.entry(name).or_default().push(value);
}
}
}
}
let files_map: HashMap<String, Value> = files
.into_iter()
.map(|(key, values)| {
let value = if values.len() == 1 {
Value::String(values.into_iter().next().unwrap())
} else {
Value::Array(values.into_iter().map(Value::String).collect())
};
(key, value)
})
.collect();
let form_map: HashMap<String, Value> = form_data
.into_iter()
.map(|(key, values)| {
let value = if values.len() == 1 {
Value::String(values.into_iter().next().unwrap())
} else {
Value::Array(values.into_iter().map(Value::String).collect())
};
(key, value)
})
.collect();
Ok(RequestInfo {
args,
data: String::new(),
files: sort_hashmap_value(files_map),
form: sort_hashmap_value(form_map),
headers: sort_hashmap(filtered_headers),
json: None,
method: req.method().to_string(),
origin,
url: req.uri().to_string(),
})
}
pub fn parse_multi_value_header(
header_value: Option<&actix_web::http::header::HeaderValue>,
) -> Vec<String> {
if let Some(value) = header_value {
if let Ok(value_str) = value.to_str() {
let mut values = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
let chars = value_str.chars();
for ch in chars {
match ch {
'"' => {
in_quotes = !in_quotes;
current.push(ch);
}
',' if !in_quotes => {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
values.push(trimmed);
}
current.clear();
}
_ => {
current.push(ch);
}
}
}
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
values.push(trimmed);
}
values
} else {
Vec::new()
}
} else {
Vec::new()
}
}
pub fn http_date() -> String {
use chrono::Utc;
let now = Utc::now();
now.format("%a, %d %b %Y %H:%M:%S GMT").to_string()
}
pub fn generate_etag() -> String {
use uuid::Uuid;
let uuid = Uuid::new_v4();
format!("\"{}\"", uuid.simple())
}
pub async fn process_request_payload(
req: &HttpRequest,
payload: web::Payload,
exclude_headers: &[String],
path_param: Option<String>,
) -> Result<RequestInfo> {
let content_type = req
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let mut request_info = if content_type
.to_lowercase()
.starts_with("multipart/form-data")
{
let multipart = Multipart::new(req.headers(), payload);
match extract_request_info_multipart(req, multipart, exclude_headers).await {
Ok(info) => info,
Err(_) => extract_request_info(req, None, exclude_headers),
}
} else {
let mut body = BytesMut::new();
let mut payload = payload;
while let Some(chunk) = payload.next().await {
let chunk = chunk?;
body.extend_from_slice(&chunk);
}
let body_string = String::from_utf8_lossy(&body);
extract_request_info(req, Some(&body_string), exclude_headers)
};
fix_request_info_url(req, &mut request_info);
if let Some(path) = path_param {
request_info
.args
.insert("anything".to_string(), serde_json::Value::String(path));
}
Ok(request_info)
}