use crate::auth::IClaims;
use crate::error::Result;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::SystemTime;
use tokio::io::{AsyncRead, AsyncSeek};
pub struct HttpStatus;
impl HttpStatus {
pub const OK: u16 = 200;
pub const CREATED: u16 = 201;
pub const NO_CONTENT: u16 = 204;
pub const PARTIAL_CONTENT: u16 = 206;
pub const NOT_MODIFIED: u16 = 304;
pub const BAD_REQUEST: u16 = 400;
pub const UNAUTHORIZED: u16 = 401;
pub const FORBIDDEN: u16 = 403;
pub const NOT_FOUND: u16 = 404;
pub const RANGE_NOT_SATISFIABLE: u16 = 416;
pub const PAYLOAD_TOO_LARGE: u16 = 413;
pub const UNSUPPORTED_MEDIA_TYPE: u16 = 415;
pub const INTERNAL_SERVER_ERROR: u16 = 500;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteRange {
pub start: u64,
pub end: u64,
}
impl ByteRange {
pub fn len(&self) -> u64 {
self.end.saturating_sub(self.start) + 1
}
pub fn is_empty(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Disposition {
#[default]
Inline,
Attachment,
}
pub const STREAM_BUF_SIZE: usize = 64 * 1024;
pub trait SeekableReader: AsyncRead + AsyncSeek + Send + Unpin {}
impl<T: AsyncRead + AsyncSeek + Send + Unpin> SeekableReader for T {}
pub enum FileSource {
Path(PathBuf),
SeekableStream {
reader: Box<dyn SeekableReader>,
len: u64,
},
Stream(Box<dyn AsyncRead + Send + Unpin>),
}
impl std::fmt::Debug for FileSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileSource::Path(path) => f.debug_tuple("Path").field(path).finish(),
FileSource::SeekableStream { len, .. } => f
.debug_struct("SeekableStream")
.field("len", len)
.finish_non_exhaustive(),
FileSource::Stream(_) => f.debug_tuple("Stream").finish(),
}
}
}
impl std::fmt::Debug for FileBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FileBody")
.field("source", &self.source)
.field("content_type", &self.content_type)
.field("file_name", &self.file_name)
.field("disposition", &self.disposition)
.field("enable_range_processing", &self.enable_range_processing)
.field("last_modified", &self.last_modified)
.field("entity_tag", &self.entity_tag)
.finish()
}
}
pub struct FileBody {
pub source: FileSource,
pub content_type: Option<String>,
pub file_name: Option<String>,
pub disposition: Disposition,
pub enable_range_processing: bool,
pub last_modified: Option<SystemTime>,
pub entity_tag: Option<String>,
}
impl FileBody {
pub fn path(path: impl Into<PathBuf>) -> Self {
Self {
source: FileSource::Path(path.into()),
content_type: None,
file_name: None,
disposition: Disposition::Inline,
enable_range_processing: true,
last_modified: None,
entity_tag: None,
}
}
pub fn stream(reader: impl AsyncRead + Send + Unpin + 'static) -> Self {
Self {
source: FileSource::Stream(Box::new(reader)),
content_type: None,
file_name: None,
disposition: Disposition::Inline,
enable_range_processing: false,
last_modified: None,
entity_tag: None,
}
}
pub fn seekable_stream(
reader: impl AsyncRead + AsyncSeek + Send + Unpin + 'static,
len: u64,
) -> Self {
Self {
source: FileSource::SeekableStream {
reader: Box::new(reader),
len,
},
content_type: None,
file_name: None,
disposition: Disposition::Inline,
enable_range_processing: true,
last_modified: None,
entity_tag: None,
}
}
pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
self.content_type = Some(content_type.into());
self
}
pub fn download_name(mut self, file_name: impl Into<String>) -> Self {
self.file_name = Some(file_name.into());
self.disposition = Disposition::Attachment;
self
}
pub fn inline_name(mut self, file_name: impl Into<String>) -> Self {
self.file_name = Some(file_name.into());
self.disposition = Disposition::Inline;
self
}
pub fn enable_range_processing(mut self, enabled: bool) -> Self {
self.enable_range_processing = enabled;
self
}
pub fn last_modified(mut self, last_modified: SystemTime) -> Self {
self.last_modified = Some(last_modified);
self
}
pub fn entity_tag(mut self, entity_tag: impl Into<String>) -> Self {
self.entity_tag = Some(entity_tag.into());
self
}
pub fn is_range_capable(&self) -> bool {
self.enable_range_processing
&& matches!(
self.source,
FileSource::Path(_) | FileSource::SeekableStream { .. }
)
}
pub fn known_len(&self) -> Option<u64> {
match &self.source {
FileSource::Path(_) | FileSource::Stream(_) => None,
FileSource::SeekableStream { len, .. } => Some(*len),
}
}
}
pub enum ResponseBody {
Bytes(Vec<u8>),
File(FileBody),
}
impl std::fmt::Debug for ResponseBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResponseBody::Bytes(bytes) => f.debug_tuple("Bytes").field(&bytes.len()).finish(),
ResponseBody::File(file) => f.debug_tuple("File").field(file).finish(),
}
}
}
impl ResponseBody {
pub fn len(&self) -> Option<u64> {
match self {
ResponseBody::Bytes(bytes) => Some(bytes.len() as u64),
ResponseBody::File(file) => file.known_len(),
}
}
pub fn is_empty(&self) -> bool {
matches!(self, ResponseBody::Bytes(bytes) if bytes.is_empty())
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
ResponseBody::Bytes(bytes) => Some(bytes),
ResponseBody::File(_) => None,
}
}
pub fn as_file(&self) -> Option<&FileBody> {
match self {
ResponseBody::File(file) => Some(file),
ResponseBody::Bytes(_) => None,
}
}
}
impl From<Vec<u8>> for ResponseBody {
fn from(bytes: Vec<u8>) -> Self {
ResponseBody::Bytes(bytes)
}
}
impl From<String> for ResponseBody {
fn from(text: String) -> Self {
ResponseBody::Bytes(text.into_bytes())
}
}
impl From<&str> for ResponseBody {
fn from(text: &str) -> Self {
ResponseBody::Bytes(text.as_bytes().to_vec())
}
}
impl From<FileBody> for ResponseBody {
fn from(file: FileBody) -> Self {
ResponseBody::File(file)
}
}
pub trait IClaimsExt {
fn set_claims(&mut self, claims: Box<dyn IClaims>);
fn claims(&self) -> Option<&dyn IClaims>;
}
pub trait IHttpContext: IClaimsExt + Send {
fn request(&self) -> &dyn IHttpRequest;
fn request_mut(&mut self) -> &mut dyn IHttpRequest;
fn response(&self) -> &dyn IHttpResponse;
fn response_mut(&mut self) -> &mut dyn IHttpResponse;
}
#[async_trait::async_trait]
pub trait IHttpRequest: Send {
fn method(&self) -> &str;
fn path(&self) -> &str;
fn header(&self, name: &str) -> Option<&str>;
fn query(&self) -> &HashMap<String, String>;
fn route_params(&self) -> &HashMap<String, String>;
fn route_params_mut(&mut self) -> &mut HashMap<String, String>;
fn route_pattern(&self) -> Option<&str>;
fn route_pattern_mut(&mut self) -> &mut Option<String>;
async fn body_bytes(&mut self) -> Result<Vec<u8>>;
async fn body_text(&mut self) -> Result<String> {
let bytes = self.body_bytes().await?;
String::from_utf8(bytes).map_err(|e| crate::error::Error::Http(e.to_string()))
}
fn content_type(&self) -> Option<&str> {
self.header("content-type")
.map(|value| value.split(';').next().unwrap_or("").trim())
.filter(|value| !value.is_empty())
}
fn is_multipart(&self) -> bool {
self.content_type()
.is_some_and(|ct| ct.eq_ignore_ascii_case("multipart/form-data"))
}
async fn multipart(&mut self) -> Result<Arc<crate::form::MultipartForm>> {
Err(crate::error::Error::UnsupportedMediaType(
"multipart/form-data is not supported by this request implementation".to_string(),
))
}
}
#[async_trait::async_trait]
pub trait IHttpResponse: Send {
fn status(&self) -> u16;
fn set_status(&mut self, code: u16);
fn set_header(&mut self, key: &str, value: &str);
fn remove_header(&mut self, _key: &str) {}
fn has_body(&self) -> bool {
false
}
async fn write_bytes(&mut self, data: Vec<u8>) -> Result<()>;
async fn write_body(&mut self, body: ResponseBody) -> Result<()> {
match body {
ResponseBody::Bytes(bytes) => self.write_bytes(bytes).await,
ResponseBody::File(_) => Err(crate::error::Error::Internal(
"file responses require the rust-webx HTTP host".to_string(),
)),
}
}
async fn write_text(&mut self, text: &str) -> Result<()> {
self.write_bytes(text.as_bytes().to_vec()).await
}
fn body_bytes(&self) -> Vec<u8> {
Vec::new()
}
fn header(&self, _key: &str) -> Option<&str> {
None
}
}
pub fn content_disposition_value(disposition: Disposition, file_name: &str) -> String {
let kind = match disposition {
Disposition::Inline => "inline",
Disposition::Attachment => "attachment",
};
let ascii: String = file_name
.chars()
.map(|ch| {
if ch.is_ascii() && ch != '"' && ch != '\\' && !ch.is_control() {
ch
} else {
'_'
}
})
.collect();
format!(
"{kind}; filename=\"{ascii}\"; filename*=UTF-8''{}",
percent_encode_ext_value(file_name)
)
}
fn percent_encode_ext_value(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for byte in value.as_bytes() {
let ch = *byte as char;
let is_attr_char = ch.is_ascii_alphanumeric()
|| matches!(
ch,
'!' | '#' | '$' | '&' | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~'
);
if is_attr_char {
out.push(ch);
} else {
out.push_str(&format!("%{byte:02X}"));
}
}
out
}
pub async fn write_json_response<T: serde::Serialize + Send>(
resp: &mut dyn IHttpResponse,
value: &T,
) -> Result<()> {
let json = serde_json::to_vec(value)?;
resp.set_header("content-type", "application/json");
resp.write_bytes(json).await
}
pub async fn read_json_body<T: serde::de::DeserializeOwned>(
req: &mut dyn IHttpRequest,
) -> Result<T> {
let bytes = req.body_bytes().await?;
serde_json::from_slice(&bytes).map_err(crate::error::Error::Serialization)
}
pub type Json<T> = T;
#[async_trait::async_trait]
pub trait FromHttpContext: Sized + Send + 'static {
async fn from_http_context(ctx: &dyn IHttpContext) -> Result<Self>;
}