commit 231c392f23facd0d16ec63d93b6fcc0a17a69577 Author: AINDUSTRIES Date: Sat Nov 30 20:40:23 2024 +0100 Project start diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b471067 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +.idea diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..05f02f3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "aweblib" +version = "0.1.0" +edition = "2021" + +[features] +default = ["windows"] +windows = [] +linux = ["dep:daemonize"] + +[dependencies] +tokio = {version = "1.41.1", features = ["macros", "rt-multi-thread", "net"]} +hyper = {version = "1.5.1", features = ["server", "http1"]} +hyper-util = {version = "0.1.10", features = ["tokio"]} +serde_json = {version = "1.0.132"} +serde = { version = "1.0.214", features = ["derive"] } +bytes = "1.8.0" +http-body-util = "0.1.2" +sqlx = {version = "0.8.1", features = ["sqlite", "sqlx-macros", "runtime-tokio"]} +daemonize = {version = "0.5.0", optional = true} + +[dev-dependencies] +futures = "0.3.31" \ No newline at end of file diff --git a/routes-test.json b/routes-test.json new file mode 100644 index 0000000..022dd2e --- /dev/null +++ b/routes-test.json @@ -0,0 +1,11 @@ +{ + "": { + "resource": {"File": "test.html"} + }, + "/test/test3": { + "resource": "None" + }, + "/test2": { + "resource": "None" + } +} \ No newline at end of file diff --git a/src/body.rs b/src/body.rs new file mode 100644 index 0000000..e550f5b --- /dev/null +++ b/src/body.rs @@ -0,0 +1,35 @@ +use bytes::Bytes; +use http_body_util::Full; +use hyper::body::Frame; +use std::convert::Infallible; +use std::pin::Pin; +use std::task::{Context, Poll}; + +#[derive(Debug)] +pub enum Body { + Empty, + Full(Full), +} +impl Body { + pub(crate) fn new(body: T) -> Body + where + Bytes: From, + { + Body::Full(Full::new(Bytes::from(body))) + } +} + +impl hyper::body::Body for Body { + type Data = Bytes; + type Error = Infallible; + + fn poll_frame( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + match &mut *self.get_mut() { + Self::Full(body) => Pin::new(body).poll_frame(cx), + Self::Empty => Poll::Ready(None), + } + } +} diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..ea57782 --- /dev/null +++ b/src/database.rs @@ -0,0 +1,36 @@ +use std::fmt::{Display, Formatter}; +use std::path::PathBuf; + +#[derive(Clone)] +enum DbType { + Sqlite, +} + +impl Display for DbType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match *self { + DbType::Sqlite => f.write_str("sqlite"), + } + } +} + +#[derive(Clone)] +pub(crate) struct Database { + db_type: DbType, + db_path: PathBuf, +} + +impl From for Database { + fn from(value: PathBuf) -> Self { + Database { + db_type: DbType::Sqlite, + db_path: value, + } + } +} + +impl From for String { + fn from(value: Database) -> String { + format!("{}:{}", value.db_type, value.db_path.display()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..30bce55 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] +#![allow(unused_variables)] +use crate::database::Database; +use crate::routing::Routes; +use crate::server::Server; +use crate::settings::Settings; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +mod body; +mod database; +mod renderer; +mod routing; +mod server; +mod settings; + +pub struct SimpleHttpServer { + server: Server, +} + +impl SimpleHttpServer { + fn new() -> Self { + SimpleHttpServer { + server: Server::new(Settings { + address: SocketAddr::from(([127, 0, 0, 1], 80)), + routes: Arc::new(Mutex::new(Routes::default())), + database: Database::from(PathBuf::from("database.db")), + }), + } + } + + fn run(self) -> Result<(), Box> { + self.server.run() + } +} diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..c8112fb --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,23 @@ +use crate::body::Body; +use hyper::{Response, StatusCode}; +use std::fs::File; +use std::io::Read; +pub(crate) fn get_static(file: &String) -> Response { + let file = read_static(file); + match file { + Ok(file) => Response::new(Body::new(file)), + Err(_) => Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::Empty) + .unwrap(), + } +} + +pub(crate) fn get_rendered() {} + +fn read_static(file: &String) -> Result, Box> { + let mut buf = Vec::new(); + let mut file = File::open(file)?; + file.read_to_end(&mut buf)?; + Ok(buf) +} diff --git a/src/routing.rs b/src/routing.rs new file mode 100644 index 0000000..dd57b79 --- /dev/null +++ b/src/routing.rs @@ -0,0 +1,183 @@ +use crate::body::Body; +use crate::renderer::get_static; +use hyper::{Request, Response}; +use serde::Deserialize; +use std::collections::HashMap; +use std::fs::File; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +#[derive(Clone, Deserialize, Default, Debug, Eq, PartialEq)] +pub(crate) enum Resource { + Page(String), + File(String), + Static, + #[default] + None, +} + +#[derive(Clone, Deserialize, Debug, Default, Eq, PartialEq)] +pub(crate) struct Route { + resource: Resource, +} + +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub(crate) struct RouteTree { + route: Route, + route_nodes: HashMap, +} + +impl From> for RouteTree { + fn from(value: HashMap) -> Self { + let mut route_tree = RouteTree::default(); + let urls: Vec = value.keys().cloned().collect(); + let splits: Vec<(String, Vec)> = urls + .into_iter() + .map(|x| { + let sp = x.split("/").map(|x| x.to_string()).collect(); + (x, sp) + }) + .collect::)>>() + .into_iter() + .map(|(k, x)| { + let subs: Vec = x.into_iter().map(|x| format!("/{}", x)).collect(); + (k, subs) + }) + .collect(); + for split in splits { + let key = split.0; + let split = split.1; + let route = value.get(&key).unwrap().clone(); + route_tree.insert(&split, route); + } + route_tree + } +} + +impl RouteTree { + fn insert(&mut self, item: &[String], route: Route) { + let key = item[0].clone(); + let entry = self.route_nodes.entry(key).or_default(); + if item.len() > 1 { + entry.insert(&item[1..item.len()], route); + } else { + entry.route = route; + } + } +} + +#[derive(Clone, Default)] +pub(crate) struct Routes { + tree: RouteTree, +} + +impl From for Routes { + fn from(path: PathBuf) -> Self { + let file = File::open(path); + match file { + Ok(file) => { + if let Ok(routes) = serde_json::from_reader::>(file) { + let tree = RouteTree::from(routes); + Self { tree } + } else { + Self::default() + } + } + Err(_) => Self::default(), + } + } +} + +pub(crate) async fn route(routes: Arc>, req: Request) -> Response +where + T: hyper::body::Body, +{ + let url = req.uri().path(); + let split = url + .split("/") + .map(|x| format!("/{}", x)) + .collect::>(); + let routes = routes.lock().unwrap(); + let mut tree = &routes.tree; + let mut i = 0; + while let Some(route) = tree.route_nodes.get(&split[i]) { + tree = route; + if i == split.len() { + break; + } + i += 1; + } + let route = &tree.route; + match &route.resource { + Resource::File(file) => get_static(file), + _ => Response::new(Body::Empty), + } +} + +#[cfg(test)] +mod test { + use crate::body::Body; + use crate::renderer::get_static; + use crate::routing::{route, Resource, Route, RouteTree, Routes}; + use futures::executor::block_on; + use http_body_util::BodyExt; + use hyper::Request; + use std::collections::HashMap; + use std::path::PathBuf; + use std::sync::{Arc, Mutex}; + + #[test] + fn load_from_file() { + let routes = Routes::from(PathBuf::from("routes-test.json")); + let tree = routes.tree; + + // Bellow is: + // "/": {"/test": {"/test3"}, "/test2"} + // with + // "/" = File("test.html") + // "/test" = None + // "/test2" = None + // "/test3" = None + let mut to_match = HashMap::new(); + let mut root_tree = RouteTree::default(); + root_tree.route = Route { + resource: Resource::File("test.html".into()), + }; + let root = to_match.entry("/".into()).or_insert(root_tree); + let mut not_empty = RouteTree::default(); + not_empty.insert(&["/test3".to_string()], Route::default()); + root.route_nodes.insert("/test".to_string(), not_empty); + root.route_nodes + .insert("/test2".to_string(), RouteTree::default()); + + assert_eq!(tree.route_nodes, to_match); + } + + #[test] + fn wrong_path() { + let routes = Routes::from(PathBuf::from("a_wrong_file.json")); + + assert_eq!(routes.tree.route_nodes, HashMap::new()); + } + + #[test] + fn route_file() { + let routes = Routes::from(PathBuf::from("routes-test.json")); + let request = Request::builder().uri("/").body(Body::Empty).unwrap(); + let from_test = block_on(Box::pin(async { + let response = route(Arc::new(Mutex::new(routes)), request).await; + let body = response.into_body().collect().await.unwrap(); + let bytes = body.to_bytes(); + let string = String::from_utf8(Vec::from(bytes)).unwrap(); + string + })); + let actual = block_on(Box::pin(async { + let response = get_static(&"test.html".into()); + let body = response.into_body().collect().await.unwrap(); + let bytes = body.to_bytes(); + let string = String::from_utf8(Vec::from(bytes)).unwrap(); + string + })); + assert_eq!(from_test, actual); + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..785b62d --- /dev/null +++ b/src/server.rs @@ -0,0 +1,47 @@ +use crate::body::Body; +use crate::routing::route; +use crate::settings::Settings; +use hyper::body::Incoming; +use hyper::server::conn::http1; +use hyper::service::Service; +use hyper::{Request, Response}; +use hyper_util::rt::TokioIo; +use std::future::Future; +use std::pin::Pin; +use tokio::net::TcpListener; + +#[derive(Clone)] +pub struct Server { + settings: Settings, +} + +impl Server { + pub fn new(settings: Settings) -> Server { + Server { settings } + } + + #[tokio::main] + pub async fn run(self) -> Result<(), Box> { + let listener = TcpListener::bind(self.settings.address).await?; + loop { + let (stream, _) = listener.accept().await?; + let io = TokioIo::new(stream); + let clone = self.clone(); + tokio::task::spawn(async move { + if let Err(error) = http1::Builder::new().serve_connection(io, clone).await { + println!("failed to serve connection: {}", error); + } + }); + } + } +} + +impl Service> for Server { + type Response = Response; + type Error = Box; + type Future = Pin> + Send>>; + fn call(&self, req: Request) -> Self::Future { + let routes = self.settings.routes.clone(); + Box::pin(async move { Ok(route(routes, req).await) }) + } +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..cfb728f --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,11 @@ +use crate::database::Database; +use crate::routing::Routes; +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub struct Settings { + pub(crate) address: SocketAddr, + pub(crate) routes: Arc>, + pub(crate) database: Database, +} diff --git a/test.html b/test.html new file mode 100644 index 0000000..60a34ce --- /dev/null +++ b/test.html @@ -0,0 +1,10 @@ + + + + + TEST + + + + + \ No newline at end of file