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()?)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
src/main.rs
132
src/main.rs
|
@ -1,6 +1,13 @@
|
||||||
use std::io::prelude::*;
|
use std::{
|
||||||
use std::net::{TcpListener, TcpStream};
|
env,
|
||||||
use std::{env, fs};
|
fs::{self, File},
|
||||||
|
io::{Read, Write},
|
||||||
|
net::{TcpListener, TcpStream},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod http;
|
||||||
|
use http::{Content, Method, Request, Response, Status};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args: Vec<String> = env::args().collect();
|
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 request = String::from_utf8_lossy(&buffer);
|
||||||
|
|
||||||
let peer_addr = stream.peer_addr().ok();
|
let peer_addr = stream.peer_addr().ok();
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Received {} bytes from {:?}\n\n[{}]",
|
"Received {} bytes from {:?}\n=======\n{}=======\n\n",
|
||||||
size,
|
size,
|
||||||
peer_addr,
|
peer_addr,
|
||||||
request
|
request
|
||||||
|
@ -47,51 +53,95 @@ fn handle_connection(mut stream: TcpStream) {
|
||||||
.replace("\\n", "\n")
|
.replace("\\n", "\n")
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut request = request.into_owned();
|
let request = Request::parse(&request);
|
||||||
while request.contains("..") {
|
|
||||||
request = request.replace("..", "");
|
let response;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
response = Response::new(Status::BadRequest).format(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
stream
|
||||||
.write_all(response.as_bytes())
|
.write_all(&response)
|
||||||
.unwrap_or_else(|_| println!("failed to respond"));
|
.unwrap_or_else(|_| println!("failed to respond"));
|
||||||
stream
|
stream
|
||||||
.flush()
|
.flush()
|
||||||
.unwrap_or_else(|_| println!("failed to respond"));
|
.unwrap_or_else(|_| println!("failed to respond"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_file(stream: &mut TcpStream, path: &str) {
|
fn get_file(request: Request) -> Option<Content> {
|
||||||
if let Ok(text) = fs::read_to_string(path) {
|
let path = PathBuf::from(format!("./{}", &request.path))
|
||||||
let contents = text + "\n\n";
|
.canonicalize()
|
||||||
let response = format!(
|
.ok()?;
|
||||||
"HTTP/1.1 200 OK\r\nContent-Type: {}; charset=UTF-8\r\nContent-Length: {}\r\n\r\n{}",
|
if path.strip_prefix(env::current_dir().unwrap()).is_err() {
|
||||||
if path.ends_with(".html") {
|
return None;
|
||||||
"text/html"
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
"text/plain"
|
generate_index(&request.path, &path)
|
||||||
},
|
}
|
||||||
contents.len(),
|
} else if path.is_file() {
|
||||||
contents
|
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
|
||||||
);
|
);
|
||||||
stream
|
if e.file_type().ok()?.is_dir() {
|
||||||
.write_all(response.as_bytes())
|
s.push('/');
|
||||||
.unwrap_or_else(|_| println!("failed to respond"));
|
}
|
||||||
} else {
|
s.push_str("</a></li>\n");
|
||||||
eprintln!("File does not exist: {}", path);
|
Some(s)
|
||||||
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
|
.fold(String::new(), |mut content, entry| {
|
||||||
.write_all(response.as_bytes())
|
content.push_str(&entry);
|
||||||
.unwrap_or_else(|_| println!("failed to respond with 404"));
|
content
|
||||||
};
|
});
|
||||||
stream
|
let page = format!(
|
||||||
.flush()
|
r#"<!DOCTYPE html>
|
||||||
.unwrap_or_else(|_| println!("failed to respond"));
|
<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