routerless http frontend and frontend manager

This commit is contained in:
silt! 2025-08-08 02:46:35 +00:00
parent d0e556ec3c
commit 20b113d30d
Signed by: silt
GPG Key ID: 7FB50C8CADDFAB20
6 changed files with 417 additions and 2 deletions

11
Cargo.lock generated
View File

@ -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",

View File

@ -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"

40
src/frontend.rs Normal file
View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2025 silt <silt@tebibyte.media>
* 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<dyn Error>> {
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(())
}

217
src/http_fe.rs Normal file
View File

@ -0,0 +1,217 @@
/*
* Copyright (c) 2025 silt <silt@tebibyte.media>
* 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<HttpErrorKind> 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<HttpErrorKind> 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::<usize>::into(self.kind),
Into::<&str>::into(self.kind)
)
}
}
impl Error for HttpError {}
impl From<HttpError> for io::Error {
fn from(val: HttpError) -> Self {
io::Error::other(val.to_string())
}
}
impl From<httparse::Error> 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<A: ToSocketAddrs>(
bind_address: A,
read_timeout: Duration,
write_timeout: Duration,
) -> Result<Self, Box<dyn Error>> {
Ok(FeConfig {
bind_address: bind_address.to_socket_addrs()?.collect::<Vec<_>>()[0],
read_timeout,
write_timeout,
})
}
}
pub struct FeStorage {
listener: TcpListener,
// TODO: tera template store
}
impl Frontend<FeStorage, FeConfig> {}
impl Iterator for Frontend<FeStorage, FeConfig> {
type Item = Incoming<'static>;
fn next(&mut self) -> Option<Self::Item> {
todo!()
}
}
impl FrontendImpl for Frontend<FeStorage, FeConfig> {
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<dyn Error>> {
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<u8> = &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<dyn Error>)
}
// Fatal parsing error; obvious bad request
(Err(e), _) => Err(Box::new(e) as Box<dyn Error>),
}?;
Ok(())
}
fn send_reply(&self, subj: Self::ReplySubject) -> Result<(), Box<dyn Error>> {
todo!()
}
fn handle_error(&self, res: Result<(), Box<dyn Error>>) {
todo!()
}
}

69
src/server.rs Normal file
View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2025 silt <silt@tebibyte.media>
* 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<S, C> {
/// 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<dyn Error>>;
fn send_reply(&self, subj: Self::ReplySubject) -> Result<(), Box<dyn Error>>;
// NOTE: handle_request.or_else(handle_error)
fn handle_error(&self, res: Result<(), Box<dyn Error>>);
}
// TODO: split frontend management code and Frontend trait stuff into diff files
#[derive(Debug)]
pub struct Pool<const N: usize> {
threads: [JoinHandle<()>; N],
}
impl<const N: usize> Pool<N> {
pub fn new() -> Result<Self, Box<dyn Error>> {
Ok(Pool {
threads: array::from_fn(|id| {
thread::spawn(move || {
eyap!("started thread #{:?}", id);
})
}),
})
}
}

77
src/yapper.rs Normal file
View File

@ -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
}};
}