252 lines
5.3 KiB
Rust
252 lines
5.3 KiB
Rust
#[derive(Debug)]
|
|
pub struct Request {
|
|
pub method: Method,
|
|
pub path: String,
|
|
pub host: String,
|
|
pub user_agent: String,
|
|
pub real_ip: Option<String>,
|
|
pub range: Option<RequestRange>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum RequestRange {
|
|
From(usize),
|
|
Full(usize, usize),
|
|
Suffix(usize),
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
|
pub enum Method {
|
|
Get,
|
|
Head,
|
|
}
|
|
|
|
pub struct Response {
|
|
pub status: Status,
|
|
pub content: Option<Content>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
#[repr(u16)]
|
|
pub enum Status {
|
|
Ok = 200,
|
|
PartialContent = 206,
|
|
BadRequest = 400,
|
|
NotFound = 404,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Content {
|
|
mime_type: &'static str,
|
|
range: Option<(usize, usize, usize)>,
|
|
bytes: Vec<u8>,
|
|
}
|
|
|
|
impl Request {
|
|
pub fn parse(source: &str) -> Option<Self> {
|
|
let mut lines = source.lines();
|
|
let head = lines.next()?;
|
|
let (method, head) = head.split_once(' ')?;
|
|
let method = Method::parse(method)?;
|
|
let (path, version) = head.split_once(' ')?;
|
|
_ = version.strip_prefix("HTTP/1")?;
|
|
|
|
// parse http headers
|
|
let mut host = None;
|
|
let mut range = None;
|
|
let mut real_ip = None;
|
|
let mut user_agent = String::new();
|
|
for line in lines {
|
|
if line.is_empty() {
|
|
break;
|
|
}
|
|
let line = line.to_lowercase();
|
|
let (key, value) = line.split_once(": ")?;
|
|
match key {
|
|
"host" => host = Some(value.to_owned()),
|
|
"range" => range = RequestRange::parse(value),
|
|
"x-real-ip" => real_ip = Some(value.to_owned()),
|
|
"user-agent" => user_agent = value.to_owned(),
|
|
_ => (),
|
|
}
|
|
}
|
|
let host = host?;
|
|
|
|
// parse percent encoding
|
|
let mut path_bytes = path.bytes();
|
|
let mut path = Vec::with_capacity(path.len());
|
|
while let Some(byte) = path_bytes.next() {
|
|
if byte == b'%' {
|
|
let s = String::from_utf8(vec![path_bytes.next()?, path_bytes.next()?]).ok()?;
|
|
path.push(u8::from_str_radix(&s, 16).ok()?);
|
|
} else {
|
|
path.push(byte);
|
|
}
|
|
}
|
|
let path = String::from_utf8(path).ok()?;
|
|
|
|
Some(Self {
|
|
method,
|
|
path,
|
|
host,
|
|
real_ip,
|
|
range,
|
|
user_agent,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Response {
|
|
pub fn new(status: Status) -> Self {
|
|
Self {
|
|
status,
|
|
content: None,
|
|
}
|
|
}
|
|
|
|
pub fn format(mut self, head_only: bool) -> Vec<u8> {
|
|
if let Some(content) = self.content {
|
|
if content.range.is_some() {
|
|
self.status = Status::PartialContent;
|
|
}
|
|
let mut buffer = format!(
|
|
"{}\r\nContent-Type: {}\r\nContent-Length: {}\r\n",
|
|
self.status.header(),
|
|
content.mime_type,
|
|
content.bytes.len(),
|
|
)
|
|
.into_bytes();
|
|
if let Some((start, end, size)) = content.range {
|
|
buffer.extend_from_slice(
|
|
format!("Content-Range: bytes {start}-{end}/{size}\r\n").as_bytes(),
|
|
);
|
|
}
|
|
buffer.extend_from_slice(b"\r\n");
|
|
if !head_only {
|
|
buffer.extend_from_slice(&content.bytes);
|
|
}
|
|
buffer
|
|
} else {
|
|
format!("{}\r\n\r\n", self.status.header()).into_bytes()
|
|
}
|
|
}
|
|
|
|
pub fn with_content(mut self, content: Content) -> Self {
|
|
self.content = Some(content);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Content {
|
|
pub fn html(text: String) -> Self {
|
|
Self::file("html", text.into_bytes())
|
|
}
|
|
|
|
pub fn text(text: String) -> Self {
|
|
Self::file("txt", text.into_bytes())
|
|
}
|
|
|
|
pub fn file(ext: &str, bytes: Vec<u8>) -> Self {
|
|
let content_type = match ext {
|
|
"txt" | "md" | "toml" => "text/plain",
|
|
"html" | "htm" => "text/html",
|
|
"css" => "text/css",
|
|
|
|
"apng" => "image/apng",
|
|
"bmp" => "image/bmp",
|
|
"gif" => "image/gif",
|
|
"jpeg" | "jpg" => "image/jpeg",
|
|
"png" => "image/png",
|
|
"svg" => "image/svg+xml",
|
|
"tif" | "tiff" => "image/tiff",
|
|
"webp" => "image/webp",
|
|
|
|
"aac" => "audio/aac",
|
|
"mp3" => "audio/mpeg",
|
|
"oga" | "ogg" => "audio/ogg",
|
|
"opus" => "audio/opus",
|
|
"wav" => "audio/wav",
|
|
"weba" => "audio/webm",
|
|
|
|
"3gp" => "video/3gpp",
|
|
"3gp2" => "video/3gpp2",
|
|
"avi" => "video/x-msvideo",
|
|
"mov" => "video/mov",
|
|
"mp4" => "video/mp4",
|
|
"mpeg" => "video/mpeg",
|
|
"ogv" => "video/ogv",
|
|
"webm" => "video/webm",
|
|
|
|
"json" => "application/json",
|
|
"gz" => "application/gzip",
|
|
_ => {
|
|
if bytes.is_ascii() {
|
|
"text/plain"
|
|
} else {
|
|
"application/octet-stream"
|
|
}
|
|
}
|
|
};
|
|
Self {
|
|
mime_type: content_type,
|
|
range: None,
|
|
bytes,
|
|
}
|
|
}
|
|
|
|
pub fn with_range(mut self, range: Option<(usize, usize, usize)>) -> Self {
|
|
self.range = range;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Status {
|
|
pub fn header(self) -> String {
|
|
format!("HTTP/1.1 {} {}", self.code(), self.name())
|
|
}
|
|
|
|
pub fn code(self) -> u16 {
|
|
self as u16
|
|
}
|
|
|
|
pub fn name(self) -> &'static str {
|
|
match self {
|
|
Status::Ok => "OK",
|
|
Status::PartialContent => "PARTIAL CONTENT",
|
|
Status::BadRequest => "BAD REQUEST",
|
|
Status::NotFound => "NOT FOUND",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Method {
|
|
fn parse(source: &str) -> Option<Self> {
|
|
match source {
|
|
"GET" => Some(Self::Get),
|
|
"HEAD" => Some(Self::Head),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for Method {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Method::Get => write!(f, "GET"),
|
|
Method::Head => write!(f, "HEAD"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RequestRange {
|
|
fn parse(source: &str) -> Option<Self> {
|
|
let source = source.strip_prefix("bytes=")?;
|
|
let (start, end) = source.split_once('-')?;
|
|
match (start.is_empty(), end.is_empty()) {
|
|
(true, true) => None,
|
|
(true, false) => Some(Self::Suffix(end.parse().ok()?)),
|
|
(false, true) => Some(Self::From(start.parse().ok()?)),
|
|
(false, false) => Some(Self::Full(start.parse().ok()?, end.parse().ok()?)),
|
|
}
|
|
}
|
|
}
|