Skip to content

Instantly share code, notes, and snippets.

@Ichbinjoe
Created December 19, 2020 19:14
Show Gist options
  • Save Ichbinjoe/f00c13d1da5846facf08c0b477f5e97d to your computer and use it in GitHub Desktop.
Save Ichbinjoe/f00c13d1da5846facf08c0b477f5e97d to your computer and use it in GitHub Desktop.
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