overhaul; generate directory index
This commit is contained in:
parent
ecadce6730
commit
c408a56830
2 changed files with 302 additions and 46 deletions
206
src/http.rs
Normal file
206
src/http.rs
Normal file
|
@ -0,0 +1,206 @@
|
|||
#[derive(Debug)]
|
||||
pub struct Request {
|
||||
pub method: Method,
|
||||
pub path: String,
|
||||
pub host: String,
|
||||
pub range: Option<ContentRange>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ContentRange {
|
||||
From(usize),
|
||||
Full(usize, usize),
|
||||
Suffix(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
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 {
|
||||
content_type: &'static str,
|
||||
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")?;
|
||||
|
||||
let mut host = None;
|
||||
let mut range = None;
|
||||
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 = ContentRange::parse(value),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
let host = host?;
|
||||
|
||||
//todo parse path %hex
|
||||
|
||||
Some(Self {
|
||||
method,
|
||||
path: path.to_owned(),
|
||||
host,
|
||||
range,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn new(status: Status) -> Self {
|
||||
Self {
|
||||
status,
|
||||
content: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(self, head_only: bool) -> Vec<u8> {
|
||||
if let Some(content) = self.content {
|
||||
let mut data = format!(
|
||||
"{}\r\nContent-Type: {}\r\nContent-Length: {}\r\n\r\n",
|
||||
self.status.header(),
|
||||
content.content_type,
|
||||
content.bytes.len(),
|
||||
)
|
||||
.into_bytes();
|
||||
if !head_only {
|
||||
data.extend_from_slice(&content.bytes);
|
||||
}
|
||||
data
|
||||
} 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",
|
||||
"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 {
|
||||
content_type,
|
||||
bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 => "",
|
||||
Status::BadRequest => "",
|
||||
Status::NotFound => "NOT FOUND",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Method {
|
||||
fn parse(source: &str) -> Option<Self> {
|
||||
match source {
|
||||
"GET" => Some(Self::Get),
|
||||
"HEAD" => Some(Self::Head),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentRange {
|
||||
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()?)),
|
||||
}
|
||||
}
|
||||
}
|
142
src/main.rs
142
src/main.rs
|
@ -1,6 +1,13 @@
|
|||
use std::io::prelude::*;
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::{env, fs};
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, File},
|
||||
io::{Read, Write},
|
||||
net::{TcpListener, TcpStream},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
mod http;
|
||||
use http::{Content, Method, Request, Response, Status};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
@ -35,9 +42,8 @@ fn handle_connection(mut stream: TcpStream) {
|
|||
let request = String::from_utf8_lossy(&buffer);
|
||||
|
||||
let peer_addr = stream.peer_addr().ok();
|
||||
|
||||
println!(
|
||||
"Received {} bytes from {:?}\n\n[{}]",
|
||||
"Received {} bytes from {:?}\n=======\n{}=======\n\n",
|
||||
size,
|
||||
peer_addr,
|
||||
request
|
||||
|
@ -47,51 +53,95 @@ fn handle_connection(mut stream: TcpStream) {
|
|||
.replace("\\n", "\n")
|
||||
);
|
||||
|
||||
let mut request = request.into_owned();
|
||||
while request.contains("..") {
|
||||
request = request.replace("..", "");
|
||||
}
|
||||
let request = Request::parse(&request);
|
||||
|
||||
if request.starts_with("GET") {
|
||||
if let Some(file) = request.split_whitespace().nth(1) {
|
||||
let path = format!("./{}", file);
|
||||
send_file(&mut stream, &path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let response = "HTTP/1.1 400 BAD REQUEST\r\n\r\n";
|
||||
stream
|
||||
.write_all(response.as_bytes())
|
||||
.unwrap_or_else(|_| println!("failed to respond"));
|
||||
stream
|
||||
.flush()
|
||||
.unwrap_or_else(|_| println!("failed to respond"));
|
||||
}
|
||||
let response;
|
||||
|
||||
fn send_file(stream: &mut TcpStream, path: &str) {
|
||||
if let Ok(text) = fs::read_to_string(path) {
|
||||
let contents = text + "\n\n";
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: {}; charset=UTF-8\r\nContent-Length: {}\r\n\r\n{}",
|
||||
if path.ends_with(".html") {
|
||||
"text/html"
|
||||
} else {
|
||||
"text/plain"
|
||||
},
|
||||
contents.len(),
|
||||
contents
|
||||
);
|
||||
stream
|
||||
.write_all(response.as_bytes())
|
||||
.unwrap_or_else(|_| println!("failed to respond"));
|
||||
if let Some(request) = request {
|
||||
let head_only = request.method == Method::Head;
|
||||
let path = request.path.clone();
|
||||
response = get_file(request)
|
||||
.map(|content| Response::new(Status::Ok).with_content(content))
|
||||
.unwrap_or_else(|| {
|
||||
Response::new(Status::NotFound)
|
||||
.with_content(Content::text(format!("FILE NOT FOUND - '{}'", path)))
|
||||
})
|
||||
.format(head_only);
|
||||
} else {
|
||||
eprintln!("File does not exist: {}", path);
|
||||
let response = format!("HTTP/1.1 404 NOT FOUND\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Length: {}\r\n\r\n{}", path.len(), path);
|
||||
stream
|
||||
.write_all(response.as_bytes())
|
||||
.unwrap_or_else(|_| println!("failed to respond with 404"));
|
||||
};
|
||||
response = Response::new(Status::BadRequest).format(false);
|
||||
}
|
||||
|
||||
stream
|
||||
.write_all(&response)
|
||||
.unwrap_or_else(|_| println!("failed to respond"));
|
||||
stream
|
||||
.flush()
|
||||
.unwrap_or_else(|_| println!("failed to respond"));
|
||||
}
|
||||
|
||||
fn get_file(request: Request) -> Option<Content> {
|
||||
let path = PathBuf::from(format!("./{}", &request.path))
|
||||
.canonicalize()
|
||||
.ok()?;
|
||||
if path.strip_prefix(env::current_dir().unwrap()).is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
let index_file = path.join("index.html");
|
||||
if index_file.is_file() {
|
||||
Some(Content::html(fs::read_to_string(index_file).ok()?))
|
||||
} else {
|
||||
generate_index(&request.path, &path)
|
||||
}
|
||||
} else if path.is_file() {
|
||||
let ext = path.extension().unwrap_or_default().to_str()?;
|
||||
let mut buf = Vec::new();
|
||||
File::open(&path).ok()?.read_to_end(&mut buf).ok()?;
|
||||
Some(Content::file(ext, buf))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_index(relative_path: &str, path: &Path) -> Option<Content> {
|
||||
let list = path
|
||||
.read_dir()
|
||||
.ok()?
|
||||
.flatten()
|
||||
.filter_map(|e| {
|
||||
let target = e.file_name().to_str()?.to_string();
|
||||
let mut s = format!(
|
||||
" <li><a href=\"{}\"> {}",
|
||||
PathBuf::from(relative_path).join(&target).display(),
|
||||
target
|
||||
);
|
||||
if e.file_type().ok()?.is_dir() {
|
||||
s.push('/');
|
||||
}
|
||||
s.push_str("</a></li>\n");
|
||||
Some(s)
|
||||
})
|
||||
.fold(String::new(), |mut content, entry| {
|
||||
content.push_str(&entry);
|
||||
content
|
||||
});
|
||||
let page = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Index of {relative_path}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Index of {relative_path}</h3>
|
||||
<ul>
|
||||
<li><a href="..">../</a></li>
|
||||
{list}
|
||||
</ul>
|
||||
</body>
|
||||
</html>"#,
|
||||
);
|
||||
Some(Content::html(page))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue