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 | 7 | ||||
-rw-r--r-- | src/http/method.rs | 16 | ||||
-rw-r--r-- | src/http/mod.rs | 1 | ||||
-rw-r--r-- | src/http/request.rs | 4 | ||||
-rw-r--r-- | src/http/response.rs | 8 | ||||
-rw-r--r-- | src/main.rs | 70 | ||||
-rw-r--r-- | src/script.rs | 94 | ||||
-rw-r--r-- | src/server.rs | 51 |
10 files changed, 181 insertions, 119 deletions
diff --git a/src/bash.rs b/src/bash.rs deleted file mode 100644 index 059909f..0000000 --- a/src/bash.rs +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index ba47282..0000000 --- a/src/http/code.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[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 index e6fc552..03e4303 100644 --- a/src/http/header.rs +++ b/src/http/header.rs @@ -57,9 +57,7 @@ impl HeaderMap { string } - pub fn serialize(lines: &mut Split<char>) -> Self { - - let mut headers = Self::new(); + pub fn serialize(&mut self, lines: &mut Split<char>) { loop { let Some(header) = lines.next() else { break }; @@ -69,10 +67,9 @@ impl HeaderMap { let Some(key) = parts.next() else { continue }; let Some(value) = parts.next() else { continue }; - headers.put(Header::new(key.trim(), value.trim())); + self.put(Header::new(key.trim(), value.trim())); } - headers } pub fn new() -> Self { diff --git a/src/http/method.rs b/src/http/method.rs index 55cea65..cecafbf 100644 --- a/src/http/method.rs +++ b/src/http/method.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash, Eq, PartialEq)] pub enum Method { Get, Head, @@ -26,5 +26,19 @@ impl Method { _ => None } } + + pub fn deserialize(&self) -> &str { + match self { + Method::Get => "GET", + Method::Head => "HEAD", + Method::Post => "POST", + Method::Put => "PUT", + Method::Delete => "DELETE", + Method::Connect => "CONNECT", + Method::Options => "OPTIONS", + Method::Trace => "TRACE", + Method::Patch => "PATCH", + } + } } diff --git a/src/http/mod.rs b/src/http/mod.rs index 62151bb..02ba89d 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,4 +1,3 @@ -pub mod code; pub mod method; pub mod uri; pub mod request; diff --git a/src/http/request.rs b/src/http/request.rs index 5ba72c9..286efa3 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -39,7 +39,9 @@ impl Request { return None }; - let headers = HeaderMap::serialize(&mut lines); + let mut headers = HeaderMap::new(); + headers.serialize(&mut lines); + let body: String = lines.collect(); Some(Self { diff --git a/src/http/response.rs b/src/http/response.rs index 850f41e..85430a4 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -1,8 +1,8 @@ -use super::{code::Code, header::{HeaderMap, Header}}; +use super::{header::{HeaderMap, Header}}; #[derive(Debug, Clone)] pub struct Response { - pub status: Code, + pub status: u16, pub headers: HeaderMap, pub body: Option<String> } @@ -20,7 +20,7 @@ impl Response { headers.put(Header::new("Server", "bashttp")); return Self { - status: Code::Success, + status: 200, headers, body: None } @@ -29,7 +29,7 @@ impl Response { pub fn deserialize(&self) -> String { let mut string = String::new(); - string += &format!("HTTP/1.1 {}\n", self.status.clone() as u16); + string += &format!("HTTP/1.1 {}\n", self.status); string += &self.headers.deserialize(); if let Some(body) = &self.body { diff --git a/src/main.rs b/src/main.rs index 9cf8e90..0f7dc59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,69 +1,23 @@ -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; +use std::{env, sync::Arc}; +use tokio::net::TcpListener; +use crate::server::handle_connection; 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; -} +mod script; +mod server; -async fn handle_connection(mut socket: TcpStream, config: Arc<HashMap<String, String>>) { - - let mut buf = [0; 1204]; +#[tokio::main] +async fn main() { - 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); + let config = match script::Config::load() { + Ok(config) => Arc::new(config), + Err(err) => { + eprintln!("failed to load config: {err}"); 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") + let port = env::var("PORT") .unwrap_or_else(|_| String::from("8080")) .parse::<u16>() .unwrap_or_else(|_| 8080); diff --git a/src/script.rs b/src/script.rs new file mode 100644 index 0000000..369fbf8 --- /dev/null +++ b/src/script.rs @@ -0,0 +1,94 @@ +use std::{env, collections::HashMap, fs::read_to_string, process::Command, result::Result, error::Error}; + +use crate::http::{response::Response, method::Method}; + +pub struct Script { + path: String +} + +impl Script { + fn new(path: String) -> Self { + Self { path } + } + + pub fn run(&self, body: Option<&String>) -> Result<Response, std::io::Error> { + let mut res = Response::new(); + + let mut command = Command::new(&self.path); + if let Some(body) = body { + command.args([body]); + } + + let output = match command.output() { + Ok(output) => output, + Err(err) => return Err(err) + }; + + let status = output.status.code().unwrap_or(500) as u16; + + let response = String::from_utf8_lossy(&output.stdout).into_owned(); + let mut lines = response.split('\n').into_iter(); + + res.headers.serialize(&mut lines); + + let body: String = lines.collect::<Vec<&str>>().join("\n"); + if body.len() > 0 { + res.body = Some(body); + } + + res.status = status; + + Ok(res) + } +} + +pub struct Config { + map: HashMap<String, Script> +} + +impl Config { + + fn key(method: &Method, route: &str) -> String { + format!("{}:{}", method.deserialize(), route) + } + + pub fn load() -> Result<Self, Box<dyn Error>> { + + let config_path = env::var("CONFIG_PATH").unwrap_or_else(|_| String::from("config")); + let config = read_to_string(&config_path)?; + + let mut map = HashMap::new(); + let lines = config.split("\n").into_iter(); + + for (i, line) in lines.enumerate() { + let mut parts = line.split_whitespace(); + + let Some(method_str) = parts.next() else { continue }; + let method = Method::serialize(&method_str) + .ok_or(format!("config line {i} has invalid http method: {method_str}"))?; + + let route = parts.next() + .ok_or(format!("config line {i} missing http route"))?; + + let script = parts.next() + .ok_or(format!{"config line {i} missing script path"})?; + + let key = Self::key(&method, route); + let value = Script::new(script.to_owned()); + + println!("adding script {script} for {} {route}", method.deserialize()); + map.insert(key, value); + } + + if map.len() < 1 { + return Err("cannot run server, config is empty".into()) + } + + Ok(Self { map }) + } + + pub fn get(&self, method: &Method, route: &str) -> Option<&Script> { + self.map.get(&Self::key(method, route)) + } + +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..4b1f2b3 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; +use tokio::{net::TcpStream, io::{AsyncWriteExt, AsyncReadExt}}; +use crate::{http::{header::Header, response::Response, request::Request}, script::Config}; + +async fn send_response(mut socket: TcpStream, code: u16, 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; +} + +pub async fn handle_connection(mut socket: TcpStream, config: Arc<Config>) { + + 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.method, &req.uri.route) else { + send_response(socket, 405, "Method Not Allowed".to_owned()).await; + return + }; + + match script.run(req.body.as_ref()) { + Ok(res) => { + let res_str = res.deserialize(); + let _ = socket.write(res_str.as_bytes()).await; + }, + Err(err) => { + println!("{err}"); + send_response(socket, 500, format!("{err}")).await; + }, + } +} |