diff --git a/Cargo.lock b/Cargo.lock index d6d3126..bb77c6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -191,6 +191,12 @@ dependencies = [ "syn", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -200,6 +206,82 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -432,6 +514,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -522,10 +613,13 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" name = "mintee" version = "0.0.1" dependencies = [ + "futures", "git2", "httparse", + "itertools", "serde", "tera", + "typed-path", ] [[package]] @@ -640,6 +734,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkg-config" version = "0.3.32" @@ -823,6 +923,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "slug" version = "0.1.6" @@ -919,6 +1025,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.18.0" diff --git a/Cargo.toml b/Cargo.toml index 8414fec..c0a6d3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,6 @@ serde = { version = "1.0.219", default-features = false, features = ["derive"] } #markdown = "1.0.0" tera = { version = "1.20.0", default-features = false, features = ["builtins"] } httparse = "1.10.1" -futures = { version = "0.3.31", default-features = false, features = ["std", "thread-pool", "executor"] } -tower-service = "0.3.3" +futures = { version = "0.3.32", default-features = false, features = ["std", "thread-pool", "executor"] } +typed-path = "=0.12.3" # pinned to avoid slop +itertools = "0.14.0" diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/src/gem_fe.rs b/src/bin/frontend/gem_fe/mod.rs similarity index 100% rename from src/gem_fe.rs rename to src/bin/frontend/gem_fe/mod.rs diff --git a/src/bin/frontend/http_fe/mod.rs b/src/bin/frontend/http_fe/mod.rs new file mode 100644 index 0000000..387e38a --- /dev/null +++ b/src/bin/frontend/http_fe/mod.rs @@ -0,0 +1,465 @@ +/* + * Copyright (c) 2025, 2026 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::{self}; +use itertools::Itertools; +use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath, Utf8UnixPathBuf}; +use std::{ + error::Error, fmt, io::{self, BufRead, BufReader, Read}, net::{Incoming, SocketAddr, TcpListener, TcpStream, ToSocketAddrs}, ops::Deref, pin::Pin, process::exit, str::FromStr, time::Duration +}; + +use mintee::util::yapper::{yap, eyap}; + +pub use super::manager::{Frontend, FrontendImpl}; + +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum HttpMethod { + GET, + POST, + HEAD, + PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH, + Unknown, +} + +impl From<&str> for HttpMethod { + fn from(val: &str) -> Self { + use HttpMethod::*; + match val { + "GET" => GET, + "POST" => POST, + "HEAD" => HEAD, + "PUT" => PUT, + "DELETE" => DELETE, + "CONNECT" => CONNECT, + "OPTIONS" => OPTIONS, + "TRACE" => TRACE, + "PATCH" => PATCH, + _ => Unknown, + } + } +} + +impl From for &'static str { + fn from(val: HttpMethod) -> Self { + use HttpMethod::*; + match val { + GET => "GET", + POST => "POST", + HEAD => "HEAD", + PUT => "PUT", + DELETE => "DELETE", + CONNECT => "CONNECT", + OPTIONS => "OPTIONS", + TRACE => "TRACE", + PATCH => "PATCH", + Unknown => "?", + } + } +} + +impl From for HttpMethod { + fn from(val: String) -> Self { + use HttpMethod::*; + match val.as_str() { + "GET" => GET, + "POST" => POST, + "HEAD" => HEAD, + "PUT" => PUT, + "DELETE" => DELETE, + "CONNECT" => CONNECT, + "OPTIONS" => OPTIONS, + "TRACE" => TRACE, + "PATCH" => PATCH, + _ => Unknown, + } + } +} + +impl From for String { + fn from(val: HttpMethod) -> Self { + use HttpMethod::*; + match val { + GET => "GET".to_string(), + POST => "POST".to_string(), + HEAD => "HEAD".to_string(), + PUT => "PUT".to_string(), + DELETE => "DELETE".to_string(), + CONNECT => "CONNECT".to_string(), + OPTIONS => "OPTIONS".to_string(), + TRACE => "TRACE".to_string(), + PATCH => "PATCH".to_string(), + Unknown => "?".to_string(), + } + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum ResponseStatus { + Okay, + Created, + MovedPermanently { + location: String, + }, + SeeOther { + location: String, + }, + TemporaryRedirect { + location: String, + }, + PermanentRedirect { + location: String, + }, + BadRequest, + Unauthorized, + Forbidden, + NotFound, + MethodNotAllowed { + allow: Vec, + }, + UriTooLong, + ImATeapot, + InternalServerError, + NotImplemented, + HttpVersionNotSupported, +} + +impl ResponseStatus { + fn as_code(&self) -> usize { + use ResponseStatus::*; + match self { + Okay => 200, + Created => 201, + MovedPermanently { .. } => 301, + SeeOther { .. } => 303, + TemporaryRedirect { .. } => 307, + PermanentRedirect { .. } => 308, + BadRequest => 400, + Unauthorized => 401, + Forbidden => 403, + NotFound => 404, + MethodNotAllowed { .. } => 405, + UriTooLong => 414, + ImATeapot => 418, + InternalServerError => 500, + NotImplemented => 501, + HttpVersionNotSupported => 505, + } + } + + fn as_description(&self) -> &'static str { + use ResponseStatus::*; + match self { + Okay => "OK", + Created => "Created", + MovedPermanently { .. } => "Moved Permanently", + SeeOther { .. } => "See Other", + TemporaryRedirect { .. } => "Temporary Redirect", + PermanentRedirect { .. } => "Permanent Redirect", + BadRequest => "Bad Request", + Unauthorized => "Unauthorized", + Forbidden => "Forbidden", + NotFound => "Not Found", + 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)] +pub struct HttpError { + kind: ResponseStatus, +} + +impl HttpError { + pub fn new(kind: ResponseStatus) -> Self { + Self { kind } + } +} + +impl fmt::Display for HttpError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "HTTP/1.1 {} {}", + self.kind.as_code(), + self.kind.as_description() + ) + } +} + +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(_: httparse::Error) -> Self { + HttpError::new(ResponseStatus::BadRequest) + } +} + +#[derive(Debug, Clone)] +pub struct Response<'a> { + pub status: ResponseStatus, + pub headers: Vec<(&'a str, String)>, + pub body: Option<&'a [u8]>, +} + +impl<'a> From> for Vec { + fn from(val: Response<'a>) -> Self { + [ + "HTTP/1.1 ".as_bytes(), + val.status.as_code().to_string().as_bytes(), + b" ", + val.status.as_description().as_bytes(), + b"\r\n", + &val.headers.into_iter().fold( + Default::default(), + |mut acc: Vec, e: (&str, String)| -> Vec { + acc.append(&mut [e.0.as_bytes(), b": ", e.1.as_bytes(), b"\r\n"].concat()); + acc + } + ), + b"\r\n", + val.body.unwrap_or_default(), + ].concat() + } +} + +impl<'a> From for Response<'a> { + fn from(err: HttpError) -> Self { + let status = err.kind.clone(); + let headers = match err.kind { + ResponseStatus::MovedPermanently { location } + | ResponseStatus::SeeOther { location } + | ResponseStatus::TemporaryRedirect { location } + | ResponseStatus::PermanentRedirect { location } => vec![("location", location)], + ResponseStatus::MethodNotAllowed { allow } => vec![( + "allow", + allow.iter().map(|x| Into::::into(*x)).join(", ") + )], + _ => vec![], + }; + Response { + status, + headers, + body: None + } + } +} + +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 { + fn router( + method: HttpMethod, + path: Utf8UnixPathBuf, + params: Option>, + headers: &[httparse::Header], + ) -> Result< as FrontendImpl>::Response, Box> { + use HttpMethod::*; + use ResponseStatus::*; + // unwrapping is safe here because the resource path it came from is a valid UTF-8 &str + match (method, path.components().map(|c| c.as_str()).collect::>().deref(), params, headers) { + (method, ["/", "index.html"], _, _) => { + if matches!(method, GET) { + Ok(Response { + status: ResponseStatus::Okay, + headers: vec![("x-test1", "test1".to_string()), ("x-test2", "test2".to_string())], + body: Some(b"totally cool and swag homepage"), + }.into()) + } else { + Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] }))) + } + } + (method, ["/", "login"], _, _) => { + if matches!(method, GET | POST) { + todo!() + } else { + Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET, POST] }))) + } + } + // oh how i long for inline const patterns + (method, ["/", user], _, _) if let Some(user) = user.strip_prefix('~') => { + if matches!(method, GET) { + todo!() + } else { + Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] }))) + } + } + (method, ["/", user, repo], _, _) if let Some(user) = user.strip_prefix('~') => { + if matches!(method, GET) { + todo!() + } else { + Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] }))) + } + } + (method, ["/", project], _, _) if let Some(project) = project.strip_prefix('+') => { + if matches!(method, GET) { + todo!() + } else { + Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] }))) + } + } + (method, ["/", project, repo], _, _) if let Some(project) = project.strip_prefix('+') => { + if matches!(method, GET) { + todo!() + } else { + Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] }))) + } + } + _ => Err(Box::new(HttpError::new(ResponseStatus::NotFound))), + } + } +} + +impl Iterator for Frontend { + type Item = Incoming<'static>; + + fn next(&mut self) -> Option { + todo!() + } +} + +impl FrontendImpl for Frontend { + type FeConfig = FeConfig; + type Request = TcpStream; + type Response = Vec; + + 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::Request) -> Result> { + 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![]; + + // TODO: validate more of the request before sending to the router + stream_read.take(8192).read_until(b'\n', buf)?; + let res = req.parse(buf); + + Ok(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, + }, + ) => { + // separate path containing get params into path and kv vec + let (path, params) = path.split_once("?").map_or_else( + || (Utf8UnixPathBuf::from_str(path).unwrap(), None), + |(path, args)| { + ( + Utf8UnixPathBuf::from_str(path).unwrap(), + Some( + args.split('&') + .filter_map(|e| e.split_once('=')) + .collect::>(), + ), + ) + }, + ); + if path.is_absolute() { + // context-valid lexical normalization without da feature + let path = Utf8UnixPathBuf::from_iter(path.components().try_fold(Vec::::new(), |mut acc, item| -> Result>, Box> { + match item { + Utf8UnixComponent::CurDir => Ok(acc), + Utf8UnixComponent::RootDir => {acc.push(item); Ok(acc)}, + Utf8UnixComponent::Normal(_) => {acc.push(item); Ok(acc)}, + Utf8UnixComponent::ParentDir => {acc.pop_if(|c| c != &Utf8UnixComponent::RootDir); Ok(acc)}, + } + })?); + + Self::router(method.into(), path, params, headers) + } else { + Err(Box::new(HttpError::new(ResponseStatus::BadRequest)) as Box) + } + } + + // Malformed request lines and HTTP/1.1 requests without a Host header + (Ok(httparse::Status::Partial), _) | (Ok(httparse::Status::Complete(_)), _) => { + Err(Box::new(HttpError::new(ResponseStatus::BadRequest)) as Box) + } + + // Fatal parsing error; obvious bad request + (Err(e), _) => Err(Box::new(e) as Box), + }?) + } + + fn handle_error(&mut self, res: Result>) -> Vec { + todo!() + } +} diff --git a/src/frontend.rs b/src/bin/frontend/main.rs similarity index 54% rename from src/frontend.rs rename to src/bin/frontend/main.rs index 2485cdd..6d0f2b6 100644 --- a/src/frontend.rs +++ b/src/bin/frontend/main.rs @@ -18,10 +18,12 @@ * along with Mintee. If not, see https://www.gnu.org/licenses/. */ -use std::{error::Error, time::Duration}; +use std::{error::Error, thread::available_parallelism, time::Duration}; -mod server; -use server::Pool; +use futures::{self, channel::mpsc, executor::ThreadPool}; + +mod manager; +use manager::Pool; mod http_fe; @@ -29,12 +31,32 @@ use http_fe::FrontendImpl; mod gem_fe; -mod yapper; +// mod util; +// use crate::; +use mintee::util::yapper::eyap; + 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 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(); + // let pool = Pool::<32>::new(); + + + let pool = ThreadPool::builder() + .pool_size(available_parallelism()?.into()) // TODO: or optional value from config + .name_prefix("mintfe-worker:") + .after_start(|i| { + eyap!("Spawned mintfe-worker:{i}"); + }) + .before_stop(|i| { + eyap!("Stopping mintfe-worker:{i}"); + }) + .create()?; + + // let (tx, rx) = mpsc::unbounded::(); + + + Ok(()) } diff --git a/src/server.rs b/src/bin/frontend/manager.rs similarity index 72% rename from src/server.rs rename to src/bin/frontend/manager.rs index 8c0a941..2afba84 100644 --- a/src/server.rs +++ b/src/bin/frontend/manager.rs @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 silt + * Copyright (c) 2025, 2026 silt * SPDX-License-Identifier: AGPL-3.0-or-later * * This file is part of Mintee. @@ -24,8 +24,9 @@ use std::{ io::{Read, Write}, thread::{self, JoinHandle}, }; +use futures::executor; -use crate::{eyap, yap}; +use mintee::util::yapper::{yap, eyap}; pub struct Frontend { /// Holds data necessary for and private to the implementor. @@ -35,15 +36,15 @@ pub struct Frontend { pub config: C, } -pub trait FrontendImpl: Iterator { +pub trait FrontendImpl: Iterator where Request: Read, Self::Response: Into> { type FeConfig: ?Sized; - type RequestSubject: Read; - type ReplySubject: Write; + type Request: Read; + type Response: 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>); + fn handle_request(&self, subj: Request) -> Result>; + // fn send_reply(&self, subj: Self::Response) -> Result<(), Box>; + // NOTE: handle_request().or_else(handle_error()) + fn handle_error(&mut self, res: Result>) -> Self::Response; } // TODO: split frontend management code and Frontend trait stuff into diff files diff --git a/src/http_fe.rs b/src/http_fe.rs deleted file mode 100644 index 0b2ba28..0000000 --- a/src/http_fe.rs +++ /dev/null @@ -1,332 +0,0 @@ -/* - * 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}, ops::Deref, path::{Component, PathBuf}, process::exit, str::FromStr, time::Duration -}; - -use crate::{eyap, yap}; - -pub use super::server::{Frontend, FrontendImpl}; - -#[derive(Debug, Clone, Copy)] -#[non_exhaustive] -pub enum HttpMethod { - GET, - POST, - HEAD, -} - -impl TryFrom<&str> for HttpMethod { - type Error = HttpError; - fn try_from(val: &str) -> Result { - use HttpMethod::*; - match val { - "GET" => Ok(GET), - "POST" => Ok(POST), - "HEAD" => Ok(HEAD), - _ => Err(HttpError { - kind: ResponseCode::MethodNotAllowed, - }), - } - } -} - -impl From for &str { - fn from(val: HttpMethod) -> Self { - use HttpMethod::*; - match val { - GET => "GET", - POST => "POST", - HEAD => "HEAD", - } - } -} - -#[derive(Debug, Clone, Copy)] -#[non_exhaustive] -pub enum ResponseCode { - Okay, - Created, - MovedPermanently, - SeeOther, - TemporaryRedirect, - PermanentRedirect, - BadRequest, - Unauthorized, - Forbidden, - NotFound, - MethodNotAllowed, - UriTooLong, - ImATeapot, - InternalServerError, - NotImplemented, - HttpVersionNotSupported, -} - -impl From for usize { - fn from(val: ResponseCode) -> Self { - use ResponseCode::*; - match val { - Okay => 200, - Created => 201, - MovedPermanently => 301, - SeeOther => 303, - TemporaryRedirect => 307, - PermanentRedirect => 308, - 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: ResponseCode) -> Self { - use ResponseCode::*; - match val { - Okay => "OK", - Created => "Created", - MovedPermanently => "Moved Permanently", - SeeOther => "See Other", - TemporaryRedirect => "Temporary Redirect", - PermanentRedirect => "Permanent Redirect", - 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: ResponseCode, -} - -impl HttpError { - pub fn new(kind: ResponseCode) -> Self { - Self { kind } - } -} - -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 { - _ => ResponseCode::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 { - fn router( - method: HttpMethod, - path: PathBuf, - params: Option>, - headers: &[httparse::Header], - ) -> Result<(), Box> { - use HttpMethod::*; - // unwrapping is safe here because the resource path it came from is a valid UTF-8 &str - match (method, path.components().map(|c| c.as_os_str().to_str().unwrap()).collect::>().deref(), params, headers) { - (GET, ["/", "index.html"], _, _) => { - todo!() - } - (GET, ["/", "login.html"], _, _) => { - todo!() - } - // can be cleaned up with rust/rust-lang #76001 or maybe #87599 - (GET, ["/", user], _, _) if user.starts_with('~') => { - todo!() - } - (GET, ["/", user, repo], _, _) if user.starts_with('~') => { - todo!() - } - (GET, ["/", project], _, _) if project.starts_with('+') => { - todo!() - } - (GET, ["/", project, repo], _, _) if project.starts_with('+') => { - todo!() - } - _ => Err(Box::new(HttpError::new(ResponseCode::NotFound))), - } - } -} - -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![]; - - // TODO: parse the rest of the request before sending to the router - 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, - }, - ) => { - // separate path containing get params into path and kv vec - let (path, params) = path.split_once("?").map_or_else( - || (PathBuf::from_str(path).unwrap(), None), - |(path, args)| { - ( - PathBuf::from_str(path).unwrap(), - Some( - args.split('&') - .filter_map(|e| e.split_once('=')) - .collect::>(), - ), - ) - }, - ); - if path.is_absolute() { - // context-valid lexical normalization without da feature - let path = PathBuf::from_iter(path.components().try_fold(Vec::::new(), |mut acc, item| -> Result>, Box> { - match item { - Component::CurDir => Ok(acc), - Component::RootDir => {acc.push(item); Ok(acc)}, - Component::Normal(_) => {acc.push(item); Ok(acc)}, - Component::ParentDir => {acc.pop_if(|c| c != &Component::RootDir); Ok(acc)}, - Component::Prefix(_) => Err(Box::new(HttpError::new(ResponseCode::BadRequest)) as Box), - } - })?); - Self::router(method.try_into()?, path, params, headers) - } else { - Err(Box::new(HttpError::new(ResponseCode::BadRequest)) as Box) - } - } - - // Malformed request lines and HTTP/1.1 requests without a Host header - (Ok(httparse::Status::Partial), _) | (Ok(httparse::Status::Complete(_)), _) => { - Err(Box::new(HttpError::new(ResponseCode::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/lib.rs b/src/lib.rs new file mode 100644 index 0000000..812d1ed --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod util; diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..901d859 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub mod yapper; diff --git a/src/util/yapper.rs b/src/util/yapper.rs new file mode 100644 index 0000000..7c29507 --- /dev/null +++ b/src/util/yapper.rs @@ -0,0 +1,114 @@ +/* + * 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/. + */ + +pub mod _inner { + #[doc(hidden)] + #[macro_export] + macro_rules! _yap_inner { + ($m:expr) => {{ + println!("[{}:{}] {}", file!(), line!(), $m); + }}; + } + pub use _yap_inner; + + #[doc(hidden)] + #[macro_export] + macro_rules! _eyap_inner { + ($m:expr) => {{ + eprintln!("[{}:{}] {}", file!(), line!(), $m); + }}; + } + pub use _eyap_inner; +} + +macro_rules! mk_yap {() => ( + #[macro_export] + macro_rules! yap { + ($m:ident) => {{ + let v = $m; + $crate::util::yapper::_inner::_yap_inner!(format!("{}", $m)); + v + }}; + ($f:literal , $m:ident) => {{ + let v = $m; + $crate::util::yapper::_inner::_yap_inner!(format!($f, $m)); + v + }}; + + ($m:literal) => {{ + let v = $m; + $crate::util::yapper::_inner::_yap_inner!(format!($m)); + v + }}; + + ($f:literal , $m:expr) => {{ + let v = $m; + $crate::util::yapper::_inner::_yap_inner!(format!($f, v)); + v + }}; + ($m:expr) => {{ + let v = $m; + $crate::util::yapper::_inner::_yap_inner!(format!("{}", v)); + v + }}; + } + + pub use yap; +)} + +mk_yap!(); + + +macro_rules! mk_eyap {() => ( + #[macro_export] + macro_rules! eyap { + ($m:ident) => {{ + let v = $m; + $crate::util::yapper::_inner::_eyap_inner!(format!("{}", $m)); + v + }}; + ($f:literal , $m:ident) => {{ + let v = $m; + $crate::util::yapper::_inner::_eyap_inner!(format!($f, $m)); + v + }}; + + ($m:literal) => {{ + let v = $m; + $crate::util::yapper::_inner::_eyap_inner!(format!($m)); + v + }}; + + ($f:literal , $m:expr) => {{ + let v = $m; + $crate::util::yapper::_inner::_eyap_inner!(format!($f, v)); + v + }}; + ($m:expr) => {{ + let v = $m; + $crate::util::yapper::_inner::_eyap_inner!(format!("{}", v)); + v + }}; + } + + pub use eyap; +)} + +mk_eyap!(); diff --git a/src/yapper.rs b/src/yapper.rs deleted file mode 100644 index decfe7f..0000000 --- a/src/yapper.rs +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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/. - */ - -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 - }}; -}