summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bash.rs41
-rw-r--r--src/http/code.rs8
-rw-r--r--src/http/header.rs96
-rw-r--r--src/http/method.rs30
-rw-r--r--src/http/mod.rs6
-rw-r--r--src/http/request.rs52
-rw-r--r--src/http/response.rs42
-rw-r--r--src/http/uri.rs106
-rw-r--r--src/main.rs92
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;
+ });
+ }
+}