routerless http frontend and frontend manager
This commit is contained in:
parent
d0e556ec3c
commit
20b113d30d
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
40
src/frontend.rs
Normal 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
217
src/http_fe.rs
Normal 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
69
src/server.rs
Normal 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
77
src/yapper.rs
Normal 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
|
||||
}};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user