diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bash.rs | 41 | ||||
-rw-r--r-- | src/http/code.rs | 8 | ||||
-rw-r--r-- | src/http/header.rs | 96 | ||||
-rw-r--r-- | src/http/method.rs | 30 | ||||
-rw-r--r-- | src/http/mod.rs | 6 | ||||
-rw-r--r-- | src/http/request.rs | 52 | ||||
-rw-r--r-- | src/http/response.rs | 42 | ||||
-rw-r--r-- | src/http/uri.rs | 106 | ||||
-rw-r--r-- | src/main.rs | 92 |
9 files changed, 473 insertions, 0 deletions
diff --git a/src/bash.rs b/src/bash.rs new file mode 100644 index 0000000..059909f --- /dev/null +++ b/src/bash.rs @@ -0,0 +1,41 @@ +use std::{env, collections::HashMap, fs::read_to_string, process::{exit, Command}}; + +pub fn load_config() -> HashMap<String, String> { + + let config_path = env::var("CONFIG_PATH").unwrap_or_else(|_| String::from("config")); + let config = match read_to_string(&config_path) { + Ok(data) => data, + Err(err) => { + eprintln!("cannot load '{config_path}': {err}"); + exit(1); + }, + }; + + let mut map = HashMap::new(); + let lines = config.split("\n").into_iter(); + + for line in lines { + let mut parts = line.trim().split(" "); + let Some(route) = parts.next() else { continue }; + let Some(script) = parts.next() else { continue }; + + println!("adding entry {route} => {script}"); + map.insert(route.to_owned(), script.to_owned()); + } + + map +} + +pub fn handle_script(script: &str, body: Option<&String>) -> Result<String, String> { + let mut command = Command::new(script); + if let Some(body) = body { + command.args([body]); + } + + let output = match command.output() { + Ok(o) => o, + Err(err) => return Err(format!("{err}")), + }; + + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} diff --git a/src/http/code.rs b/src/http/code.rs new file mode 100644 index 0000000..ba47282 --- /dev/null +++ b/src/http/code.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum Code { + Success = 200, + MethodNotAllowed = 405, + TooManyRequests = 429, + InternalServerError = 500, +} diff --git a/src/http/header.rs b/src/http/header.rs new file mode 100644 index 0000000..e6fc552 --- /dev/null +++ b/src/http/header.rs @@ -0,0 +1,96 @@ +use std::{collections::HashMap, str::Split}; + +#[derive(Debug, Clone)] +pub struct HeaderMap { + map: HashMap<String, usize>, + headers: Vec<Header> +} + +impl HeaderMap { + + #[allow(dead_code)] + pub fn get(&self, key: &str) -> Option<&Header> { + let Some(index) = self.map.get(key) else { + return None; + }; + return Some(&self.headers[index.to_owned()]); + } + + pub fn put(&mut self, header: Header) { + if let Some(index) = self.map.get(&header.key) { + self.headers[index.to_owned()] = header; + return + } + + let index = self.headers.len(); + self.map.insert(header.key.clone(), index); + self.headers.push(header); + } + + #[allow(dead_code)] + pub fn del(&mut self, key: &str) -> Option<Header> { + let Some(index) = self.map.get(key) else { + return None + }; + + let removed = self.headers.remove(index.to_owned()); + for i in (index.to_owned())..self.headers.len() { + let key = &self.headers[i].key; + + let Some(index) = self.map.get(key) else { + continue; + }; + + self.map.insert(key.clone(), index - 1); + } + + Some(removed) + } + + pub fn deserialize(&self) -> String { + let mut string = String::new(); + + for header in &self.headers { + string += &format!("{}: {}\n", header.key, header.value); + } + + string + } + + pub fn serialize(lines: &mut Split<char>) -> Self { + + let mut headers = Self::new(); + + loop { + let Some(header) = lines.next() else { break }; + if header.trim().len() < 1 { break } + + let mut parts = header.split(": ").into_iter(); + let Some(key) = parts.next() else { continue }; + let Some(value) = parts.next() else { continue }; + + headers.put(Header::new(key.trim(), value.trim())); + } + + headers + } + + pub fn new() -> Self { + Self { + map: HashMap::new(), + headers: Vec::new() + } + } +} + +#[derive(Debug, Clone)] +pub struct Header { + pub key: String, + pub value: String +} + +impl Header { + pub fn new(key: &str, value: &str) -> Self { + return Self {key: key.to_owned(), value: value.to_owned()} + } +} diff --git a/src/http/method.rs b/src/http/method.rs new file mode 100644 index 0000000..55cea65 --- /dev/null +++ b/src/http/method.rs @@ -0,0 +1,30 @@ +#[derive(Debug, Clone)] +pub enum Method { + Get, + Head, + Post, + Put, + Delete, + Connect, + Options, + Trace, + Patch +} + +impl Method { + pub fn serialize(string: &str) -> Option<Self> { + match string { + "GET" => Some(Self::Get), + "HEAD" => Some(Self::Head), + "POST" => Some(Self::Post), + "PUT" => Some(Self::Put), + "DELETE" => Some(Self::Delete), + "CONNECT" => Some(Self::Connect), + "OPTIONS" => Some(Self::Options), + "TRACE" => Some(Self::Trace), + "PATCH" => Some(Self::Patch), + _ => None + } + } +} + diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 0000000..62151bb --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,6 @@ +pub mod code; +pub mod method; +pub mod uri; +pub mod request; +pub mod response; +pub mod header; diff --git a/src/http/request.rs b/src/http/request.rs new file mode 100644 index 0000000..5ba72c9 --- /dev/null +++ b/src/http/request.rs @@ -0,0 +1,52 @@ +use super::{method::Method, uri::URI, header::HeaderMap}; + +#[derive(Debug, Clone)] +pub struct Request { + pub method: Method, + pub uri: URI, + pub headers: HeaderMap, + pub body: Option<String> +} + +impl Request { + pub fn serialize(req: &str) -> Option<Self> { + let mut lines = req.split('\n').to_owned(); + + let Some(head) = lines.next() else { + eprintln!("missing head str"); + return None + }; + + let mut parts = head.trim().split(" "); + + let Some(method_str) = parts.next() else { + eprintln!("missing method str"); + return None + }; + + let Some(method) = Method::serialize(method_str.trim()) else { + eprintln!("invalid http method"); + return None + }; + + let Some(uri_str) = parts.next() else { + eprintln!("missing uri str"); + return None + }; + + let Some(uri) = URI::serialize(uri_str.trim()) else { + eprintln!("invalid http uri"); + return None + }; + + let headers = HeaderMap::serialize(&mut lines); + let body: String = lines.collect(); + + Some(Self { + method, + uri, + headers, + body: if body.len() > 0 { Some(body) } else { None }, + }) + } +} diff --git a/src/http/response.rs b/src/http/response.rs new file mode 100644 index 0000000..850f41e --- /dev/null +++ b/src/http/response.rs @@ -0,0 +1,42 @@ +use super::{code::Code, header::{HeaderMap, Header}}; + +#[derive(Debug, Clone)] +pub struct Response { + pub status: Code, + pub headers: HeaderMap, + pub body: Option<String> +} + +impl Response { + + pub fn new() -> Self { + + let mut headers = HeaderMap::new(); + headers.put(Header::new("Connection", "close")); + + let date = chrono::offset::Utc::now(); + headers.put(Header::new("Date", &date.to_rfc2822())); + + headers.put(Header::new("Server", "bashttp")); + + return Self { + status: Code::Success, + headers, + body: None + } + } + + pub fn deserialize(&self) -> String { + let mut string = String::new(); + + string += &format!("HTTP/1.1 {}\n", self.status.clone() as u16); + string += &self.headers.deserialize(); + + if let Some(body) = &self.body { + string += "\n"; + string += body; + } + + string + } +} diff --git a/src/http/uri.rs b/src/http/uri.rs new file mode 100644 index 0000000..8faff69 --- /dev/null +++ b/src/http/uri.rs @@ -0,0 +1,106 @@ +#[derive(Debug, Clone)] +pub enum Protocol { + HTTP, + HTTPS, +} + +impl Protocol { + pub fn serialize(string: &str) -> Option<Self> { + match string { + "http" => return Some(Self::HTTP), + "https" => return Some(Self::HTTPS), + _ => return None + } + } + + pub fn deserialize(&self) -> &str { + match self { + Self::HTTP => "http", + Self::HTTPS => "https", + } + } +} + +#[derive(Debug, Clone)] +pub struct URI { + pub protocol: Option<Protocol>, + pub host: Option<String>, + pub port: Option<u16>, + pub route: String +} + +impl URI { + + #[allow(dead_code)] + pub fn deserialize(&self) -> String { + let mut string = String::new(); + + if let Some(protocol) = &self.protocol { + string += protocol.deserialize(); + string += "://"; + } + if let Some(host) = &self.host { + string += host; + } + if let Some(port) = self.port { + string += &format!(":{port}"); + } + string += &self.route; + + string + } + + pub fn serialize(head: &str) -> Option<Self> { + + let protocol_end = match head.find("://") { + Some(i) => i, + None => 0 + }; + + let protocol: Option<Protocol>; + let host_start: usize; + if protocol_end == 0 { + protocol = None; + host_start = 0; + } else { + let Some(p) = Protocol::serialize(&head[..protocol_end]) else { + return None + }; + protocol = Some(p); + host_start = protocol_end + 3; + } + + let host_route = &head[host_start..]; + let host_end = host_route.find("/").unwrap_or(head.len()); + + let host: Option<String>; + let port: Option<u16>; + if host_start == host_end { + host = None; + port = None; + } else { + if let Some (host_split) = host_route.find(":") { + let port_start = host_split + 1; + let port_str = &head[port_start..host_end]; + let Ok(p) = port_str.parse::<u16>() else { + return None + }; + host = Some(head[host_start..host_split].to_owned()); + port = Some(p); + } else { + host = Some(head[host_start..host_end].to_owned()); + port = None; + } + } + + let route = &head[host_end..]; + + Some(Self { + protocol, + host, + port, + route: route.to_owned() + }) + } + +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9cf8e90 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,92 @@ +use std::collections::HashMap; +use std::sync::Arc; +use http::code::Code; +use http::header::Header; +use http::request::Request; +use http::response::Response; +use tokio::net::{TcpListener, TcpStream}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::bash::handle_script; + +mod http; +mod bash; + +async fn handle_response(mut socket: TcpStream, code: Code, body: String) { + let mut res = Response::new(); + res.headers.put(Header::new("Content-Type", "text/plain")); + res.status = code; + res.body = Some(body); + + let res_str = res.deserialize(); + + let _ = socket.write(res_str.as_bytes()).await; +} + +async fn handle_connection(mut socket: TcpStream, config: Arc<HashMap<String, String>>) { + + let mut buf = [0; 1204]; + + let n: usize = match socket.read(&mut buf).await { + Ok(n) if n == 0 => return, + Ok(n) => n as usize, + Err(e) => { + eprintln!("failed to read from socket; err = {:?}", e); + return + } + }; + + let str = String::from_utf8_lossy(&buf[0..n]); + + let Some(req) = Request::serialize(&str) else { + return + }; + + + let Some(script) = config.get(&req.uri.route) else { + handle_response(socket, Code::MethodNotAllowed, "Method Not Allowed".to_owned()).await; + return + }; + + match handle_script(script, req.body.as_ref()) { + Ok(out) => { + handle_response(socket, Code::Success, out).await; + }, + Err(err) => { + handle_response(socket, Code::MethodNotAllowed, err).await; + }, + } +} + +#[tokio::main] +async fn main() { + + let config = Arc::new(bash::load_config()); + + let port = std::env::var("PORT") + .unwrap_or_else(|_| String::from("8080")) + .parse::<u16>() + .unwrap_or_else(|_| 8080); + + let addr = format!("127.0.0.1:{port}"); + + let Ok(listener) = TcpListener::bind(&addr).await else { + println!("failed to bind {addr}"); + return + }; + + println!("listening to tcp requests on {addr}"); + + loop { + let Ok((socket, _)) = listener.accept().await else { + println!("failed to accept new connection"); + continue + }; + + let config = config.clone(); + + tokio::spawn(async move { + handle_connection(socket, config).await; + }); + } +} |