diff options
Diffstat (limited to 'spider_server')
| -rw-r--r-- | spider_server/Cargo.toml | 7 | ||||
| -rw-r--r-- | spider_server/src/http_method.rs | 39 | ||||
| -rw-r--r-- | spider_server/src/http_server.rs | 135 | ||||
| -rw-r--r-- | spider_server/src/http_status.rs | 60 | ||||
| -rw-r--r-- | spider_server/src/lib.rs | 5 | ||||
| -rw-r--r-- | spider_server/src/request.rs | 114 | ||||
| -rw-r--r-- | spider_server/src/response.rs | 59 |
7 files changed, 419 insertions, 0 deletions
diff --git a/spider_server/Cargo.toml b/spider_server/Cargo.toml new file mode 100644 index 0000000..df70b3b --- /dev/null +++ b/spider_server/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "spider_server" +version = "0.1.0" +authors = ["Andrew <saintruler@gmail.com>"] +edition = "2018" + +[dependencies] diff --git a/spider_server/src/http_method.rs b/spider_server/src/http_method.rs new file mode 100644 index 0000000..814c93b --- /dev/null +++ b/spider_server/src/http_method.rs @@ -0,0 +1,39 @@ +use std::fmt; + +#[derive(PartialEq)] +pub enum HttpMethod { + GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH +} + +impl fmt::Display for HttpMethod { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + HttpMethod::GET => write!(f, "GET"), + HttpMethod::HEAD => write!(f, "HEAD"), + HttpMethod::POST => write!(f, "POST"), + HttpMethod::PUT => write!(f, "PUT"), + HttpMethod::DELETE => write!(f, "DELETE"), + HttpMethod::CONNECT => write!(f, "CONNECT"), + HttpMethod::OPTIONS => write!(f, "OPTIONS"), + HttpMethod::TRACE => write!(f, "TRACE"), + HttpMethod::PATCH => write!(f, "PATCH"), + } + } +} + +impl HttpMethod { + pub fn parse(s: String) -> Option<HttpMethod> { + match &*s { + "GET" => Some(HttpMethod::GET), + "HEAD" => Some(HttpMethod::HEAD), + "POST" => Some(HttpMethod::POST), + "PUT" => Some(HttpMethod::PUT), + "DELETE" => Some(HttpMethod::DELETE), + "CONNECT" => Some(HttpMethod::CONNECT), + "OPTIONS" => Some(HttpMethod::OPTIONS), + "TRACE" => Some(HttpMethod::TRACE), + "PATCH" => Some(HttpMethod::PATCH), + _ => None + } + } +} diff --git a/spider_server/src/http_server.rs b/spider_server/src/http_server.rs new file mode 100644 index 0000000..479ef3f --- /dev/null +++ b/spider_server/src/http_server.rs @@ -0,0 +1,135 @@ +use std::net::{TcpListener, TcpStream, Shutdown}; +use std::io::Write; +use std::str; +use std::fmt; + +use crate::request::Request; +use crate::response::Response; +use crate::http_method::HttpMethod; + +pub trait HttpHandler { + fn do_get(&self, _request: Request) -> Response { + return self.default_action(_request); + } + + fn do_head(&self, _request: Request) -> Response { + return self.default_action(_request); + } + + fn do_post(&self, _request: Request) -> Response { + return self.default_action(_request); + } + + fn do_put(&self, _request: Request) -> Response { + return self.default_action(_request); + } + + fn do_delete(&self, _request: Request) -> Response { + return self.default_action(_request); + } + + fn do_connect(&self, _request: Request) -> Response { + return self.default_action(_request); + } + + fn do_options(&self, _request: Request) -> Response { + return self.default_action(_request); + } + + fn do_trace(&self, _request: Request) -> Response { + return self.default_action(_request); + } + + fn do_patch(&self, _request: Request) -> Response { + return self.default_action(_request); + } + + fn default_action(&self, _request: Request) -> Response { + let msg = String::from("<h1>Method not allowed</h1>"); + return Response::html(msg, 405); + } +} + +pub struct HttpServer<T: HttpHandler> { + host: String, + port: u16, + socket: TcpListener, + handler: T +} + +impl<T> HttpServer<T> where T: HttpHandler { + // TODO(andrew): add explanations for errors? + pub fn new(host: &str, port: u16, handler: T) -> Result<HttpServer<T>, &str> { + let addr = format!("{}:{}", host, port); + let sock = TcpListener::bind(addr); + match sock { + Ok(s) => { + let server = HttpServer { + host: String::from(host), + port: port, + socket: s, + handler: handler, + }; + return Ok(server); + }, + Err(_) => return Err("Couldn't start server") + }; + } + + pub fn serve_forever(&self) -> Result<(), &str> { + for stream in self.socket.incoming() { + match stream { + Ok(s) => { + // TODO(andrew): replace println with logging. + println!("Got connection!"); + match self.handle_client(&s) { + Ok(_) => (), + Err(msg) => return Err(msg) + }; + match s.shutdown(Shutdown::Both) { + Ok(_) => println!("Closed connection"), + Err(_) => return Err("Couldn't close client socket") + }; + }, + Err(_) => break + }; + } + return Ok(()); + } + + fn handle_client(&self, mut stream: &TcpStream) -> Result<(), &str> { + let mut buf: [u8; 1024] = [0; 1024]; + // TODO(andrew): read all body, not first 1024 bytes. + stream.peek(&mut buf).expect("Couldn't read from socket"); + + let request = Request::from(&buf); + let request = match request { + Some(r) => r, + None => return Err("Request parsed with errors") + }; + + let response = match request.method { + HttpMethod::GET => self.handler.do_get(request), + HttpMethod::HEAD => self.handler.do_head(request), + HttpMethod::POST => self.handler.do_post(request), + HttpMethod::PUT => self.handler.do_put(request), + HttpMethod::DELETE => self.handler.do_delete(request), + HttpMethod::CONNECT => self.handler.do_connect(request), + HttpMethod::OPTIONS => self.handler.do_options(request), + HttpMethod::TRACE => self.handler.do_trace(request), + HttpMethod::PATCH => self.handler.do_patch(request), + }; + let response = response.format(); + match stream.write(&response) { + Ok(_) => return Ok(()), + Err(_) => return Err("Couldn't write to client socket") + }; + } +} + +impl<T> fmt::Display for HttpServer<T> where T: HttpHandler { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + return write!(f, "HttpServer({}:{})", self.host, self.port); + } +} + diff --git a/spider_server/src/http_status.rs b/spider_server/src/http_status.rs new file mode 100644 index 0000000..0ea889e --- /dev/null +++ b/spider_server/src/http_status.rs @@ -0,0 +1,60 @@ +pub fn get_status_text(code: u16) -> Option<String> { + match code { + 100 => Some(String::from("Continue")), + 101 => Some(String::from("Switching Protocols")), + 102 => Some(String::from("Processing")), + 200 => Some(String::from("OK")), + 201 => Some(String::from("Created")), + 202 => Some(String::from("Accepted")), + 203 => Some(String::from("Non Authoritative Information")), + 204 => Some(String::from("No Content")), + 205 => Some(String::from("Reset Content")), + 206 => Some(String::from("Partial Content")), + 207 => Some(String::from("Multi Status")), + 226 => Some(String::from("IM Used")), + 300 => Some(String::from("Multiple Choices")), + 301 => Some(String::from("Moved Permanently")), + 302 => Some(String::from("Found")), + 303 => Some(String::from("See Other")), + 304 => Some(String::from("Not Modified")), + 305 => Some(String::from("Use Proxy")), + 307 => Some(String::from("Temporary Redirect")), + 308 => Some(String::from("Permanent Redirect")), + 400 => Some(String::from("Bad Request")), + 401 => Some(String::from("Unauthorized")), + 402 => Some(String::from("Payment Required")), + 403 => Some(String::from("Forbidden")), + 404 => Some(String::from("Not Found")), + 405 => Some(String::from("Method Not Allowed")), + 406 => Some(String::from("Not Acceptable")), + 407 => Some(String::from("Proxy Authentication Required")), + 408 => Some(String::from("Request Timeout")), + 409 => Some(String::from("Conflict")), + 410 => Some(String::from("Gone")), + 411 => Some(String::from("Length Required")), + 412 => Some(String::from("Precondition Failed")), + 413 => Some(String::from("Request Entity Too Large")), + 414 => Some(String::from("Request URI Too Long")), + 415 => Some(String::from("Unsupported Media Type")), + 416 => Some(String::from("Requested Range Not Satisfiable")), + 417 => Some(String::from("Expectation Failed")), + 418 => Some(String::from("I'm a teapot")), + 421 => Some(String::from("Misdirected Request")), + 422 => Some(String::from("Unprocessable Entity")), + 423 => Some(String::from("Locked")), + 424 => Some(String::from("Failed Dependency")), + 426 => Some(String::from("Upgrade Required")), + 428 => Some(String::from("Precondition Required")), + 429 => Some(String::from("Too Many Requests")), + 431 => Some(String::from("Request Header Fields Too Large")), + 449 => Some(String::from("Retry With")), + 451 => Some(String::from("Unavailable For Legal Reasons")), + 500 => Some(String::from("Internal Server Error")), + 501 => Some(String::from("Not Implemented")), + 502 => Some(String::from("Bad Gateway")), + 503 => Some(String::from("Service Unavailable")), + 504 => Some(String::from("Gateway Timeout")), + 505 => Some(String::from("HTTP Version Not Supported")), + _ => None + } +} diff --git a/spider_server/src/lib.rs b/spider_server/src/lib.rs new file mode 100644 index 0000000..9f4074f --- /dev/null +++ b/spider_server/src/lib.rs @@ -0,0 +1,5 @@ +pub mod request; +pub mod response; +pub mod http_method; +pub mod http_server; +mod http_status; diff --git a/spider_server/src/request.rs b/spider_server/src/request.rs new file mode 100644 index 0000000..3bd6a8f --- /dev/null +++ b/spider_server/src/request.rs @@ -0,0 +1,114 @@ +use std::collections::HashMap; +use std::fmt; + +use crate::http_method::HttpMethod; + +pub struct Request { + pub method: HttpMethod, + pub path: String, + pub http_version: String, + pub headers: HashMap<String, String>, + pub body: Vec<u8>, +} + +impl Request { + pub fn new( method: HttpMethod + , path: String + , http_version: String + , headers: HashMap<String, String> + , body: Vec<u8> ) -> Request { + return Request { + method, + path, + http_version, + headers, + body, + }; + } + + // TODO(andrew): add some error handling. + pub fn from(data: &[u8]) -> Option<Request> { + let mut lines: Vec<String> = Vec::new(); + let mut line = String::new(); + let mut idx = 0; + let mut char_count = 0; + + // Parsing headers until first empty line (char_count == 0). + for c in data { + let c = *c as char; + line.push(c); + idx += 1; + + if c == '\n' { + lines.push(line); + if char_count == 0 { break; } + else { line = String::new(); } + } + if c != '\r' && c != '\n' { + char_count += 1; + } + } + + let mut body = data[idx..].to_vec(); + + let mut headers: HashMap<String, String> = HashMap::new(); + if lines.len() > 0 { + for line in &lines[1..] { + let line = line + .trim_end_matches("\r\n") + .split(": ") + .collect::<Vec<&str>>(); + headers.insert(String::from(line[0]), line[1..].join(" ")); + } + } + + let request_line = lines[0] + .trim_end_matches("\r\n") + .split(" ") + .collect::<Vec<&str>>(); + + let (path, params) = split_path(String::from(request_line[1])); + + let method = String::from(request_line[0]); + match HttpMethod::parse(method) { + Some(method) => { + if method == HttpMethod::GET { + if let Some(p) = params { + body = p.as_bytes().to_vec(); + } + } + return Some(Request::new( + method, + path, + String::from(request_line[2]), + headers, + body )); + } + None => return None + }; + } +} + +fn split_path(resource: String) -> (String, Option<String>) { + let mut idx = None; + for (i, c) in resource.chars().enumerate() { + if c == '?' { + idx = Some(i); + break; + } + } + match idx { + Some(n) => { + let path = &resource[..n]; + let params = &resource[n + 1..]; + return (path.to_string(), Some(params.to_string())); + }, + None => return (resource, None) + }; +} + +impl fmt::Display for Request { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + return write!(f, "Request({}, {})", self.method, self.path); + } +} diff --git a/spider_server/src/response.rs b/spider_server/src/response.rs new file mode 100644 index 0000000..fe1df78 --- /dev/null +++ b/spider_server/src/response.rs @@ -0,0 +1,59 @@ +use std::collections::HashMap; +use std::fmt; + +use crate::http_status::get_status_text; + +pub struct Response { + code: u16, + headers: HashMap<String, String>, + body: Vec<u8>, +} + +// TODO(andrew): add more constructors for different content types. +impl Response { + pub fn html(html: String, status_code: u16) -> Response { + let mut headers = HashMap::new(); + headers.insert( String::from("Content-Type") + , String::from("text/html") ); + + return Response { + code: status_code, + headers: headers, + body: html.as_bytes().to_vec(), + }; + } + + pub fn format<'a>(&self) -> Vec<u8> { + // FIXME(andrew): is undefined status code an error that should + // be handled here? + let status_text = match get_status_text(self.code) { + Some(text) => text, + None => String::from("UNDEFINED") + }; + + let mut data = Vec::new(); + + let first_line = format!("HTTP/1.1 {} {}", self.code, status_text); + let headers = format_headers(&self.headers); + let head = format!("{}\r\n{}\r\n", first_line, headers); + + data.extend_from_slice(head.as_bytes()); + data.extend_from_slice(&self.body); + return data; + } +} + +impl fmt::Display for Response { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + return write!(f, "Response({})", self.code); + } +} + +fn format_headers(headers: &HashMap<String, String>) -> String { + let mut result = String::new(); + for (key, value) in headers.iter() { + let line = format!("{}: {}\r\n", key, value); + result.push_str(&line); + } + return result; +} |