use crate::http::headers::HttpHeaderName;
use crate::http::request::HttpVersion;
use crate::http::request_body::RequestBody;
use crate::http::RequestHead;
use crate::stream::ConnectionStream;
use crate::tii_error::{RequestHeadParsingError, TiiError, TiiResult};
use crate::tii_server::ConnectionStreamMetadata;
use crate::util::unwrap_some;
use crate::{
debug_log, error_log, trace_log, util, warn_log, AcceptMimeCharset, AcceptQualityMimeType,
Cookie, HttpHeader, HttpMethod, MimeType, MimeTypeWithCharset, TypeSystem, TypeSystemError,
UserError,
};
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::error::Error;
use std::fmt::Debug;
use std::io::ErrorKind;
use std::str::FromStr;
use std::sync::Arc;
use std::time::SystemTime;
use std::{io, mem};
#[derive(Debug)]
pub struct RequestContext {
id: u128,
timestamp: u128,
peer_address: String,
local_address: String,
request: RequestHead,
body: Option<RequestBody>,
request_entity: Option<Box<dyn Any + Send + Sync>>,
force_connection_close: bool,
stream_meta: Option<Arc<dyn ConnectionStreamMetadata>>,
routed_path: Option<String>,
path_params: Option<HashMap<String, String>>,
properties: Option<HashMap<String, Box<dyn Any + Send>>>,
type_system: TypeSystem,
}
impl RequestContext {
#[allow(clippy::too_many_arguments)] pub fn new(
id: u128,
peer_address: impl ToString,
local_address: impl ToString,
method: HttpMethod,
version: HttpVersion,
path: impl ToString,
query: Vec<(impl ToString, impl ToString)>,
headers: Vec<HttpHeader>,
body: Option<RequestBody>,
stream_meta: Option<Arc<dyn ConnectionStreamMetadata>>,
type_system: TypeSystem,
) -> TiiResult<Self> {
Ok(Self {
id,
timestamp: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|a| a.as_millis())
.unwrap_or(0),
peer_address: peer_address.to_string(),
local_address: local_address.to_string(),
request: RequestHead::new(method, version, path, query, headers)?,
body,
request_entity: None,
force_connection_close: false,
stream_meta,
routed_path: None,
path_params: None,
properties: None,
type_system,
})
}
#[allow(clippy::too_many_arguments)]
fn new_http09(
id: u128,
timestamp: u128,
local_address: String,
peer_address: String,
req: RequestHead,
_stream: &dyn ConnectionStream,
stream_meta: Option<Arc<dyn ConnectionStreamMetadata>>,
type_system: TypeSystem,
) -> TiiResult<RequestContext> {
trace_log!("tii: Request {id} is http 0.9");
Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: None,
request_entity: None,
force_connection_close: true,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
})
}
#[allow(clippy::too_many_arguments)]
fn new_http10(
id: u128,
timestamp: u128,
local_address: String,
peer_address: String,
req: RequestHead,
stream: &dyn ConnectionStream,
stream_meta: Option<Arc<dyn ConnectionStreamMetadata>>,
type_system: TypeSystem,
) -> TiiResult<RequestContext> {
trace_log!("tii: Request {id} is http 1.0");
if let Some(content_length) = req.get_header(&HttpHeaderName::ContentLength) {
let content_length: u64 = content_length.parse().map_err(|_| {
TiiError::from(RequestHeadParsingError::InvalidContentLength(content_length.to_string()))
})?;
if content_length == 0 {
trace_log!("tii: Request {id} has no request body");
return Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: None,
request_entity: None,
force_connection_close: true,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
});
}
trace_log!("tii: Request {id} has {content_length} bytes of request body");
let body = RequestBody::new_with_content_length(stream.new_ref_read(), content_length);
return Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: Some(body),
request_entity: None,
force_connection_close: true,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
});
}
trace_log!(
"tii: Request {id} did not sent Content-Length header. Assuming that it has no request body"
);
Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: None,
request_entity: None,
force_connection_close: true,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
})
}
#[allow(clippy::too_many_arguments)]
fn new_http11(
id: u128,
timestamp: u128,
local_address: String,
peer_address: String,
req: RequestHead,
stream: &dyn ConnectionStream,
stream_meta: Option<Arc<dyn ConnectionStreamMetadata>>,
type_system: TypeSystem,
) -> TiiResult<RequestContext> {
trace_log!("tii: Request {id} is http 1.1");
let content_length =
if let Some(content_length) = req.get_header(&HttpHeaderName::ContentLength) {
Some(content_length.parse::<u64>().map_err(|_| {
TiiError::from(RequestHeadParsingError::InvalidContentLength(content_length.to_string()))
})?)
} else {
None
};
match (
req.get_header(&HttpHeaderName::ContentEncoding),
req.get_header(&HttpHeaderName::TransferEncoding),
) {
(None, None) => match content_length {
None => {
if req.get_header(&HttpHeaderName::Connection) != Some("keep-alive") {
trace_log!(
"tii: Request {id} did not sent Content-Length header. Assuming that it has no request body. Connection: keep-alive was not explicitly requested, so will send Connection: close");
return Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: None,
request_entity: None,
force_connection_close: true,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
});
}
if req.get_method().is_likely_to_have_request_body() {
warn_log!(
"tii: Request {id} did not sent Content-Length header but did request Connection: keep-alive. Assuming that it has no request body. The request method {} usually has a body, will force Connection: close to be safe.", req.get_method()
);
return Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: None,
request_entity: None,
force_connection_close: true,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
});
}
trace_log!(
"tii: Request {id} did not sent Content-Length header. Assuming that it has no request body. Connection: keep-alive was requested, so will trust the client that the request actually has no body.");
Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: None,
request_entity: None,
force_connection_close: false,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
})
}
Some(0) => {
trace_log!("tii: Request {id} has no request body");
Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: None,
request_entity: None,
force_connection_close: false,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
})
}
Some(content_length) => {
trace_log!("tii: Request {id} has {content_length} bytes of request body");
Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: Some(RequestBody::new_with_content_length(stream.new_ref_read(), content_length)),
request_entity: None,
force_connection_close: false,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
})
}
},
(None, Some("chunked")) => {
trace_log!("tii: Request {id} has chunked request body");
let body = RequestBody::new_chunked(stream.new_ref_read());
Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: Some(body),
request_entity: None,
force_connection_close: false,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
})
}
(None, Some("x-gzip")) | (None, Some("gzip")) => {
trace_log!("tii: Request {id} has gzip request body with length of uncompressed content");
let Some(content_length) = content_length else {
error_log!("tii: Request {id} not implemented no transfer encoding, Content-Encoding: gzip/x-gzip without Content-Length header");
return Err(TiiError::from(RequestHeadParsingError::ContentLengthHeaderMissing));
};
let body =
RequestBody::new_gzip_with_uncompressed_length(stream.new_ref_read(), content_length)?;
Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: Some(body),
request_entity: None,
force_connection_close: true,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
})
}
(Some("gzip"), None) | (Some("x-gzip"), None) => {
trace_log!("tii: Request {id} has gzip request body with length of compressed content");
let Some(content_length) = content_length else {
error_log!("tii: Request {id} not implemented Transfer-Encoding: gzip/x-gzip, no Content-Encoding without Content-Length header");
return Err(TiiError::from(RequestHeadParsingError::ContentLengthHeaderMissing));
};
let body = RequestBody::new_gzip_with_compressed_content_length(
stream.new_ref_read(),
content_length,
)?;
Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: Some(body),
request_entity: None,
force_connection_close: false,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
})
}
(Some("gzip"), Some("chunked"))
| (Some("x-gzip"), Some("chunked"))
| (None, Some("gzip, chunked"))
| (None, Some("x-gzip, chunked")) => {
trace_log!("tii: Request {id} has chunked gzip request body");
let body = RequestBody::new_gzip_chunked(stream.new_ref_read())?;
Ok(RequestContext {
id,
timestamp,
peer_address,
local_address,
request: req,
body: Some(body),
request_entity: None,
force_connection_close: false,
properties: None,
routed_path: None,
stream_meta,
path_params: None,
type_system,
})
}
(other_encoding, other_transfer) => {
match other_transfer {
Some("chunked") | None => (),
Some(other) => {
error_log!("tii: Request {id} has unimplemented transfer encoding: {}", other);
return Err(TiiError::from(RequestHeadParsingError::TransferEncodingNotSupported(
other.to_string(),
)));
}
}
let Some(other_encoding) = other_encoding else {
error_log!(
"tii: BUG! Fatal unreachable syntax/encoding reached {:?} {:?}",
other_encoding,
other_transfer
);
util::unreachable();
};
error_log!("tii: Request {id} has unimplemented content encoding: {}", other_encoding);
Err(TiiError::from(RequestHeadParsingError::ContentEncodingNotSupported(
other_encoding.to_string(),
)))
}
}
}
pub fn read(
stream: &dyn ConnectionStream,
stream_meta: Option<Arc<dyn ConnectionStreamMetadata>>,
max_head_buffer_size: usize,
type_system: TypeSystem,
) -> TiiResult<RequestContext> {
let now: u128 =
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map(|a| a.as_millis()).unwrap_or(0);
let id = util::next_id();
let peer_address = stream.peer_addr()?;
let local_address = stream.local_addr()?;
debug_log!("tii: Request {id} local: {} peer: {}", &local_address, &peer_address);
let req = RequestHead::read(id, stream, max_head_buffer_size)?;
match req.get_version() {
HttpVersion::Http09 => Self::new_http09(
id,
now,
local_address,
peer_address,
req,
stream,
stream_meta,
type_system,
),
HttpVersion::Http10 => Self::new_http10(
id,
now,
local_address,
peer_address,
req,
stream,
stream_meta,
type_system,
),
HttpVersion::Http11 => Self::new_http11(
id,
now,
local_address,
peer_address,
req,
stream,
stream_meta,
type_system,
),
}
}
pub fn id(&self) -> u128 {
self.id
}
pub fn get_timestamp(&self) -> u128 {
self.timestamp
}
pub fn peer_address(&self) -> &str {
self.peer_address.as_str()
}
pub fn local_address(&self) -> &str {
self.local_address.as_str()
}
pub fn contains_property(&self, key: impl AsRef<str>) -> bool {
if let Some(prop) = self.properties.as_ref() {
return prop.contains_key(key.as_ref());
}
false
}
pub fn get_property<T: Any + Send>(&self, key: impl AsRef<str>) -> Option<&T> {
if let Some(prop) = self.properties.as_ref() {
if let Some(value) = prop.get(key.as_ref()) {
return value.downcast_ref::<T>();
}
}
None
}
pub fn get_stream_meta<T: ConnectionStreamMetadata>(&self) -> Option<&T> {
if let Some(arc) = self.stream_meta.as_ref() {
return arc.as_ref().as_any().downcast_ref::<T>();
}
None
}
pub fn remove_property(&mut self, key: impl AsRef<str>) -> Option<Box<dyn Any + Send>> {
if let Some(prop) = self.properties.as_mut() {
if let Some(value) = prop.remove(key.as_ref()) {
return Some(value);
}
}
None
}
pub fn set_property<T: Any + Send>(
&mut self,
key: impl ToString,
value: T,
) -> Option<Box<dyn Any + Send>> {
let boxed = Box::new(value) as Box<dyn Any + Send>;
let k = key.to_string();
if let Some(prop) = self.properties.as_mut() {
if let Some(value) = prop.insert(k, boxed) {
return Some(value);
}
return None;
}
let mut nmap = HashMap::new();
nmap.insert(k, boxed);
self.properties = Some(nmap);
None
}
pub fn get_property_keys(&self) -> Box<dyn Iterator<Item = &String> + '_> {
match self.properties.as_ref() {
Some(props) => Box::new(props.keys()),
None => Box::new(std::iter::empty()),
}
}
pub fn get_request_entity(&self) -> Option<&(dyn Any + Send + Sync)> {
self.request_entity.as_ref().map(Box::as_ref)
}
pub fn get_request_entity_mut(&mut self) -> Option<&mut (dyn Any + Send + Sync)> {
self.request_entity.as_mut().map(Box::as_mut)
}
pub fn set_request_entity(
&mut self,
entity: Option<Box<dyn Any + Send + Sync>>,
) -> Option<Box<dyn Any + Send + Sync>> {
mem::replace(&mut self.request_entity, entity)
}
pub fn cast_request_entity<DST: Any + ?Sized + 'static, RET: Any + 'static>(
&self,
receiver: impl FnOnce(&DST) -> RET + 'static,
) -> Result<RET, TypeSystemError> {
let src = self.get_request_entity().ok_or(TypeSystemError::SourceTypeUnknown)?;
let caster = self.type_system.type_cast_wrapper(src.type_id(), TypeId::of::<DST>())?;
caster.call(src, receiver)
}
pub fn cast_request_entity_mut<DST: Any + ?Sized + 'static, RET: Any + 'static>(
&mut self,
receiver: impl FnOnce(&mut DST) -> RET + 'static,
) -> Result<RET, TypeSystemError> {
let src =
self.request_entity.as_mut().map(Box::as_mut).ok_or(TypeSystemError::SourceTypeUnknown)?;
let caster = self.type_system.type_cast_wrapper_mut(Any::type_id(src), TypeId::of::<DST>())?;
caster.call(src, receiver)
}
pub fn get_version(&self) -> HttpVersion {
self.request.get_version()
}
pub fn get_raw_status_line(&self) -> &str {
self.request.get_raw_status_line()
}
pub fn get_path(&self) -> &str {
self.request.get_path()
}
pub fn set_path(&mut self, path: impl ToString) {
self.request.set_path(path)
}
pub fn get_query(&self) -> &[(String, String)] {
self.request.get_query()
}
pub fn query_mut(&mut self) -> &mut Vec<(String, String)> {
self.request.query_mut()
}
pub fn set_query(&mut self, query: Vec<(String, String)>) {
self.request.set_query(query)
}
pub fn add_query_param(&mut self, key: impl ToString, value: impl ToString) {
self.request.add_query_param(key, value)
}
pub fn remove_query_params(&mut self, key: impl AsRef<str>) -> Vec<String> {
self.request.remove_query_params(key)
}
pub fn set_query_param(&mut self, key: impl ToString, value: impl ToString) -> Vec<String> {
self.request.set_query_param(key, value)
}
pub fn get_query_param(&self, key: impl AsRef<str>) -> Option<&str> {
self.request.get_query_param(key)
}
pub fn parse_query_param<T: Any + FromStr<Err = E>, E: Error + Send + Sync + 'static>(
&self,
name: impl AsRef<str>,
) -> TiiResult<Option<T>> {
let name = name.as_ref();
let Some(param) = self.get_query_param(name) else {
return Ok(None);
};
param.parse::<T>().map(Some).map_err(|e| {
TiiError::UserError(UserError::InvalidQueryParameter(
name.to_string(),
TypeId::of::<T>(),
Box::new(e),
))
})
}
pub fn parse_query_param_or<T: Any + FromStr<Err = E>, E: Error + Send + Sync + 'static>(
&self,
name: impl AsRef<str>,
default_value: T,
) -> TiiResult<T> {
Ok(self.parse_query_param(name)?.unwrap_or(default_value))
}
pub fn parse_query_param_or_else<T: Any + FromStr<Err = E>, E: Error + Send + Sync + 'static>(
&self,
name: impl AsRef<str>,
default_value: impl FnOnce() -> T,
) -> TiiResult<T> {
Ok(self.parse_query_param(name)?.unwrap_or_else(default_value))
}
pub fn get_query_params(&self, key: impl AsRef<str>) -> Vec<&str> {
self.request.get_query_params(key)
}
pub fn get_method(&self) -> &HttpMethod {
self.request.get_method()
}
pub fn set_method(&mut self, method: HttpMethod) {
self.request.set_method(method)
}
pub fn get_cookies(&self) -> Vec<Cookie> {
self.request.get_cookies()
}
pub fn get_cookie(&self, name: impl AsRef<str>) -> Option<Cookie> {
self.request.get_cookie(name)
}
pub fn set_accept(&mut self, types: Vec<AcceptQualityMimeType>) {
self.request.set_accept(types)
}
pub fn get_content_type(&self) -> Option<&MimeTypeWithCharset> {
self.request.get_content_type()
}
pub fn set_content_type(&mut self, content_type: MimeType) {
self.request.set_content_type(content_type)
}
pub fn remove_content_type(&mut self) -> Option<MimeTypeWithCharset> {
self.request.remove_content_type()
}
pub fn get_accept(&self) -> &[AcceptQualityMimeType] {
self.request.get_accept()
}
pub fn get_accept_charset(&self) -> &[AcceptMimeCharset] {
self.request.get_accept_charset()
}
pub fn iter_headers(&self) -> impl Iterator<Item = &HttpHeader> {
self.request.iter_headers()
}
pub fn get_header(&self, name: impl AsRef<str>) -> Option<&str> {
self.request.get_header(name)
}
pub fn accepts_gzip(&self) -> bool {
self.request.accepts_gzip()
}
pub fn get_headers(&self, name: impl AsRef<str>) -> Vec<&str> {
self.request.get_headers(name)
}
pub fn remove_headers(&mut self, hdr: impl AsRef<str>) -> TiiResult<()> {
self.request.remove_headers(hdr)
}
pub fn set_header(&mut self, hdr: impl AsRef<str>, value: impl ToString) -> TiiResult<()> {
self.request.set_header(hdr, value)
}
pub fn add_header(&mut self, hdr: impl AsRef<str>, value: impl ToString) -> TiiResult<()> {
self.request.add_header(hdr, value)
}
pub fn request_body(&self) -> Option<&RequestBody> {
self.body.as_ref()
}
pub fn routed_path(&self) -> &str {
self.routed_path.as_deref().unwrap_or("")
}
pub fn get_path_param_keys(&self) -> Box<dyn Iterator<Item = &str> + '_> {
match self.path_params.as_ref() {
Some(props) => Box::new(props.keys().map(|k| k.as_str())),
None => Box::new(std::iter::empty()),
}
}
pub fn get_path_params(&self) -> Box<dyn Iterator<Item = (&str, &str)> + '_> {
match self.path_params.as_ref() {
Some(props) => Box::new(props.iter().map(|(k, v)| (k.as_str(), v.as_str()))),
None => Box::new(std::iter::empty()),
}
}
pub fn get_path_param(&self, asr: impl AsRef<str>) -> Option<&str> {
if let Some(path) = self.path_params.as_ref() {
return path.get(asr.as_ref()).map(|e| e.as_str());
}
None
}
pub fn parse_path_param<T: Any + FromStr<Err = E>, E: Error + Send + Sync + 'static>(
&self,
name: impl AsRef<str>,
) -> TiiResult<T> {
let name = name.as_ref();
self
.path_params
.as_ref()
.and_then(|params| params.get(name))
.ok_or(TiiError::UserError(UserError::MissingPathParameter(name.to_string())))?
.parse::<T>()
.map_err(|e| {
TiiError::UserError(UserError::InvalidPathParameter(
name.to_string(),
TypeId::of::<T>(),
Box::new(e),
))
})
}
pub fn set_path_param(&mut self, key: impl ToString, value: impl ToString) -> Option<String> {
if let Some(path) = self.path_params.as_mut() {
return path.insert(key.to_string(), value.to_string());
}
self.path_params = Some(HashMap::new());
unwrap_some(self.path_params.as_mut()).insert(key.to_string(), value.to_string());
None
}
pub fn set_routed_path<T: ToString>(&mut self, rp: T) {
self.routed_path.replace(rp.to_string());
}
pub fn set_body_consume_old(&mut self, body: Option<RequestBody>) -> io::Result<()> {
if let Some(old_body) = self.body.as_ref() {
consume_body(old_body)?
}
self.body = body;
Ok(())
}
pub fn force_connection_close(&mut self) {
self.force_connection_close = true;
}
pub fn is_connection_close_forced(&self) -> bool {
self.force_connection_close
}
pub fn consume_request_body(&self) -> io::Result<()> {
if let Some(body) = self.body.as_ref() {
consume_body(body)?
}
Ok(())
}
pub(crate) fn get_type_system(&self) -> &TypeSystem {
&self.type_system
}
}
fn consume_body(body: &RequestBody) -> io::Result<()> {
let mut discarding_buffer = [0; 0x1_00_00]; loop {
let discarded = body.read(discarding_buffer.as_mut_slice()).or_else(|e| {
if e.kind() == ErrorKind::UnexpectedEof {
Ok(0)
} else {
Err(e)
}
})?;
if discarded == 0 {
return Ok(());
}
}
}