use crate::request::Request;
use crate::websocket::Websocket;
use crate::Protocol::HTTP2;
use crate::Status;
use crate::{ContentType, Encoding, Handler, HttpError, Method, Protocol, Upgrade};
use chrono::{DateTime, Duration as dur, Utc};
use hpack::Encoder;
use json::{object, JsonValue};
use log::{error, info};
use std::io::Error;
use std::path::{Path, PathBuf};
use std::{fs, io, thread};
const FLAG_END_STREAM: u8 = 0x01;
const FLAG_END_HEADERS: u8 = 0x04;
const DEFAULT_CACHE_MAX_AGE: u64 = 86400;
#[derive(Clone, Debug)]
pub struct Response {
pub request: Request,
pub status: Status,
pub headers: JsonValue,
pub cookies: JsonValue,
pub body: Vec<u8>,
pub stream_id: u32,
pub key: String,
pub version: String,
pub factory: fn(out: Websocket) -> Box<dyn Handler>,
pub allow_origins: Vec<&'static str>,
pub allow_methods: Vec<&'static str>,
pub allow_headers: Vec<&'static str>,
pub content_type: ContentType,
}
fn sanitize_header_value(value: &str) -> String {
value.chars().filter(|c| *c != '\r' && *c != '\n').collect()
}
impl Response {
pub fn new(request: &Request, factory: fn(out: Websocket) -> Box<dyn Handler>) -> Self {
Self {
request: request.clone(),
status: Status::default(),
headers: object! {},
cookies: object! {},
body: vec![],
stream_id: 0,
key: String::new(),
version: String::new(),
factory,
allow_origins: vec![],
allow_methods: vec![],
allow_headers: vec![],
content_type: ContentType::Other(String::new()),
}
}
pub fn handle(mut self) -> io::Result<()> {
match self.request.upgrade {
Upgrade::Websocket => {
self.handle_protocol_ws()?;
return Ok(());
}
Upgrade::Http | Upgrade::Other(_) => {}
Upgrade::H2c => {
let _ = self.handle_protocol_h2c();
}
}
match self.request.protocol {
Protocol::HTTP1_0 => self.handle_protocol_http0()?,
Protocol::HTTP1_1 => self.handle_protocol_http1()?,
Protocol::HTTP2 => self.handle_protocol_http2()?,
Protocol::HTTP3 | Protocol::Other(_) => {}
}
Ok(())
}
fn handle_protocol_http0(&mut self) -> io::Result<()> {
let websocket = Websocket::http(self.request.clone(), self.clone());
let mut factory = (self.factory)(websocket);
match self.request.method {
Method::OPTIONS => {
factory.on_options(self);
match self.on_options() {
Ok(()) => match self.status(200).send() {
Ok(()) => {}
Err(e) => {
return Err(Error::other(format!(
"1004: {} {} {}",
self.request.uri.path, e.code, e.body
)))
}
},
Err(e) => {
return match self.status(e.code).txt(e.body.as_str()).send() {
Ok(()) => Ok(()),
Err(e2) => Err(Error::other(format!(
"1005: {} {} {}",
self.request.uri.path, e2.code, e2.body
))),
}
}
}
}
Method::GET => {
if let Ok(e) = self.read_resource() {
if self.request.header.has_key("range") {
return match self.status(206).range(&e).send() {
Ok(()) => Ok(()),
Err(e) => Err(Error::other(format!(
"1003: {} {} {}",
self.request.uri.path, e.code, e.body
))),
};
}
return match self.status(200).file(&e).send() {
Ok(_) => Ok(()),
Err(e) => Err(Error::other(format!(
"1003: {} {} {}",
self.request.uri.path, e.code, e.body
))),
};
}
factory.on_request(self.request.clone(), self);
factory.on_response(self);
match self.send() {
Ok(()) => {}
Err(e) => {
return Err(Error::other(format!(
"1002: {} {} {}",
self.request.uri.path, e.code, e.body
)))
}
}
}
_ => {
factory.on_request(self.request.clone(), self);
factory.on_response(self);
match self.send() {
Ok(_) => {}
Err(e) => return Err(Error::other(format!("1001: {} {}", e.code, e.body))),
}
}
}
Ok(())
}
fn handle_protocol_http1(&mut self) -> io::Result<()> {
self.handle_protocol_http0()
}
fn handle_protocol_http2(&mut self) -> io::Result<()> {
let websocket = Websocket::http(self.request.clone(), self.clone());
let mut factory = (self.factory)(websocket);
match self.request.method {
Method::OPTIONS => {
factory.on_options(self);
match self.send() {
Ok(_) => {}
Err(e) => return Err(Error::other(format!("2001: {} {}", e.code, e.body))),
};
}
Method::GET => {
if let Ok(e) = self.read_resource() {
return match self.status(200).file(&e).send() {
Ok(_) => Ok(()),
Err(e) => Err(Error::other(e.body)),
};
}
factory.on_request(self.request.clone(), self);
factory.on_response(self);
match self.send() {
Ok(()) => {}
Err(e) => return Err(Error::other(format!("2002: {} {}", e.code, e.body))),
}
}
_ => {
factory.on_request(self.request.clone(), self);
factory.on_response(self);
match self.send() {
Ok(_) => {}
Err(e) => return Err(Error::other(format!("2003: {} {}", e.code, e.body))),
}
}
}
Ok(())
}
fn handle_protocol_ws(&mut self) -> io::Result<()> {
let mut websocket = Websocket::new(self.request.clone(), self.clone());
let _ = websocket.handle();
Ok(())
}
fn handle_protocol_h2c(&mut self) -> Result<(), HttpError> {
self.header("Upgrade", "h2c");
self.header("Connection", "Upgrade");
match self.status(101).send() {
Ok(()) => self.headers.clear(),
Err(e) => return Err(e),
};
self.request.protocol = HTTP2;
self.request
.scheme
.lock()
.map_err(|e| HttpError::new(500, &format!("lock: {}", e)))?
.http2_send_server_settings()?;
let mut data = vec![];
self.request
.scheme
.lock()
.map_err(|e| HttpError::new(500, &format!("lock: {}", e)))?
.read(&mut data)?;
let t = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
if let Some(pos) = data.windows(t.len()).position(|window| window == t) {
let _ = data.drain(..pos).collect::<Vec<u8>>();
data.drain(..t.len());
} else {
return Err(HttpError::new(400, "请求行错误"));
}
let (_payload, _frame_type, flags, _stream_id) = self
.request
.scheme
.lock()
.map_err(|e| HttpError::new(500, &format!("lock: {}", e)))?
.http2_packet(&mut data)?;
if flags & 0x01 == 0 {
self.request
.scheme
.lock()
.map_err(|e| HttpError::new(500, &format!("lock: {}", e)))?
.http2_settings_ack()?;
}
let (_payload, _frame_type, _flags, _stream_id) = self
.request
.scheme
.lock()
.map_err(|e| HttpError::new(500, &format!("lock: {}", e)))?
.http2_packet(&mut data)?;
let (payload, _frame_type, _flags, _stream_id) = self
.request
.scheme
.lock()
.map_err(|e| HttpError::new(500, &format!("lock: {}", e)))?
.http2_packet(&mut data)?;
if payload.len() == 4 {
let raw = u32::from_be_bytes(
<[u8; 4]>::try_from(&payload[..4])
.map_err(|_| HttpError::new(400, "Invalid WindowUpdate frame data"))?,
);
let increment = raw & 0x7FFF_FFFF; if self.request.config.debug {
info!("WindowUpdate: increment = {} {:?}", increment, payload);
}
} else {
return Err(HttpError::new(
400,
format!("Invalid WindowUpdate frame length: {}", payload.len()).as_str(),
));
}
Ok(())
}
fn read_resource(&mut self) -> io::Result<PathBuf> {
let sanitized_path = self.request.uri.path.trim_start_matches('/');
if sanitized_path.contains("..") {
return Err(Error::other(
"Invalid path: directory traversal not allowed",
));
}
if self.request.uri.path != "/" {
let base_dir = self
.request
.config
.root_path
.join(self.request.config.public.clone());
let file = base_dir.join(sanitized_path);
if let (Ok(canonical_base), Ok(canonical_file)) =
(base_dir.canonicalize(), file.canonicalize())
{
if canonical_file.starts_with(&canonical_base) && file.is_file() {
return Ok(file);
}
}
}
if self.request.uri.path == "/" {
let file = self
.request
.config
.root_path
.join("webpage")
.join(self.request.config.webpage.clone())
.join("index.html");
if file.is_file() {
return Ok(file);
}
} else {
let base_dir = self
.request
.config
.root_path
.join("webpage")
.join(self.request.config.webpage.clone());
let file = base_dir.join(sanitized_path);
if let (Ok(canonical_base), Ok(canonical_file)) =
(base_dir.canonicalize(), file.canonicalize())
{
if canonical_file.starts_with(&canonical_base) && file.is_file() {
return Ok(file);
}
}
}
Err(Error::other("Not a file"))
}
pub fn status(&mut self, code: u16) -> &mut Self {
self.status.set_code(code);
self
}
pub fn location(&mut self, uri: &str) -> &mut Self {
self.header("Location", uri);
self
}
pub fn set_host(&mut self, host: &str) -> &mut Self {
self.header("Host", host);
self
}
fn sanitize_header_value(value: &str) -> String {
sanitize_header_value(value)
}
pub fn header(&mut self, key: &str, value: &str) -> &mut Self {
self.headers[key] = Self::sanitize_header_value(value).into();
self
}
pub fn cookie(&mut self, key: &str, value: &str) -> &mut Self {
self.cookies[key] = value.into();
self
}
pub fn cookie_with_options(&mut self, options: CookieOptions) -> &mut Self {
self.cookies[options.name.as_str()] = options.to_header_value().into();
self
}
fn get_date(&self, s: i64) -> String {
let utc: DateTime<Utc> = Utc::now();
let future = utc + dur::seconds(s); future.format("%a, %d %b %Y %H:%M:%S GMT").to_string()
}
pub fn html(&mut self, value: &str) -> &mut Self {
if self.request.config.charset.is_empty() {
self.header(
"Content-Type",
format!("{};", Extension::form("html").as_str()).as_str(),
);
} else {
self.header(
"Content-Type",
format!(
"{}; charset={}",
Extension::form("html").as_str(),
self.request.config.charset
)
.as_str(),
);
}
self.content_type = ContentType::Html;
self.body = value.as_bytes().to_vec();
self
}
pub fn txt(&mut self, value: &str) -> &mut Self {
if self.request.config.charset.is_empty() {
self.header(
"Content-Type",
Extension::form("txt").as_str().to_string().as_str(),
);
} else {
self.header(
"Content-Type",
format!(
"{}; charset={}",
Extension::form("txt").as_str(),
self.request.config.charset
)
.as_str(),
);
}
self.content_type = ContentType::Text;
self.body = value.as_bytes().to_vec();
self
}
pub fn json(&mut self, value: JsonValue) -> &mut Self {
if self.request.config.charset.is_empty() {
self.header(
"Content-Type",
Extension::form("json").as_str().to_string().as_str(),
);
} else {
self.header(
"Content-Type",
format!(
"{}; charset={}",
Extension::form("json").as_str(),
self.request.config.charset
)
.as_str(),
);
}
self.content_type = ContentType::Json;
self.body = value.to_string().into_bytes();
self
}
pub fn download(&mut self, filename: &Path) -> &mut Self {
let Ok(file) = fs::read(filename) else {
self.status(404);
return self;
};
let extension = filename
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if self.request.config.charset.is_empty() {
self.header(
"Content-Type",
Extension::form(extension.as_str())
.as_str()
.to_string()
.as_str(),
);
} else {
self.header(
"Content-Type",
format!(
"{}; charset={}",
Extension::form(extension.as_str()).as_str(),
self.request.config.charset
)
.as_str(),
);
}
let encoded_file_name = br_crypto::encoding::urlencoding_encode(
filename
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("download"),
);
self.header(
"Content-Disposition",
format!(r"attachment; filename={encoded_file_name}").as_str(),
);
self.header("Cache-Control", "no-cache");
self.header("ETag", br_crypto::md5::encrypt_hex(&file).as_str());
self.body = file;
self
}
pub fn range(&mut self, filename: &Path) -> &mut Self {
let Ok(file) = fs::read(filename) else {
self.status(404);
return self;
};
let range = self.request.header["range"].to_string();
let range = range.trim_start_matches("bytes=");
let parts: Vec<&str> = range.split('-').collect();
let range_start = match parts.first().and_then(|s| s.parse::<usize>().ok()) {
Some(v) => v,
None => {
self.status(416);
self.header("Content-Range", format!("bytes */{}", file.len()).as_str());
return self;
}
};
let range_end = if parts.len() > 1 && !parts[1].is_empty() {
match parts[1].parse::<usize>() {
Ok(v) => v,
Err(_) => {
self.status(416);
self.header("Content-Range", format!("bytes */{}", file.len()).as_str());
return self;
}
}
} else {
file.len().saturating_sub(1)
};
if file.len() < range_end {
self.status(416);
self.header("Content-Range", format!("bytes */{}", file.len()).as_str());
return self;
}
let extension = filename
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if self.request.config.charset.is_empty() {
self.header(
"Content-Type",
Extension::form(extension.as_str())
.as_str()
.to_string()
.as_str(),
);
} else {
self.header(
"Content-Type",
format!(
"{}; charset={}",
Extension::form(extension.as_str()).as_str(),
self.request.config.charset
)
.as_str(),
);
}
self.header("Accept-Ranges", "bytes");
self.header(
"content-length",
(range_end - range_start + 1).to_string().as_str(),
);
self.header(
"Content-Range",
format!("bytes {}-{}/{}", range_start, range_end, file.len()).as_str(),
);
self.body = file[range_start..=range_end].to_vec();
self
}
pub fn file(&mut self, filename: &Path) -> &mut Self {
let Ok(file) = fs::read(filename) else {
self.status(404);
return self;
};
self.header("Access-Control-Expose-Headers", "Content-Disposition");
let extension = match filename.extension() {
None => String::new(),
Some(e) => e.to_str().unwrap_or("").to_lowercase(),
};
if self.request.config.charset.is_empty() {
self.header(
"Content-Type",
Extension::form(extension.as_str())
.as_str()
.to_string()
.as_str(),
);
} else {
self.header(
"Content-Type",
format!(
"{}; charset={}",
Extension::form(extension.as_str()).as_str(),
self.request.config.charset
)
.as_str(),
);
}
self.header(
"Content-Disposition",
format!(
r#"inline; filename="{}""#,
filename
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("download")
)
.as_str(),
);
self.header("Accept-Ranges", "bytes");
self.header(
"Cache-Control",
format!("public, max-age={}", DEFAULT_CACHE_MAX_AGE).as_str(),
);
self.header("Expires", &self.get_date(DEFAULT_CACHE_MAX_AGE as i64));
self.body = file;
self
}
pub fn send(&mut self) -> Result<(), HttpError> {
match &self.request.protocol {
Protocol::HTTP1_0 | Protocol::HTTP1_1 => Ok(self.send_http1()?),
Protocol::HTTP2 => Ok(self.send_http2()?),
Protocol::HTTP3 => {
error!("Other1:{:?} {:?}", thread::current().id(), self.request);
Err(HttpError::new(500, "暂未实现HTTP3"))
}
Protocol::Other(e) => {
error!("Other:{:?} {:?}", thread::current().id(), self.request);
Err(HttpError::new(
500,
format!(
"暂未实现Other: {} {} {:?}",
e,
self.request.uri.path,
thread::current().id()
)
.as_str(),
))
}
}
}
fn send_http1(&mut self) -> Result<(), HttpError> {
let mut header = vec![];
header.push(format!(
"{} {} {}",
self.request.protocol.str(),
self.status.code,
self.status.reason
));
self.header("Date", &self.get_date(0));
if !self.headers.has_key("X-Content-Type-Options") {
self.header("X-Content-Type-Options", "nosniff");
}
if !self.headers.has_key("X-Frame-Options") {
self.header("X-Frame-Options", "SAMEORIGIN");
}
if !self.headers.has_key("X-XSS-Protection") {
self.header("X-XSS-Protection", "1; mode=block");
}
match self.request.method {
Method::HEAD => {
self.header("Content-Length", self.body.len().to_string().as_str());
self.body = vec![];
}
Method::OPTIONS => {
self.body = vec![];
}
_ => {
if self.status.code == 101 {
} else {
match self.request.accept_encoding {
Encoding::Gzip => {
self.header(
"Content-Encoding",
self.request.accept_encoding.clone().str(),
);
if let Ok(e) = self.request.accept_encoding.compress(&self.body) {
self.body = e
};
self.header("Content-Length", self.body.len().to_string().as_str());
}
_ => {
self.header("Content-Length", self.body.len().to_string().as_str());
}
}
}
}
};
for (key, value) in self.headers.entries() {
header.push(format!("{key}: {value}"));
}
for (key, value) in self.cookies.entries() {
header.push(format!(
"Set-Cookie: {key}={value}; Path=/; HttpOnly; SameSite=Lax"
));
}
if self.request.config.debug {
info!("\r\n=================响应信息 {:?}=================\r\n{}\r\n===========================================",thread::current().id(),header.join("\r\n"));
match self.request.accept_encoding {
Encoding::Gzip => {}
_ => {
match self.content_type {
ContentType::Text
| ContentType::Html
| ContentType::Json
| ContentType::FormUrlencoded => {
info!("\r\n=================响应体 {:?}=================\r\n{}\r\n===========================================",thread::current().id(),String::from_utf8_lossy(self.body.as_slice()));
}
_ => {}
};
}
}
}
let mut headers = format!("{}\r\n\r\n", header.join("\r\n")).into_bytes();
headers.extend(self.body.clone());
self.request
.scheme
.lock()
.map_err(|e| HttpError::new(500, &format!("lock: {}", e)))?
.write_all(headers.as_slice())?;
Ok(())
}
fn send_http2(&mut self) -> Result<(), HttpError> {
let max_frame_size: usize = 16_384;
self.stream_id += 1;
let header_frames = self.http2_header(self.stream_id, max_frame_size);
let data_frames = self.http2_body(self.stream_id, true, max_frame_size);
let mut writer = self
.request
.scheme
.lock()
.map_err(|e| HttpError::new(500, &format!("lock: {}", e)))?;
for f in header_frames.clone() {
writer.write_all(&f)?;
}
for f in data_frames.clone() {
writer.write_all(&f)?;
}
Ok(())
}
fn http2_header(&mut self, stream_id: u32, max_frame_size: usize) -> Vec<Vec<u8>> {
let mut headers: Vec<(Vec<u8>, Vec<u8>)> = vec![
(
b":status".to_vec(),
self.status.code.to_string().into_bytes(),
),
(b"date".to_vec(), self.get_date(0).into_bytes()),
];
match self.request.method {
Method::HEAD => {
self.header("content-length", self.body.len().to_string().as_str());
self.body.clear(); }
Method::OPTIONS => {
self.body.clear();
self.header("content-length", "0");
}
_ => {
self.header("content-length", self.body.len().to_string().as_str());
}
}
headers.extend(self.headers.entries().map(|(k, v)| {
(
k.to_string().to_lowercase().into_bytes(),
v.to_string().into_bytes(),
)
}));
headers.extend(self.cookies.entries().map(|(k, v)| {
(
b"set-cookie".to_vec(),
format!("{k}={v}; Path=/; HttpOnly; SameSite=Lax").into_bytes(),
)
}));
if self.request.config.debug {
let dbg = headers
.iter()
.map(|(n, v)| {
format!(
"{}: {}",
String::from_utf8_lossy(n),
String::from_utf8_lossy(v)
)
})
.collect::<Vec<_>>()
.join("\r\n");
info!("\n=================响应信息 {:?}=================\n{}\n===========================================",
std::thread::current().id(), dbg);
}
let mut encoder = Encoder::new();
let block = encoder.encode(headers.iter().map(|h| (&h.0[..], &h.1[..])));
let end_stream = self.body.is_empty();
let mut frames = Vec::new();
let mut remaining = block.as_slice();
let mut first = true;
while !remaining.is_empty() {
let take = remaining.len().min(max_frame_size);
let (chunk, rest) = remaining.split_at(take);
remaining = rest;
let is_last = remaining.is_empty();
let mut flags = 0u8;
if is_last {
flags |= FLAG_END_HEADERS;
}
if is_last && end_stream {
flags |= FLAG_END_STREAM;
}
let mut f = Vec::with_capacity(9 + chunk.len());
let len = chunk.len();
f.push(((len >> 16) & 0xFF) as u8);
f.push(((len >> 8) & 0xFF) as u8);
f.push((len & 0xFF) as u8);
f.push(if first { 0x01 } else { 0x09 }); f.push(flags);
f.extend_from_slice(&(stream_id & 0x7FFF_FFFF).to_be_bytes());
f.extend_from_slice(chunk);
frames.push(f);
first = false;
}
if frames.is_empty() {
let mut f = vec![
0,
0,
0,
0x01,
FLAG_END_HEADERS | if end_stream { FLAG_END_STREAM } else { 0 },
0,
0,
0,
0,
];
f[5..9].copy_from_slice(&(stream_id & 0x7FFF_FFFF).to_be_bytes());
frames.push(f);
}
frames
}
fn http2_body(
&mut self,
stream_id: u32,
end_stream: bool,
max_frame_size: usize,
) -> Vec<Vec<u8>> {
if self.body.is_empty() {
return Vec::new();
}
let mut frames = Vec::new();
let mut off = 0usize;
let total = self.body.len();
while off < total {
let take = (total - off).min(max_frame_size);
let chunk = &self.body[off..off + take];
off += take;
let last = off == total;
frames.push(self.build_data_frame(stream_id, chunk, last && end_stream));
}
frames
}
fn build_data_frame(&self, stream_id: u32, payload: &[u8], end_stream: bool) -> Vec<u8> {
let len = payload.len();
let mut f = Vec::with_capacity(9 + len);
f.push(((len >> 16) & 0xFF) as u8);
f.push(((len >> 8) & 0xFF) as u8);
f.push((len & 0xFF) as u8);
f.push(0x00); f.push(if end_stream { FLAG_END_STREAM } else { 0 });
f.extend_from_slice(&(stream_id & 0x7FFF_FFFF).to_be_bytes());
if !payload.is_empty() {
f.extend_from_slice(payload);
}
f
}
fn on_options(&mut self) -> Result<(), HttpError> {
if self.allow_origins.is_empty() {
self.header("Access-Control-Allow-Origin", "*");
} else if self.request.header.has_key("origin") {
if self.allow_origins.contains(&self.request.origin.as_str()) {
self.header(
"Access-Control-Allow-Origin",
self.request.origin.to_string().as_str(),
);
} else {
return Err(HttpError::new(403, "Origin not allowed"));
}
} else {
return Err(HttpError::new(403, "Origin not allowed"));
}
if self.allow_headers.is_empty() {
if !self
.request
.header
.has_key("access-control-request-headers")
{
return Err(HttpError::new(403, "headers not allowed"));
}
self.header(
"Access-Control-Allow-Headers",
self.request.header["access-control-request-headers"]
.to_string()
.as_str(),
);
} else if !self
.request
.header
.has_key("access-control-request-headers")
{
let headers = self.allow_headers.join(",");
self.header("Access-Control-Allow-Headers", headers.to_string().as_str());
} else {
let headers = self.allow_headers.join(",");
self.header(
"Access-Control-Allow-Headers",
format!(
"{},{}",
self.request.header["access-control-request-headers"], headers
)
.as_str(),
);
}
if self.allow_methods.is_empty() {
if !self.request.header.has_key("access-control-request-method") {
return Err(HttpError::new(403, "methods not allowed"));
}
self.header(
"Access-Control-Allow-Methods",
self.request.header["access-control-request-method"]
.to_string()
.as_str(),
);
} else {
let methods = self.allow_methods.join(",");
self.header("Access-Control-Allow-Methods", methods.to_string().as_str());
}
self.header(
"Vary",
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
Ok(())
}
}
enum Extension {}
impl Extension {
pub fn form(extension: &str) -> String {
match extension.to_lowercase().as_str() {
"html" | "htm" => "text/html",
"css" => "text/css",
"js" | "mjs" => "application/javascript",
"json" => "application/json",
"xml" => "application/xml",
"txt" => "text/plain",
"csv" => "text/csv",
"md" => "text/markdown",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"svg" => "image/svg+xml",
"webp" => "image/webp",
"ico" => "image/x-icon",
"bmp" => "image/bmp",
"tiff" | "tif" => "image/tiff",
"avif" => "image/avif",
"mp4" => "video/mp4",
"webm" => "video/webm",
"avi" => "video/x-msvideo",
"mov" => "video/quicktime",
"mkv" => "video/x-matroska",
"mp3" => "audio/mpeg",
"wav" => "audio/wav",
"ogg" => "audio/ogg",
"flac" => "audio/flac",
"aac" => "audio/aac",
"woff" => "font/woff",
"woff2" => "font/woff2",
"ttf" => "font/ttf",
"otf" => "font/otf",
"eot" => "application/vnd.ms-fontobject",
"pdf" => "application/pdf",
"doc" => "application/msword",
"docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"xls" => "application/vnd.ms-excel",
"xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"ppt" => "application/vnd.ms-powerpoint",
"pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"zip" => "application/zip",
"rar" => "application/vnd.rar",
"7z" => "application/x-7z-compressed",
"tar" => "application/x-tar",
"gz" => "application/gzip",
"wasm" => "application/wasm",
"map" => "application/json",
_ => "application/octet-stream",
}
.to_string()
}
}
#[derive(Clone, Debug)]
pub struct CookieOptions {
pub name: String,
pub value: String,
pub path: Option<String>,
pub domain: Option<String>,
pub max_age: Option<i64>,
pub expires: Option<String>,
pub secure: bool,
pub http_only: bool,
pub same_site: SameSite,
}
impl Default for CookieOptions {
fn default() -> Self {
Self {
name: String::new(),
value: String::new(),
path: None,
domain: None,
max_age: None,
expires: None,
secure: true,
http_only: false,
same_site: SameSite::default(),
}
}
}
#[derive(Clone, Debug, Default)]
pub enum SameSite {
Strict,
#[default]
Lax,
None,
}
impl CookieOptions {
#[must_use]
pub fn new(name: &str, value: &str) -> Self {
Self {
name: name.to_string(),
value: value.to_string(),
path: Some("/".to_string()),
http_only: true,
same_site: SameSite::Lax,
..Default::default()
}
}
pub fn path(mut self, path: &str) -> Self {
self.path = Some(path.to_string());
self
}
pub fn domain(mut self, domain: &str) -> Self {
self.domain = Some(domain.to_string());
self
}
pub fn max_age(mut self, seconds: i64) -> Self {
self.max_age = Some(seconds);
self
}
pub fn expires(mut self, date: &str) -> Self {
self.expires = Some(date.to_string());
self
}
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
pub fn http_only(mut self, http_only: bool) -> Self {
self.http_only = http_only;
self
}
pub fn same_site(mut self, same_site: SameSite) -> Self {
self.same_site = same_site;
self
}
#[must_use]
pub fn to_header_value(&self) -> String {
let mut parts = vec![format!(
"{}={}",
sanitize_header_value(&self.name),
sanitize_header_value(&self.value)
)];
if let Some(ref path) = self.path {
parts.push(format!("Path={path}"));
}
if let Some(ref domain) = self.domain {
parts.push(format!("Domain={domain}"));
}
if let Some(max_age) = self.max_age {
parts.push(format!("Max-Age={max_age}"));
}
if let Some(ref expires) = self.expires {
parts.push(format!("Expires={expires}"));
}
if self.secure {
parts.push("Secure".to_string());
}
if self.http_only {
parts.push("HttpOnly".to_string());
}
match self.same_site {
SameSite::Strict => parts.push("SameSite=Strict".to_string()),
SameSite::Lax => parts.push("SameSite=Lax".to_string()),
SameSite::None => parts.push("SameSite=None".to_string()),
}
parts.join("; ")
}
}