Created
December 19, 2020 19:14
-
-
Save Ichbinjoe/f00c13d1da5846facf08c0b477f5e97d to your computer and use it in GitHub Desktop.
This file contains 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 crate::pool::ThreadPool; | |
use crate::request::Request; | |
use crate::response::Response; | |
use crate::serveroptions::ServerOptions; | |
use crate::statuscode::StatusCode; | |
use crossbeam_utils::thread; | |
use regex::Regex; | |
use std::collections::HashMap; | |
use std::io::prelude::*; | |
use std::net::{TcpListener, TcpStream}; | |
#[derive(Clone, Debug)] | |
pub struct Server { | |
// How many of these fields actually have to be pub? | |
// So this makes sense, but is still strange. Is ServerOptions small enough to just copy / own? | |
pub options: &'static ServerOptions, | |
pub heartbeats: usize, | |
pub router: Router, | |
} | |
#[derive(Clone, Debug)] | |
pub struct Router { | |
// Again, how much of this has to be public? | |
pub strict_slash: bool, | |
// Interesting thing to think about: HashMaps actually have a bad worstcase (which you won't | |
// hit but I'm going to throw out) - O(n) as they fall back to a linear search when there are | |
// lots of hash collisions. Even in normal cases you can see this. See | |
// https://abseil.io/blog/20180927-swisstables for a brief description of what rust uses | |
// internally these days | |
// | |
// But whats the alternative? BTreeMap https://doc.rust-lang.org/std/collections/struct.BTreeMap.html | |
// Check it out - BTrees are rather intresting to begin with as they are disk & cache friendly. | |
// I was taught they were only disk freindly in school, but cache friendlyness is a | |
// surprisingly big deal as well! | |
pub routes: HashMap<String, Route>, | |
} | |
impl Router { | |
pub fn new() -> Router { | |
Router { | |
// But when is it false? :thinking: | |
strict_slash: true, | |
routes: HashMap::new(), | |
} | |
} | |
fn map_route(&mut self, route: Route) { | |
self.routes | |
.insert(route.http_method.clone().to_owned() + route.path, route); | |
} | |
} | |
#[derive(Clone, Debug)] | |
pub struct Route { | |
pub handler: fn(Request, Response), | |
// These should be owned. Why? Well, because thats what you eventually turn it into anyways. | |
// While rust puts a lot of emphasis on references & borrowing, this is a classic case where | |
// you should really just take ownership of the object instead. 'static is gross, especially | |
// considering its basically instantly stripped. | |
pub path: &'static str, | |
pub http_method: &'static str, | |
} | |
impl Route { | |
pub fn new( | |
path: &'static str, | |
http_method: &'static str, | |
handler: fn(Request, Response), | |
) -> Route { | |
// You can use shorthand init here - https://doc.rust-lang.org/book/ch05-01-defining-structs.html#using-the-field-init-shorthand-when-variables-and-fields-have-the-same-name | |
Route { | |
path: path, | |
http_method: http_method, | |
handler: handler, | |
} | |
} | |
} | |
impl Server { | |
pub fn new(router: Router) -> Server { | |
Server { | |
options: ServerOptions::new(), | |
heartbeats: 0, | |
// Shorthand! | |
router: router, | |
} | |
} | |
pub fn get_listener(&self) -> Option<TcpListener> { | |
// Nit: this really isn't getting self's listener, its creating a new listener. | |
// Further, self.options.host should be an owned string or be instead just passed into this | |
// function directly. | |
let mut host: String = self.options.host.to_owned(); | |
// You don't actually need any of this. this all has to get reparsed - see | |
// https://doc.rust-lang.org/src/std/net/addr.rs.html#961 (or the String equivalent which | |
// is actually what this is) | |
host.push_str(":"); | |
host.push_str(&self.options.port.to_string()[..]); | |
// but what can you do instead? This works: | |
// | |
// ``` | |
// Some(TcpListener::bind((host, port)).unwrap()) | |
// ``` | |
// | |
// Why? (str, u16) implements ToSocketAddrs: | |
// https://doc.rust-lang.org/std/net/trait.ToSocketAddrs.html (see Implementors section) | |
// | |
// But! You are making an optional but have no 'None' case. You are also using .unwrap, | |
// which should be avoided in production code like the plague unless the library you are | |
// using doesn't allow you to do whatever you are doing unsafely and you _know_ it won't | |
// panic. Soooooooooooooooooooooooooooooooooooo why not just make the function return | |
// io::Result<TcpListener> and your body is just TcpListener::bind((self.options.host, | |
// self.options.port)) or somethign like that idk figure out the types | |
Some(TcpListener::bind(host).unwrap()) | |
} | |
pub fn map_route(&mut self, route: Route) { | |
// hot take - when is this ever used when the server is running? Immutability is a gift if | |
// you can afford it! | |
self.router.map_route(route); | |
} | |
pub fn handle_connection( | |
&self, | |
// stream: &mut TcpStream??? I don't think this does what you think it does or this is | |
// another syntax but regardless we want a mutable borrow of stream here | |
mut stream: &TcpStream, | |
) -> std::io::Result<(Request, Response)> { | |
// This works great.... as long as the request is always <2k | |
// Also, this is stack allocated. Thats fine, but also potentially not. I'm not your mom so | |
// I won't tell you what to do with your buffers | |
let mut byte_buffer = [0; 2048]; | |
// why unwrap when you can just bail | |
// stream.read(&mut byte_buffer)?; | |
stream.read(&mut byte_buffer).unwrap(); | |
// This is fine but incurs a parse penalty over the entire buffer, which may be incorrect | |
// for POST w/ payload (as that can be straight binary iirc) | |
let buffer = String::from_utf8_lossy(&byte_buffer[..]).to_string(); | |
let mut request = Request::new(); | |
// So this is a nit becasue its a test project.... but you can totally compile these once. | |
// Most of the penalty of using regexes is actually in the compilation phase. I would throw | |
// these in a lazy_static: https://docs.rs/lazy_static/1.4.0/lazy_static/ | |
let request_header_regex = Regex::new(r"^(\w+) (\S+) HTTP/1.1").unwrap(); | |
let host_regex = Regex::new(r"Host: (\S+)").unwrap(); | |
let content_type_regex = Regex::new(r"Content-Type: (\S+)").unwrap(); | |
let content_length_regex = Regex::new(r"content-length: (\d+)").unwrap(); | |
let content_regex = Regex::new(r"Connection: (\S+)").unwrap(); | |
let user_agent_regex = Regex::new(r"User-Agent: (\S+)").unwrap(); | |
// So this is sort of getting into edgecase hell - all of this info is _supposed_ to appear | |
// in a certain order (request, headers, optional post data), but this code assumes it can | |
// just be in any order anywhere within the first 2k. Big scare. This likely isn't a big | |
// deal for your application but yeah. | |
// Asserting on a user supplied input? Tell me it isn't so!!! | |
assert!(request_header_regex.is_match(&buffer.to_string())); | |
// Woah there buddy. Actually a memory leak. Like, | |
// https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak advertises this | |
let bufferwith_static_lifetime: &'static str = Box::leak(buffer.into_boxed_str()); | |
// A lot of this gets cleaned up if the above is fixed. | |
let request_method; | |
// request_header_regex.captures(bufferwith_static_lifetime).map(|captures| captures.get(1).unwrap().as_str()).unwrap_or("") | |
match request_header_regex.captures(bufferwith_static_lifetime) { | |
Some(captures) => { | |
request_method = captures.get(1).unwrap().as_str(); | |
} | |
None => { | |
request_method = ""; | |
} | |
} | |
let request_path; | |
match request_header_regex.captures(bufferwith_static_lifetime) { | |
Some(captures) => { | |
request_path = captures.get(2).unwrap().as_str(); | |
} | |
None => { | |
request_path = ""; | |
} | |
} | |
let host; | |
match host_regex.captures(bufferwith_static_lifetime) { | |
Some(captures) => { | |
host = captures.get(1).unwrap().as_str(); | |
} | |
None => { | |
host = ""; | |
} | |
} | |
let content_type; | |
match content_type_regex.captures(bufferwith_static_lifetime) { | |
Some(captures) => { | |
content_type = captures.get(1).unwrap().as_str(); | |
} | |
None => { | |
content_type = "text/html"; | |
} | |
} | |
let content_length; | |
match content_length_regex.captures(bufferwith_static_lifetime) { | |
Some(captures) => { | |
content_length = captures.get(1).unwrap().as_str(); | |
} | |
None => { | |
content_length = "0"; | |
} | |
} | |
let user_agent; | |
match user_agent_regex.captures(bufferwith_static_lifetime) { | |
Some(captures) => { | |
user_agent = captures.get(1).unwrap().as_str(); | |
} | |
None => { | |
user_agent = ""; | |
} | |
} | |
// This needs its types cleaned up. I think this does a syscall, and you really shouldn't | |
// need to do that here. | |
let mut response = Response::new(stream.try_clone()?); | |
response.content_length = content_length.parse::<usize>().unwrap(); | |
request.host = host; | |
request.content_type = content_type; | |
request.user_agent = user_agent; | |
request.request_method = request_method; | |
request.path = request_path; | |
let mut _content: &'static str; | |
if let Some(_content) = content_regex.captures(bufferwith_static_lifetime) { | |
request.body = &bufferwith_static_lifetime[_content.get(1).unwrap().end() + 1 | |
.._content.get(1).unwrap().end() + 1 + content_length.parse::<usize>().unwrap()]; | |
} | |
Ok((request, response)) | |
} | |
pub fn heartbeat(&mut self) -> &mut Self { | |
self.heartbeats += 1; | |
println!("{}", self.heartbeats); | |
self | |
} | |
pub fn start(&self) { | |
println!("Running"); | |
// This will open a port, print the port, then close the port... | |
println!("{:?}", self.get_listener().as_ref().unwrap()); | |
let pool = ThreadPool::new(4); | |
// Only to open it here again! | |
for stream in self.get_listener().as_ref().unwrap().incoming() { | |
// thread::scope(|s| { | |
// s.spawn(|_| { | |
pool.execute(move || { | |
let (req, mut res) = self.handle_connection(&stream.unwrap()).unwrap(); | |
let result = self | |
.router | |
.routes | |
.get(&(req.request_method.clone().to_owned() + req.path)); | |
match result { | |
Some(route) => { | |
(route.handler)(req, res); | |
} | |
_ => { | |
res.send_status(StatusCode::NotFound); | |
} | |
} | |
}); | |
// }); | |
// }); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment