use crate::{error_log, AcceptMimeCharset, HttpMethod, MimeCharset, MimeTypeWithCharset};
use crate::{trace_log, Cookie};
use crate::{Headers, HttpHeader, HttpHeaderName};
use crate::tii_error::{RequestHeadParsingError, TiiError, TiiResult, UserError};
use crate::util::{unwrap_ok, unwrap_some};
use crate::warn_log;
use crate::ConnectionStream;
use crate::{AcceptQualityMimeType, MimeType, QValue};
use std::fmt::{Display, Formatter};
use std::io::ErrorKind;
use std::vec;
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[non_exhaustive] pub enum HttpVersion {
Http09,
Http10,
Http11,
}
impl HttpVersion {
pub fn as_str(&self) -> &'static str {
match self {
HttpVersion::Http09 => "HTTP/0.9",
HttpVersion::Http10 => "HTTP/1.0",
HttpVersion::Http11 => "HTTP/1.1",
}
}
pub fn as_net_str(&self) -> &'static str {
match self {
HttpVersion::Http09 => "",
HttpVersion::Http10 => "HTTP/1.0",
HttpVersion::Http11 => "HTTP/1.1",
}
}
}
impl Display for HttpVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
HttpVersion::Http09 => f.write_str("HTTP/0.9"),
HttpVersion::Http10 => f.write_str("HTTP/1.0"),
HttpVersion::Http11 => f.write_str("HTTP/1.1"),
}
}
}
impl HttpVersion {
pub fn try_from_net_str<T: AsRef<str>>(value: T) -> Result<Self, T> {
match value.as_ref() {
"HTTP/1.0" => Ok(HttpVersion::Http10),
"HTTP/1.1" => Ok(HttpVersion::Http11),
"" => Ok(HttpVersion::Http09),
_ => Err(value),
}
}
pub fn try_from_str<T: AsRef<str>>(value: T) -> Result<Self, T> {
match value.as_ref() {
"HTTP/1.0" => Ok(HttpVersion::Http10),
"HTTP/1.1" => Ok(HttpVersion::Http11),
"HTTP/0.9" => Ok(HttpVersion::Http09),
_ => Err(value),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct RequestHead {
method: HttpMethod,
version: HttpVersion,
status_line: String,
path: String,
query: Vec<(String, String)>,
accept: Vec<AcceptQualityMimeType>,
content_type: Option<MimeTypeWithCharset>,
accept_charset: Vec<AcceptMimeCharset>,
headers: Headers,
}
fn validate_raw_path(raw_path: &str) -> TiiResult<()> {
if !raw_path.starts_with("/") {
trace_log!("validate_raw_path Err {raw_path} because it does not start with /");
return Err(TiiError::RequestHeadParsing(RequestHeadParsingError::InvalidPath(
raw_path.to_string(),
)));
}
for n in raw_path.bytes() {
match n {
b'/' => {}
b'-' => {}
b'.' => {}
b'_' => {}
b'~' => {}
b'!' => {}
b'$' => {}
b'\'' => {}
b'(' => {}
b')' => {}
b'*' => {}
b'+' => {}
b',' => {}
b';' => {}
b'=' => {}
b':' => {}
b'@' => {}
b'%' => {}
b'\\' => {}
_ => {
if !n.is_ascii_alphanumeric() {
trace_log!("validate_raw_path Err {raw_path} due to byte {n}");
return Err(TiiError::RequestHeadParsing(RequestHeadParsingError::InvalidPath(
raw_path.to_string(),
)));
}
}
}
}
Ok(())
}
fn parse_status_line(start_line_buf: &Vec<u8>) -> TiiResult<&str> {
for n in start_line_buf {
match *n {
b'!' => {}
b'$' => {}
b'&' => {}
b'\'' => {}
b'(' => {}
b')' => {}
b'*' => {}
b'+' => {}
b',' => {}
b'/' => {}
b':' => {}
b';' => {}
b'=' => {}
b'?' => {}
b'@' => {}
b'[' => {}
b']' => {}
b'-' => {}
b'.' => {}
b'_' => {}
b'~' => {}
b'%' => {}
b' ' => {}
b'\\' => {} b'\r' => {} b'\n' => {} other => {
if other.is_ascii_alphanumeric() {
continue;
}
return Err(RequestHeadParsingError::StatusLineContainsInvalidBytes.into());
}
}
}
if let Ok(res) = std::str::from_utf8(start_line_buf) {
return Ok(res);
}
error_log!("parse_status_line: fatal error std::str::from_utf8 with buf failed utf8 validation even tho it succeeded ascii validation. buf={start_line_buf:?}");
crate::util::unreachable()
}
fn parse_raw_query(raw_query: &str) -> TiiResult<Vec<(String, String)>> {
if raw_query.is_empty() {
return Ok(Vec::new());
}
let mut query = Vec::new();
let mut current_key = Vec::new();
let mut current_value = Vec::new();
let mut matching_value = false;
let mut matching_percent = 0;
for n in raw_query.as_bytes().iter().copied() {
if matching_percent != 0 {
match n {
b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => {
matching_percent -= 1;
}
_ => {
return Err(RequestHeadParsingError::InvalidQueryString(raw_query.to_string()).into());
}
}
}
match n {
b'=' => {
if matching_value {
return Err(RequestHeadParsingError::InvalidQueryString(raw_query.to_string()).into());
}
matching_value = true;
continue;
}
b'&' => {
if !matching_value {
return Err(RequestHeadParsingError::InvalidQueryString(raw_query.to_string()).into());
}
let key = urlencoding::decode(unwrap_ok(std::str::from_utf8(current_key.as_slice())))
.map_err(|_| RequestHeadParsingError::InvalidQueryString(raw_query.to_string()))?
.replace('+', " ");
let value = urlencoding::decode(unwrap_ok(std::str::from_utf8(current_value.as_slice())))
.map_err(|_| RequestHeadParsingError::InvalidQueryString(raw_query.to_string()))?
.replace('+', " ");
query.push((key, value));
matching_value = false;
current_key = Vec::new();
current_value = Vec::new();
continue;
}
b'%' => {
matching_percent = 2;
}
b'!' | b'$' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b'-' | b'.' | b'/' | b':' | b';'
| b'@' | b'_' | b'~' => {}
other => {
if !other.is_ascii_alphanumeric() {
return Err(RequestHeadParsingError::InvalidQueryString(raw_query.to_string()).into());
}
}
}
if matching_value {
current_value.push(n);
} else {
current_key.push(n);
}
}
if !matching_value || matching_percent != 0 {
return Err(RequestHeadParsingError::InvalidQueryString(raw_query.to_string()).into());
}
let key = urlencoding::decode(unwrap_ok(std::str::from_utf8(current_key.as_slice())))
.map_err(|_| RequestHeadParsingError::InvalidQueryString(raw_query.to_string()))?
.replace('+', " ");
let value = urlencoding::decode(unwrap_ok(std::str::from_utf8(current_value.as_slice())))
.map_err(|_| RequestHeadParsingError::InvalidQueryString(raw_query.to_string()))?
.replace('+', " ");
query.push((key, value));
Ok(query)
}
impl RequestHead {
pub(crate) fn new(
method: HttpMethod,
version: HttpVersion,
path: impl ToString,
query: Vec<(impl ToString, impl ToString)>,
headers: Vec<HttpHeader>,
) -> TiiResult<Self> {
let mut path = path.to_string();
if validate_raw_path(path.as_str()).is_err() {
let mut path_encoder = String::new();
for (idx, part) in path.split("/").enumerate() {
if idx == 0 {
if !part.is_empty() {
return Err(TiiError::RequestHeadParsing(RequestHeadParsingError::InvalidPath(path)));
}
continue;
}
path_encoder.push('/');
path_encoder.push_str(urlencoding::encode(part).as_ref());
}
validate_raw_path(&path_encoder)?;
path = path_encoder;
}
let query: Vec<(String, String)> = query
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.filter(|(k, v)| !k.is_empty() || !v.is_empty())
.collect();
let mut query_string = String::new();
for (idx, (key, value)) in query.iter().enumerate() {
if idx == 0 {
query_string.push('?');
} else {
query_string.push('&');
}
query_string.push_str(urlencoding::encode(key).as_ref());
if !value.is_empty() {
query_string.push('=');
query_string.push_str(urlencoding::encode(value).as_ref());
}
}
if version == HttpVersion::Http09 {
if method != HttpMethod::Get {
return Err(TiiError::RequestHeadParsing(
RequestHeadParsingError::MethodNotSupportedByHttpVersion(version, method),
));
}
if !headers.is_empty() {
return Err(TiiError::UserError(UserError::HeaderNotSupportedByHttpVersion(version)));
}
let status_line = format!("GET {}{}", path.as_str(), query_string.as_str());
return Ok(Self {
method: HttpMethod::Get,
version: HttpVersion::Http09,
status_line,
path,
query,
accept: vec![AcceptQualityMimeType::from_mime(
MimeType::TextHtml,
QValue::default(),
MimeCharset::Unspecified,
)],
accept_charset: vec![AcceptMimeCharset::new(MimeCharset::UsAscii, QValue::default())],
content_type: None,
headers: Default::default(),
});
}
let status_line = format!("{} {}{} {}", method, path.as_str(), query_string.as_str(), version);
let headers = Headers::from(headers);
let accept_hdr = headers.get(HttpHeaderName::Accept).unwrap_or("*/*");
let Some(accept) = AcceptQualityMimeType::parse(accept_hdr) else {
return Err(TiiError::UserError(UserError::IllegalAcceptHeaderValueSet(
accept_hdr.to_string(),
)));
};
let accept_charset_hdr = headers.get(HttpHeaderName::AcceptCharset).unwrap_or("");
let Some(accept_charset) = AcceptMimeCharset::parse(accept_charset_hdr) else {
return Err(TiiError::UserError(UserError::IllegalAcceptHeaderValueSet(
accept_hdr.to_string(),
)));
};
let content_type = if let Some(ctype_raw) = headers.get(HttpHeaderName::ContentType) {
let Some(ctype) = MimeTypeWithCharset::parse_from_content_type_header(ctype_raw) else {
return Err(TiiError::UserError(UserError::IllegalContentTypeHeaderValueSet(
ctype_raw.to_string(),
)));
};
Some(ctype)
} else {
None
};
Ok(Self {
method,
version,
status_line,
path,
query,
accept,
accept_charset,
content_type,
headers,
})
}
pub fn read(
id: u128,
stream: &dyn ConnectionStream,
max_head_buffer_size: usize,
) -> TiiResult<Self> {
let mut start_line_buf: Vec<u8> = Vec::with_capacity(256);
let count = stream.read_until(0xA, max_head_buffer_size, &mut start_line_buf)?;
if count == 0 {
error_log!("tii: RequestHead::new call to ConnectionStream::read_until returned 0 bytes, but this RequestHead::new is only called when the stream should have at least one byte buffered. Is the ConnectionStream impl buggy? Will return io::Error UnexpectedEof.");
return Err(TiiError::from_io_kind(ErrorKind::UnexpectedEof));
}
if count == max_head_buffer_size {
error_log!(
"tii: Request {} Client sent more than {} bytes for status line",
id,
max_head_buffer_size
);
return Err(RequestHeadParsingError::StatusLineTooLong(start_line_buf).into());
}
trace_log!(
"tii: Request {} received {} bytes of data until 0xA (\\n) byte for status line",
id,
count
);
let start_line_string = parse_status_line(&start_line_buf)?;
let status_line =
start_line_string.strip_suffix("\r\n").ok_or(RequestHeadParsingError::StatusLineNoCRLF)?;
trace_log!("tii: Request {} status line: {}", id, status_line);
let mut start_line = status_line.split(' ');
let method = HttpMethod::from(unwrap_some(start_line.next()));
let mut uri_iter =
start_line.next().ok_or(RequestHeadParsingError::StatusLineNoWhitespace)?.splitn(2, '?');
let version = start_line
.next()
.map(HttpVersion::try_from_net_str)
.unwrap_or(Ok(HttpVersion::Http09)) .map_err(|version| RequestHeadParsingError::HttpVersionNotSupported(version.to_string()))?;
if start_line.next().is_some() {
return Err(TiiError::from(RequestHeadParsingError::StatusLineTooManyWhitespaces));
}
let raw_path = unwrap_some(uri_iter.next());
validate_raw_path(raw_path)?;
let path = urlencoding::decode(raw_path)
.map_err(|_| {
TiiError::from(RequestHeadParsingError::InvalidPathUrlEncoding(raw_path.to_string()))
})?
.to_string();
let raw_query = uri_iter.next().unwrap_or("");
let query = parse_raw_query(raw_query)?;
let mut headers = Headers::new();
if version == HttpVersion::Http09 {
if method != HttpMethod::Get {
return Err(TiiError::from(RequestHeadParsingError::MethodNotSupportedByHttpVersion(
version, method,
)));
}
return Ok(Self {
method,
path,
query,
version,
headers,
content_type: None,
accept: vec![AcceptQualityMimeType::from_mime(
MimeType::TextHtml,
QValue::default(),
MimeCharset::Unspecified,
)], accept_charset: vec![AcceptMimeCharset::new(MimeCharset::UsAscii, QValue::default())],
status_line: status_line.to_string(),
});
}
loop {
let mut line_buf: Vec<u8> = Vec::with_capacity(256);
let count = stream.read_until(0xA, max_head_buffer_size, &mut line_buf)?;
if count == max_head_buffer_size {
error_log!(
"tii: Request {id} Client sent more than {max_head_buffer_size} bytes for header line"
);
return Err(RequestHeadParsingError::HeaderLineTooLong(line_buf).into());
}
trace_log!(
"tii: Request {id} received {count} bytes of data until 0xA (\\n) byte for header line"
);
let line = std::str::from_utf8(&line_buf)
.map_err(|_| RequestHeadParsingError::HeaderLineIsNotUsAscii)?;
if line == "\r\n" {
trace_log!("tii: Request {id} Client sent CRLF, end of header section");
break;
}
let line = line.strip_suffix("\r\n").ok_or(RequestHeadParsingError::HeaderLineNoCRLF)?;
#[cfg(feature = "log")]
{
if log::max_level() == log::Level::Trace {
if line.starts_with("Authorization:") {
trace_log!("tii: Request {id} next header line: Authorization: ***MASKED***");
} else {
trace_log!("tii: Request {id} next header line: {line}");
}
}
}
let mut line_parts = line.splitn(2, ": ");
let name = unwrap_some(line_parts.next()).trim();
if name.is_empty() {
return Err(TiiError::from(RequestHeadParsingError::HeaderNameEmpty));
}
let value = line_parts.next().ok_or(RequestHeadParsingError::HeaderValueMissing)?.trim();
if value.is_empty() {
return Err(TiiError::from(RequestHeadParsingError::HeaderValueEmpty));
}
headers.add(HttpHeaderName::from(name), value);
}
let accept_hdr = headers.get(HttpHeaderName::Accept).unwrap_or("*/*"); let accept = AcceptQualityMimeType::parse(accept_hdr);
if accept.is_none() {
warn_log!(
"tii: Request to '{}' has invalid Accept header '{}' will assume 'Accept: */*'",
path.as_str(),
accept_hdr
);
}
let accept = accept.unwrap_or_else(|| vec![AcceptQualityMimeType::default()]);
let content_type = headers.get(HttpHeaderName::ContentType).map(|ctype_raw| {
let ctype = MimeTypeWithCharset::parse_from_content_type_header(ctype_raw);
if ctype.is_none() {
warn_log!(
"tii: Request to '{}' has invalid Content-Type header '{}' will assume 'Content-Type: application/octet-stream'",
path.as_str(),
ctype_raw
);
}
ctype.unwrap_or(MimeTypeWithCharset::APPLICATION_OCTET_STREAM)
});
let accept_charset_hdr = headers.get(HttpHeaderName::AcceptCharset).unwrap_or("");
let accept_charset = AcceptMimeCharset::parse(accept_charset_hdr);
if accept_charset.is_none() {
warn_log!(
"tii: Request to '{}' has invalid Accept-Charset header '{}' will assume the header is not set",
path.as_str(),
accept_charset_hdr
);
}
let accept_charset = accept_charset.unwrap_or_default();
Ok(Self {
method,
path,
query,
version,
headers,
accept,
accept_charset,
content_type,
status_line: status_line.to_string(),
})
}
pub fn get_version(&self) -> HttpVersion {
self.version
}
pub fn get_raw_status_line(&self) -> &str {
self.status_line.as_str()
}
pub fn get_path(&self) -> &str {
self.path.as_str()
}
pub fn set_path(&mut self, path: impl ToString) {
self.path = path.to_string();
}
pub fn get_query(&self) -> &[(String, String)] {
self.query.as_slice()
}
pub fn query_mut(&mut self) -> &mut Vec<(String, String)> {
&mut self.query
}
pub fn set_query(&mut self, query: Vec<(String, String)>) {
self.query = query;
}
pub fn add_query_param(&mut self, key: impl ToString, value: impl ToString) {
self.query.push((key.to_string(), value.to_string()));
}
pub fn remove_query_params(&mut self, key: impl AsRef<str>) -> Vec<String> {
let key = key.as_ref();
let mut result = Vec::new();
for n in (0..self.query.len()).rev() {
let (k, _) = unwrap_some(self.query.get(n));
if k == key {
let (_, v) = self.query.remove(n);
result.push(v);
}
}
result.reverse();
result
}
pub fn set_query_param(&mut self, key: impl ToString, value: impl ToString) -> Vec<String> {
let key = key.to_string();
let value = value.to_string();
let mut result = Vec::new();
let mut added = false;
for n in (0..self.query.len()).rev() {
let (k, v) = unwrap_some(self.query.get(n));
if k == key.as_str() {
if !added && v == value.as_str() {
added = true;
continue;
}
let (_, v) = self.query.remove(n);
result.push(v);
}
}
if !added {
self.query.push((key, value));
}
result.reverse();
result
}
pub fn get_query_param(&self, key: impl AsRef<str>) -> Option<&str> {
let key = key.as_ref();
for (k, v) in &self.query {
if k == key {
return Some(v.as_str());
}
}
None
}
pub fn get_query_params(&self, key: impl AsRef<str>) -> Vec<&str> {
let mut result = Vec::new();
let key = key.as_ref();
for (k, v) in &self.query {
if k == key {
result.push(v.as_str());
}
}
result
}
pub fn get_method(&self) -> &HttpMethod {
&self.method
}
pub fn set_method(&mut self, method: HttpMethod) {
self.method = method;
}
pub fn get_cookies(&self) -> Vec<Cookie> {
self
.headers
.get(HttpHeaderName::Cookie)
.map(|cookies| {
cookies
.split(';')
.filter_map(|cookie| {
let (k, v) = cookie.split_once('=')?;
Some(Cookie::new(k.trim(), v.trim()))
})
.collect()
})
.unwrap_or_default()
}
pub fn get_cookie(&self, name: impl AsRef<str>) -> Option<Cookie> {
self.get_cookies().into_iter().find(|cookie| cookie.name == name.as_ref())
}
pub fn set_accept(&mut self, types: Vec<AcceptQualityMimeType>) {
let hdr_value = AcceptQualityMimeType::elements_to_header_value(&types);
self.headers.set(HttpHeaderName::Accept, hdr_value);
self.accept = types;
}
pub fn get_content_type(&self) -> Option<&MimeTypeWithCharset> {
self.content_type.as_ref()
}
pub fn set_content_type(&mut self, content_type: impl Into<MimeTypeWithCharset>) {
let content_type = content_type.into();
self.headers.set(HttpHeaderName::ContentType, content_type.to_string());
self.content_type = Some(content_type);
}
pub fn remove_content_type(&mut self) -> Option<MimeTypeWithCharset> {
self.headers.remove(HttpHeaderName::ContentType);
self.content_type.take()
}
pub fn get_accept(&self) -> &[AcceptQualityMimeType] {
self.accept.as_slice()
}
pub fn get_accept_charset(&self) -> &[AcceptMimeCharset] {
self.accept_charset.as_slice()
}
pub fn iter_headers(&self) -> impl Iterator<Item = &HttpHeader> {
self.headers.iter()
}
pub fn get_header(&self, name: impl AsRef<str>) -> Option<&str> {
self.headers.get(name)
}
pub fn accepts_gzip(&self) -> bool {
let Some(accept_enc) = self.get_header(HttpHeaderName::AcceptEncoding) else {
return false;
};
accept_enc.contains("gzip")
}
pub fn get_headers(&self, name: impl AsRef<str>) -> Vec<&str> {
self.headers.get_all(name)
}
pub fn remove_headers(&mut self, hdr: impl AsRef<str>) -> TiiResult<()> {
match &hdr.as_ref().into() {
HttpHeaderName::Accept => {
self.accept = vec![AcceptQualityMimeType::default()];
self.headers.set(hdr, "*/*");
Ok(())
}
HttpHeaderName::ContentType => {
self.headers.remove(hdr);
self.content_type = None;
Ok(())
}
HttpHeaderName::TransferEncoding => {
UserError::ImmutableRequestHeaderRemoved(HttpHeaderName::TransferEncoding).into()
}
HttpHeaderName::ContentLength => {
UserError::ImmutableRequestHeaderRemoved(HttpHeaderName::ContentLength).into()
}
_ => {
self.headers.remove(hdr);
Ok(())
}
}
}
pub fn set_header(&mut self, hdr: impl AsRef<str>, value: impl ToString) -> TiiResult<()> {
let hdr_value = value.to_string();
match &hdr.as_ref().into() {
HttpHeaderName::Accept => {
if let Some(accept) = AcceptQualityMimeType::parse(&hdr_value) {
self.accept = accept;
self.headers.set(hdr, value);
return Ok(());
}
UserError::IllegalAcceptHeaderValueSet(hdr_value.to_string()).into()
}
HttpHeaderName::ContentType => {
let mime = MimeTypeWithCharset::parse_from_content_type_header(&hdr_value)
.ok_or_else(|| UserError::IllegalContentTypeHeaderValueSet(hdr_value.to_string()))?;
self.headers.set(HttpHeaderName::ContentType, hdr_value);
self.content_type = Some(mime);
Ok(())
}
HttpHeaderName::TransferEncoding => UserError::ImmutableRequestHeaderModified(
HttpHeaderName::TransferEncoding,
hdr_value.to_string(),
)
.into(),
HttpHeaderName::ContentLength => UserError::ImmutableRequestHeaderModified(
HttpHeaderName::ContentLength,
hdr_value.to_string(),
)
.into(),
_ => {
self.headers.set(hdr, value);
Ok(())
}
}
}
pub fn add_header(&mut self, hdr: impl AsRef<str>, value: impl ToString) -> TiiResult<()> {
let hdr_value = value.to_string();
match &hdr.as_ref().into() {
HttpHeaderName::Accept => {
if let Some(accept) = AcceptQualityMimeType::parse(&hdr_value) {
if let Some(old_value) = self.headers.try_set(hdr, &hdr_value) {
return UserError::MultipleAcceptHeaderValuesSet(old_value.to_string(), hdr_value)
.into();
}
self.accept = accept;
return Ok(());
}
UserError::IllegalAcceptHeaderValueSet(hdr_value.to_string()).into()
}
HttpHeaderName::ContentType => {
let mime = MimeTypeWithCharset::parse_from_content_type_header(&hdr_value)
.ok_or_else(|| UserError::IllegalContentTypeHeaderValueSet(hdr_value.to_string()))?;
if let Some(old_value) = self.headers.try_set(hdr, &hdr_value) {
return UserError::MultipleContentTypeHeaderValuesSet(old_value.to_string(), hdr_value)
.into();
}
self.content_type = Some(mime);
Ok(())
}
HttpHeaderName::TransferEncoding => UserError::ImmutableRequestHeaderModified(
HttpHeaderName::TransferEncoding,
hdr_value.to_string(),
)
.into(),
HttpHeaderName::ContentLength => UserError::ImmutableRequestHeaderModified(
HttpHeaderName::ContentLength,
hdr_value.to_string(),
)
.into(),
_ => {
self.headers.add(hdr, value);
Ok(())
}
}
}
}