Created
July 15, 2024 07:06
-
-
Save 0x24a/1cf4e9c5597ef48c621607a3e9ce3b8c to your computer and use it in GitHub Desktop.
Yet Another Rust HTTP Server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::{collections::HashMap, io::{BufRead, BufReader, Write, Read}, net::TcpListener, thread}; | |
struct HTTP101Request { | |
method: String, | |
path: String, | |
headers: HashMap<String, String>, | |
body: Vec<u8>, | |
} | |
struct HTTP101Response { | |
response_code: i16, | |
response_text: String, | |
headers: HashMap<String, String>, | |
body: Vec<u8>, | |
} | |
struct YARHSError { | |
message: String, | |
} | |
impl std::fmt::Display for YARHSError { | |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
write!(f, "{}", self.message) | |
} | |
} | |
fn resolve_http101_request(request: &str) -> Result<HTTP101Request, YARHSError> { | |
let mut lines = request.lines(); | |
let headline = lines.next().ok_or(YARHSError { message: String::from("Invalid headline: Empty") })?; | |
let headline_parts: Vec<&str> = headline.split_whitespace().collect(); | |
if headline_parts.len() != 3 { | |
return Err(YARHSError { message: String::from("Invalid headline: Invalid length") }); | |
} | |
let method = headline_parts[0].to_string(); | |
let path = headline_parts[1].to_string(); | |
let mut headers = HashMap::new(); | |
let mut body = Vec::new(); | |
for line in lines { | |
if line.is_empty() { | |
break; | |
} | |
let elements: Vec<&str> = line.splitn(2, ": ").collect(); | |
if elements.len() == 2 { | |
headers.insert(elements[0].to_string(), elements[1].to_string()); | |
} | |
} | |
// Collect the body | |
let body_start = request.find("\r\n\r\n").unwrap_or(request.len()) + 4; | |
if body_start < request.len() { | |
body.extend_from_slice(&request.as_bytes()[body_start..]); | |
} | |
Ok(HTTP101Request { | |
method, | |
path, | |
headers, | |
body, | |
}) | |
} | |
fn construct_http101_response(response: HTTP101Response) -> Vec<u8> { | |
let mut data = String::new(); | |
data.push_str("HTTP/1.1 "); | |
data.push_str(&response.response_code.to_string()); | |
data.push_str(" "); | |
data.push_str(&response.response_text); | |
data.push_str("\r\n"); | |
for (key, value) in &response.headers { | |
data.push_str(key); | |
data.push_str(": "); | |
data.push_str(value); | |
data.push_str("\r\n"); | |
} | |
data.push_str("\r\n"); | |
let mut head = data.as_bytes().to_vec(); | |
head.extend(response.body); | |
head | |
} | |
fn create_app(addr: String, handler: fn(HTTP101Request) -> HTTP101Response) { | |
println!("Listening YetAnotherRustHTTPServer on http://{}", addr); | |
let server = TcpListener::bind(addr).expect("Failed to start HTTP server"); | |
for connection in server.incoming() { | |
match connection { | |
Ok(stream) => { | |
thread::spawn(move || { | |
handle_connection(stream, handler); | |
}); | |
} | |
Err(e) => eprintln!("Failed to accept connection: {}", e), | |
} | |
} | |
} | |
fn handle_connection(mut stream: std::net::TcpStream, handler: fn(HTTP101Request) -> HTTP101Response) { | |
let mut buf_reader = BufReader::new(&mut stream); | |
let mut request = String::new(); | |
// Read headers | |
loop { | |
let mut line = String::new(); | |
match buf_reader.read_line(&mut line) { | |
Ok(0) => break, // End of stream | |
Ok(_) => { | |
if line == "\r\n" { | |
break; // End of headers | |
} | |
request.push_str(&line); | |
}, | |
Err(e) => { | |
eprintln!("Failed to read request header: {}", e); | |
return; | |
} | |
} | |
} | |
// Read the body | |
let content_length = request.find("Content-Length: ") | |
.and_then(|start| { | |
let end = request[start..].find("\r\n")?; | |
request[start + 16..start + end].trim().parse::<usize>().ok() | |
}) | |
.unwrap_or(0); | |
let mut body = vec![0; content_length]; | |
if let Err(e) = buf_reader.read_exact(&mut body) { | |
eprintln!("Failed to read request body: {}", e); | |
return; | |
} | |
request.push_str("\r\n"); | |
request.push_str(&String::from_utf8_lossy(&body)); | |
if request.is_empty() { | |
return; | |
} | |
let request = match resolve_http101_request(&request) { | |
Ok(req) => req, | |
Err(e) => { | |
eprintln!("Failed to resolve request: {}", e); | |
return; | |
}, | |
}; | |
let req_method = request.method.clone(); | |
let req_path = request.path.clone(); | |
let mut response = handler(request); | |
let resp_code = response.response_code; | |
let resp_text = response.response_text.clone(); | |
response.headers.insert("Server".to_string(), "YetAnotherRustHTTPServer".to_string()); | |
let response = construct_http101_response(response); | |
if let Err(e) = stream.write_all(&response) { | |
eprintln!("Failed to write response: {}", e); | |
} | |
if let Err(e) = stream.flush() { | |
eprintln!("Failed to flush stream: {}", e); | |
} | |
println!("{} {} - {} {} - {}", req_method, req_path, resp_code, resp_text, stream.peer_addr().expect("Failed to read remote address")); | |
} | |
fn main() { | |
fn handler(request: HTTP101Request) -> HTTP101Response { | |
let mut headers_processed = String::new(); | |
for item in request.headers.iter(){ | |
headers_processed.push_str(format!("{}: {}<br>", item.0, item.1).as_str()); | |
} | |
let _ = request.body; | |
let body=format!("<h1>YetAnotherRustHTTPServer</h1>This is a demo HTTP server by 0x24a using Rust.<h2>Request Infomations</h2>Method: {}<br>Path: {}<br>Headers:<br>{}", request.method, request.path, headers_processed); | |
let mut headers = HashMap::new(); | |
headers.insert("Content-Type".to_string(), "text/html".to_string()); | |
HTTP101Response { | |
response_code: 200, | |
response_text: "OK".to_string(), | |
headers: headers, | |
body: body.as_bytes().to_vec(), | |
} | |
} | |
create_app("127.0.0.1:10087".to_string(), handler); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'm new to rust - just 2 days ago. So this may be crappy.