use crate::webserver::request_variables::SetVariablesMap;
use crate::webserver::server_timing::ServerTiming;
use crate::AppState;
use actix_multipart::form::bytes::Bytes;
use actix_multipart::form::tempfile::TempFile;
use actix_multipart::form::FieldReader;
use actix_multipart::form::Limits;
use actix_multipart::Multipart;
use actix_web::dev::ServiceRequest;
use actix_web::http::header::Header;
use actix_web::http::header::CONTENT_TYPE;
use actix_web::web;
use actix_web::web::Form;
use actix_web::FromRequest;
use actix_web::HttpMessage as _;
use actix_web::HttpRequest;
use actix_web_httpauth::headers::authorization::Authorization;
use actix_web_httpauth::headers::authorization::Basic;
use anyhow::anyhow;
use anyhow::Context;
use std::cell::RefCell;
use std::collections::HashMap;
use std::net::IpAddr;
use std::rc::Rc;
use std::sync::Arc;
use tokio_stream::StreamExt;
use super::oidc::OidcClaims;
use super::request_variables::param_map;
use super::request_variables::ParamMap;
#[derive(Debug)]
pub struct RequestInfo {
pub method: actix_web::http::Method,
pub path: String,
pub protocol: String,
pub url_params: ParamMap,
pub post_variables: ParamMap,
pub uploaded_files: Rc<HashMap<String, TempFile>>,
pub headers: ParamMap,
pub client_ip: Option<IpAddr>,
pub cookies: ParamMap,
pub basic_auth: Option<Basic>,
pub app_state: Arc<AppState>,
pub raw_body: Option<Vec<u8>>,
pub oidc_claims: Option<OidcClaims>,
pub server_timing: Arc<super::server_timing::ServerTiming>,
}
#[derive(Debug)]
pub struct ExecutionContext {
pub request: Rc<RequestInfo>,
pub set_variables: RefCell<SetVariablesMap>,
pub clone_depth: u8,
}
impl ExecutionContext {
#[must_use]
pub fn new(request: RequestInfo) -> Self {
Self {
request: Rc::new(request),
set_variables: RefCell::new(SetVariablesMap::new()),
clone_depth: 0,
}
}
#[must_use]
pub fn fork(&self) -> Self {
Self {
request: Rc::clone(&self.request),
set_variables: RefCell::new(self.set_variables.borrow().clone()),
clone_depth: self.clone_depth + 1,
}
}
#[must_use]
pub fn fork_with_variables(&self, variables: SetVariablesMap) -> Self {
Self {
request: Rc::clone(&self.request),
set_variables: RefCell::new(variables),
clone_depth: self.clone_depth + 1,
}
}
pub fn request(&self) -> &RequestInfo {
self.request.as_ref()
}
}
impl Clone for ExecutionContext {
fn clone(&self) -> Self {
self.fork()
}
}
impl std::ops::Deref for ExecutionContext {
type Target = RequestInfo;
fn deref(&self) -> &Self::Target {
self.request()
}
}
impl<'a> From<&'a ExecutionContext> for &'a RequestInfo {
fn from(ctx: &'a ExecutionContext) -> Self {
ctx.request()
}
}
pub(crate) async fn extract_request_info(
req: &mut ServiceRequest,
app_state: Arc<AppState>,
server_timing: ServerTiming,
) -> anyhow::Result<ExecutionContext> {
let (http_req, payload) = req.parts_mut();
let method = http_req.method().clone();
let protocol = http_req.connection_info().scheme().to_string();
let config = &app_state.config;
let (post_variables, uploaded_files, raw_body) =
extract_post_data(http_req, payload, config).await?;
let headers = req.headers().iter().map(|(name, value)| {
(
name.to_string(),
String::from_utf8_lossy(value.as_bytes()).to_string(),
)
});
let get_variables = web::Query::<Vec<(String, String)>>::from_query(req.query_string())
.map(web::Query::into_inner)
.unwrap_or_default();
let client_ip = req.peer_addr().map(|addr| addr.ip());
let raw_cookies = req.cookies();
let cookies = raw_cookies
.iter()
.flat_map(|c| c.iter())
.map(|cookie| (cookie.name().to_string(), cookie.value().to_string()));
let basic_auth = Authorization::<Basic>::parse(req)
.ok()
.map(Authorization::into_scheme);
let oidc_claims: Option<OidcClaims> = req.extensions().get::<OidcClaims>().cloned();
Ok(ExecutionContext::new(RequestInfo {
method,
path: req.path().to_string(),
headers: param_map(headers),
url_params: param_map(get_variables),
post_variables: param_map(post_variables),
uploaded_files: Rc::new(HashMap::from_iter(uploaded_files)),
client_ip,
cookies: param_map(cookies),
basic_auth,
app_state,
protocol,
raw_body,
oidc_claims,
server_timing: Arc::new(server_timing),
}))
}
async fn extract_post_data(
http_req: &mut actix_web::HttpRequest,
payload: &mut actix_web::dev::Payload,
config: &crate::app_config::AppConfig,
) -> anyhow::Result<(
Vec<(String, String)>,
Vec<(String, TempFile)>,
Option<Vec<u8>>,
)> {
let content_type = http_req
.headers()
.get(&CONTENT_TYPE)
.map(AsRef::as_ref)
.unwrap_or_default();
if content_type.starts_with(b"application/x-www-form-urlencoded") {
let vars = extract_urlencoded_post_variables(http_req, payload).await?;
Ok((vars, Vec::new(), None))
} else if content_type.starts_with(b"multipart/form-data") {
let (vars, files) = extract_multipart_post_data(http_req, payload, config).await?;
Ok((vars, files, None))
} else {
let body = actix_web::web::Bytes::from_request(http_req, payload)
.await
.map(|bytes| bytes.to_vec())
.unwrap_or_default();
Ok((Vec::new(), Vec::new(), Some(body)))
}
}
async fn extract_urlencoded_post_variables(
http_req: &mut actix_web::HttpRequest,
payload: &mut actix_web::dev::Payload,
) -> anyhow::Result<Vec<(String, String)>> {
Form::<Vec<(String, String)>>::from_request(http_req, payload)
.await
.map(Form::into_inner)
.map_err(|e| {
anyhow!(super::ErrorWithStatus {
status: actix_web::http::StatusCode::BAD_REQUEST,
})
.context(format!(
"could not parse request as urlencoded form data: {e}"
))
})
}
async fn extract_multipart_post_data(
http_req: &mut actix_web::HttpRequest,
payload: &mut actix_web::dev::Payload,
config: &crate::app_config::AppConfig,
) -> anyhow::Result<(Vec<(String, String)>, Vec<(String, TempFile)>)> {
let mut post_variables = Vec::new();
let mut uploaded_files = Vec::new();
let mut multipart = Multipart::from_request(http_req, payload)
.await
.map_err(|e| anyhow!("could not parse request as multipart form data: {e}"))?;
let mut limits = Limits::new(config.max_uploaded_file_size, config.max_uploaded_file_size);
log::trace!(
"Parsing multipart form data with a {:?} KiB limit",
limits.total_limit_remaining / 1024
);
while let Some(part) = multipart.next().await {
let field = part.map_err(|e| anyhow!("unable to read form field: {e}"))?;
let content_disposition = field
.content_disposition()
.ok_or_else(|| anyhow!("missing Content-Disposition in form field"))?;
let filename = content_disposition.get_filename();
let field_name = content_disposition
.get_name()
.unwrap_or_default()
.to_string();
log::trace!("Parsing multipart field: {field_name}");
if let Some(filename) = filename {
log::debug!("Extracting file: {field_name} ({filename})");
let extracted = extract_file(http_req, field, &mut limits)
.await
.with_context(|| {
format!(
"Failed to extract file {field_name:?}. Max file size: {} kiB",
config.max_uploaded_file_size / 1_024
)
})?;
log::trace!(
"Extracted file {field_name} to \"{}\"",
extracted.file.path().display()
);
if is_file_field_empty(&extracted).await? {
log::debug!("Ignoring empty file field: {field_name}");
continue;
}
uploaded_files.push((field_name, extracted));
} else {
let text_contents = extract_text(http_req, field, &mut limits).await?;
log::trace!("Extracted field as text: {field_name} = {text_contents:?}");
post_variables.push((field_name, text_contents));
}
}
Ok((post_variables, uploaded_files))
}
async fn extract_text(
req: &HttpRequest,
field: actix_multipart::Field,
limits: &mut Limits,
) -> anyhow::Result<String> {
let data = Bytes::read_field(req, field, limits)
.await
.map(|bytes| bytes.data)
.map_err(|e| anyhow!("failed to read form field data: {e}"))?;
Ok(String::from_utf8(data.to_vec())?)
}
async fn extract_file(
req: &HttpRequest,
field: actix_multipart::Field,
limits: &mut Limits,
) -> anyhow::Result<TempFile> {
let file = TempFile::read_field(req, field, limits)
.await
.map_err(|e| anyhow!("Failed to save uploaded file: {e}"))?;
Ok(file)
}
async fn is_file_field_empty(
uploaded_file: &actix_multipart::form::tempfile::TempFile,
) -> anyhow::Result<bool> {
Ok(
uploaded_file.content_type == Some(mime_guess::mime::APPLICATION_OCTET_STREAM)
&& uploaded_file
.file_name
.as_ref()
.filter(|x| !x.is_empty())
.is_none()
&& tokio::fs::metadata(&uploaded_file.file.path()).await?.len() == 0,
)
}
#[cfg(test)]
mod test {
use super::*;
use crate::webserver::single_or_vec::SingleOrVec;
use crate::{app_config::AppConfig, webserver::server_timing::ServerTiming};
use actix_web::{http::header::ContentType, test::TestRequest};
#[actix_web::test]
async fn test_extract_empty_request() {
let config =
serde_json::from_str::<AppConfig>(r#"{"listen_on": "localhost:1234"}"#).unwrap();
let mut service_request = TestRequest::default().to_srv_request();
let app_data = Arc::new(AppState::init(&config).await.unwrap());
let server_timing = ServerTiming::default();
let request_ctx = extract_request_info(&mut service_request, app_data, server_timing)
.await
.unwrap();
let request_info = request_ctx.request();
assert_eq!(request_info.post_variables.len(), 0);
assert_eq!(request_info.uploaded_files.len(), 0);
assert_eq!(request_info.url_params.len(), 0);
}
#[actix_web::test]
async fn test_extract_urlencoded_request() {
let config =
serde_json::from_str::<AppConfig>(r#"{"listen_on": "localhost:1234"}"#).unwrap();
let mut service_request = TestRequest::get()
.uri("/?my_array[]=5")
.insert_header(ContentType::form_url_encoded())
.set_payload("my_array[]=3&my_array[]=Hello%20World&repeated=1&repeated=2")
.to_srv_request();
let app_data = Arc::new(AppState::init(&config).await.unwrap());
let server_timing = ServerTiming::default();
let request_ctx = extract_request_info(&mut service_request, app_data, server_timing)
.await
.unwrap();
let request_info = request_ctx.request();
assert_eq!(
request_info.post_variables,
vec![
(
"my_array".to_string(),
SingleOrVec::Vec(vec!["3".to_string(), "Hello World".to_string()])
),
("repeated".to_string(), SingleOrVec::Single("2".to_string())), ]
.into_iter()
.collect::<ParamMap>()
);
assert_eq!(request_info.uploaded_files.len(), 0);
assert_eq!(
request_info.url_params,
vec![(
"my_array".to_string(),
SingleOrVec::Vec(vec!["5".to_string()])
)] .into_iter()
.collect::<ParamMap>()
);
}
#[actix_web::test]
async fn test_extract_multipart_form_data() {
let _ = env_logger::try_init();
let config =
serde_json::from_str::<AppConfig>(r#"{"listen_on": "localhost:1234"}"#).unwrap();
let mut service_request = TestRequest::get()
.insert_header(("content-type", "multipart/form-data;boundary=xxx"))
.set_payload(
"--xxx\r\n\
Content-Disposition: form-data; name=\"my_array[]\"\r\n\
Content-Type: text/plain\r\n\
\r\n\
3\r\n\
--xxx\r\n\
Content-Disposition: form-data; name=\"my_uploaded_file\"; filename=\"test.txt\"\r\n\
Content-Type: text/plain\r\n\
\r\n\
Hello World\r\n\
--xxx--\r\n"
)
.to_srv_request();
let app_data = Arc::new(AppState::init(&config).await.unwrap());
let server_timing = ServerTiming::enabled(false);
let request_ctx = extract_request_info(&mut service_request, app_data, server_timing)
.await
.unwrap();
let request_info = request_ctx.request();
assert_eq!(
request_info.post_variables,
vec![(
"my_array".to_string(),
SingleOrVec::Vec(vec!["3".to_string()])
),]
.into_iter()
.collect::<ParamMap>()
);
assert_eq!(request_info.uploaded_files.len(), 1);
let my_upload = &request_info.uploaded_files["my_uploaded_file"];
assert_eq!(my_upload.file_name.as_ref().unwrap(), "test.txt");
assert_eq!(request_info.url_params.len(), 0);
assert_eq!(std::fs::read(&my_upload.file).unwrap(), b"Hello World");
assert_eq!(request_info.url_params.len(), 0);
}
}