From 20b113d30d7ad9eb3a33312276df1f1cf148ff71 Mon Sep 17 00:00:00 2001 From: silt Date: Fri, 8 Aug 2025 02:46:35 +0000 Subject: [PATCH] routerless http frontend and frontend manager --- Cargo.lock | 11 ++- Cargo.toml | 5 ++ src/frontend.rs | 40 +++++++++ src/http_fe.rs | 217 ++++++++++++++++++++++++++++++++++++++++++++++++ src/server.rs | 69 +++++++++++++++ src/yapper.rs | 77 +++++++++++++++++ 6 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 src/frontend.rs create mode 100644 src/http_fe.rs create mode 100644 src/server.rs create mode 100644 src/yapper.rs diff --git a/Cargo.lock b/Cargo.lock index 314c573..9a516d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,6 +272,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "humansize" version = "2.1.3" @@ -535,6 +541,7 @@ name = "mintee" version = "0.0.1" dependencies = [ "git2", + "httparse", "serde", "tera", ] @@ -1362,9 +1369,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdbb9122ea75b11bf96e7492afb723e8a7fbe12c67417aa95e7e3d18144d37cd" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", diff --git a/Cargo.toml b/Cargo.toml index b691f7f..4fa9049 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,10 @@ name = "mintee" version = "0.0.1" edition = "2024" +[[bin]] +name = "frontend" +path = "src/frontend.rs" + [dependencies] git2 = "0.20.2" serde = { version = "1.0.219", default-features = false, features = ["derive"] } @@ -10,3 +14,4 @@ serde = { version = "1.0.219", default-features = false, features = ["derive"] } #inkjet = "0.11.1" #markdown = "1.0.0" tera = { version = "1.20.0", default-features = false, features = ["builtins"] } +httparse = "1.10.1" diff --git a/src/frontend.rs b/src/frontend.rs new file mode 100644 index 0000000..2485cdd --- /dev/null +++ b/src/frontend.rs @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 silt + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This file is part of Mintee. + * + * Mintee is free software: you can redistribute it and/or modify it under the + * terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Mintee is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Mintee. If not, see https://www.gnu.org/licenses/. + */ + +use std::{error::Error, time::Duration}; + +mod server; +use server::Pool; + + +mod http_fe; +use http_fe::FrontendImpl; + +mod gem_fe; + +mod yapper; + +fn main() -> Result<(), Box> { + let http_fe = http_fe::Frontend::init(http_fe::FeConfig::init("0.0.0.0:8080", Duration::new(2, 0), Duration::new(2, 0))?); + + let pool = Pool::<32>::new(); + + Ok(()) +} diff --git a/src/http_fe.rs b/src/http_fe.rs new file mode 100644 index 0000000..e6ffb48 --- /dev/null +++ b/src/http_fe.rs @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2025 silt + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This file is part of Mintee. + * + * Mintee is free software: you can redistribute it and/or modify it under the + * terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Mintee is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Mintee. If not, see https://www.gnu.org/licenses/. + */ + +use httparse; +use std::{ + error::Error, + fmt, + io::{self, BufRead, BufReader, Read}, + net::{Incoming, SocketAddr, TcpListener, TcpStream, ToSocketAddrs}, + process::exit, + time::Duration, +}; + +use crate::{eyap, yap}; + +pub use super::server::{Frontend, FrontendImpl}; + +#[derive(Debug, Clone, Copy)] +pub enum HttpErrorKind { + BadRequest, + Unauthorized, + Forbidden, + NotFound, + MethodNotAllowed, + UriTooLong, + ImATeapot, + InternalServerError, + NotImplemented, + HttpVersionNotSupported, +} + +impl From for usize { + fn from(val: HttpErrorKind) -> Self { + use HttpErrorKind::*; + match val { + BadRequest => 400, + Unauthorized => 401, + Forbidden => 403, + NotFound => 404, + MethodNotAllowed => 405, + UriTooLong => 414, + ImATeapot => 418, + InternalServerError => 500, + NotImplemented => 501, + HttpVersionNotSupported => 505, + } + } +} + +impl From for &str { + fn from(val: HttpErrorKind) -> Self { + use HttpErrorKind::*; + match val { + BadRequest => "Bad Request", + Unauthorized => "Unauthorized", + Forbidden => "Forbidden", + NotFound => "NotFound", + MethodNotAllowed => "Method Not Allowed", + UriTooLong => "URI Too Long", + ImATeapot => "I'm A Teapot", + InternalServerError => "Internal Server Error", + NotImplemented => "Not Implemented", + HttpVersionNotSupported => "HTTP Version Not Supported", + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct HttpError { + kind: HttpErrorKind, +} + +impl fmt::Display for HttpError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "HTTP/1.1 {} {}", + Into::::into(self.kind), + Into::<&str>::into(self.kind) + ) + } +} + +impl Error for HttpError {} + +impl From for io::Error { + fn from(val: HttpError) -> Self { + io::Error::other(val.to_string()) + } +} + +impl From for HttpError { + fn from(value: httparse::Error) -> Self { + HttpError { + kind: match value { + _ => HttpErrorKind::BadRequest, + }, + } + } +} + +pub struct FeConfig { + bind_address: SocketAddr, + read_timeout: Duration, + write_timeout: Duration, +} + +impl FeConfig { + pub fn init( + bind_address: A, + read_timeout: Duration, + write_timeout: Duration, + ) -> Result> { + Ok(FeConfig { + bind_address: bind_address.to_socket_addrs()?.collect::>()[0], + read_timeout, + write_timeout, + }) + } +} + +pub struct FeStorage { + listener: TcpListener, + // TODO: tera template store +} + +impl Frontend {} + +impl Iterator for Frontend { + type Item = Incoming<'static>; + + fn next(&mut self) -> Option { + todo!() + } +} + +impl FrontendImpl for Frontend { + type FeConfig = FeConfig; + type RequestSubject = TcpStream; + type ReplySubject = TcpStream; + + fn init(config: FeConfig) -> Self { + // TODO: load tera templates into FeStorage + Frontend { + storage: self::FeStorage { + listener: TcpListener::bind(config.bind_address).unwrap_or_else(|e| { + eyap!(&e); + exit(1) + }), + }, + config: config, + } + } + + fn handle_request(&self, subj: Self::RequestSubject) -> Result<(), Box> { + subj.set_read_timeout(Some(self.config.read_timeout)) + .and_then(|_| subj.set_write_timeout(Some(self.config.write_timeout)))?; + + let stream_read = BufReader::new(subj); + let mut headers = [httparse::EMPTY_HEADER; 32]; + let mut req = httparse::Request::new(&mut headers); + let buf: &mut Vec = &mut vec![]; + + stream_read.take(8192).read_until(b'\n', buf)?; + let res = req.parse(buf); + + match (res, req) { + // Presumably well-formed enough to get sent off to the route handler + ( + Ok(httparse::Status::Partial), + httparse::Request { + method: Some(method), + path: Some(path), + version: Some(1), + headers: _, + }, + ) => todo!(), + + // Malformed request lines and HTTP/1.1 requests without a Host header + (Ok(httparse::Status::Partial), _) | (Ok(httparse::Status::Complete(_)), _) => { + Err(Box::new(HttpError { + kind: HttpErrorKind::BadRequest, + }) as Box) + } + + // Fatal parsing error; obvious bad request + (Err(e), _) => Err(Box::new(e) as Box), + }?; + + Ok(()) + } + + fn send_reply(&self, subj: Self::ReplySubject) -> Result<(), Box> { + todo!() + } + + fn handle_error(&self, res: Result<(), Box>) { + todo!() + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..c44d80f --- /dev/null +++ b/src/server.rs @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 silt + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This file is part of Mintee. + * + * Mintee is free software: you can redistribute it and/or modify it under the + * terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Mintee is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Mintee. If not, see https://www.gnu.org/licenses/. + */ + +use std::{ + array, + error::Error, + io::{self, BufRead, BufReader, BufWriter, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs}, + num::NonZeroUsize, + os::{linux::net::TcpStreamExt, unix::thread::JoinHandleExt}, + thread::{self, JoinHandle, Thread}, +}; + +use crate::{eyap, yap}; + +pub struct Frontend { + /// Holds data necessary for and private to the implementor. + pub storage: S, + + /// Holds data to be set during initialization. + pub config: C, +} + +pub trait FrontendImpl: Iterator { + type FeConfig: ?Sized; + type RequestSubject: Read; + type ReplySubject: Write; + fn init(storage: Self::FeConfig) -> Self; + fn handle_request(&self, subj: Self::RequestSubject) -> Result<(), Box>; + fn send_reply(&self, subj: Self::ReplySubject) -> Result<(), Box>; + // NOTE: handle_request.or_else(handle_error) + fn handle_error(&self, res: Result<(), Box>); +} + +// TODO: split frontend management code and Frontend trait stuff into diff files + +#[derive(Debug)] +pub struct Pool { + threads: [JoinHandle<()>; N], +} + +impl Pool { + pub fn new() -> Result> { + Ok(Pool { + threads: array::from_fn(|id| { + thread::spawn(move || { + eyap!("started thread #{:?}", id); + }) + }), + }) + } +} diff --git a/src/yapper.rs b/src/yapper.rs new file mode 100644 index 0000000..207145e --- /dev/null +++ b/src/yapper.rs @@ -0,0 +1,77 @@ +pub mod _inner { + macro_rules! _yap_inner { + ($m:expr) => {{ + println!("[{}:{}] {}", file!(), line!(), $m); + }}; + } + pub(crate) use _yap_inner; + + macro_rules! _eyap_inner { + ($m:expr) => {{ + println!("[{}:{}] {}", file!(), line!(), $m); + }}; + } + pub(crate) use _eyap_inner; +} + +#[macro_export] +macro_rules! yap { + ($m:ident) => {{ + let v = $m; + crate::yapper::_inner::_yap_inner!(format!("{}", $m)); + v + }}; + ($f:literal , $m:ident) => {{ + let v = $m; + crate::yapper::_inner::_yap_inner!(format!($f, $m)); + v + }}; + + ($m:literal) => {{ + let v = $m; + crate::yapper::_inner::_yap_inner!(format!($m)); + v + }}; + + ($f:literal , $m:expr) => {{ + let v = $m; + crate::yapper::_inner::_yap_inner!(format!($f, v)); + v + }}; + ($m:expr) => {{ + let v = $m; + crate::yapper::_inner::_yap_inner!(format!("{}", v)); + v + }}; +} + +#[macro_export] +macro_rules! eyap { + ($m:ident) => {{ + let v = $m; + crate::yapper::_inner::_eyap_inner!(format!("{}", $m)); + v + }}; + ($f:literal , $m:ident) => {{ + let v = $m; + crate::yapper::_inner::_eyap_inner!(format!($f, $m)); + v + }}; + + ($m:literal) => {{ + let v = $m; + crate::yapper::_inner::_eyap_inner!(format!($m)); + v + }}; + + ($f:literal , $m:expr) => {{ + let v = $m; + crate::yapper::_inner::_eyap_inner!(format!($f, v)); + v + }}; + ($m:expr) => {{ + let v = $m; + crate::yapper::_inner::_eyap_inner!(format!("{}", v)); + v + }}; +}