summaryrefslogtreecommitdiff
path: root/spider_server
diff options
context:
space:
mode:
Diffstat (limited to 'spider_server')
-rw-r--r--spider_server/Cargo.toml7
-rw-r--r--spider_server/src/http_method.rs39
-rw-r--r--spider_server/src/http_server.rs135
-rw-r--r--spider_server/src/http_status.rs60
-rw-r--r--spider_server/src/lib.rs5
-rw-r--r--spider_server/src/request.rs114
-rw-r--r--spider_server/src/response.rs59
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;
+}