partial code reorganization ; http_fe progress
This commit is contained in:
116
Cargo.lock
generated
116
Cargo.lock
generated
@@ -34,9 +34,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.9.1"
|
version = "2.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
@@ -191,6 +191,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -200,6 +206,82 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"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]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -432,6 +514,15 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@@ -522,10 +613,13 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
|||||||
name = "mintee"
|
name = "mintee"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures",
|
||||||
"git2",
|
"git2",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
"itertools",
|
||||||
"serde",
|
"serde",
|
||||||
"tera",
|
"tera",
|
||||||
|
"typed-path",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -640,6 +734,12 @@ dependencies = [
|
|||||||
"siphasher",
|
"siphasher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -823,6 +923,12 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slug"
|
name = "slug"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -919,6 +1025,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-path"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.18.0"
|
version = "1.18.0"
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ serde = { version = "1.0.219", default-features = false, features = ["derive"] }
|
|||||||
#markdown = "1.0.0"
|
#markdown = "1.0.0"
|
||||||
tera = { version = "1.20.0", default-features = false, features = ["builtins"] }
|
tera = { version = "1.20.0", default-features = false, features = ["builtins"] }
|
||||||
httparse = "1.10.1"
|
httparse = "1.10.1"
|
||||||
futures = { version = "0.3.31", default-features = false, features = ["std", "thread-pool", "executor"] }
|
futures = { version = "0.3.32", default-features = false, features = ["std", "thread-pool", "executor"] }
|
||||||
tower-service = "0.3.3"
|
typed-path = "=0.12.3" # pinned to avoid slop
|
||||||
|
itertools = "0.14.0"
|
||||||
|
|||||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
465
src/bin/frontend/http_fe/mod.rs
Normal file
465
src/bin/frontend/http_fe/mod.rs
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025, 2026 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::{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<HttpMethod> 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<String> 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<HttpMethod> 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<HttpMethod>,
|
||||||
|
},
|
||||||
|
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<HttpError> for io::Error {
|
||||||
|
fn from(val: HttpError) -> Self {
|
||||||
|
io::Error::other(val.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<httparse::Error> 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<Response<'a>> for Vec<u8> {
|
||||||
|
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<u8>, e: (&str, String)| -> Vec<u8> {
|
||||||
|
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<HttpError> 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::<String>::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<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> {
|
||||||
|
fn router(
|
||||||
|
method: HttpMethod,
|
||||||
|
path: Utf8UnixPathBuf,
|
||||||
|
params: Option<Vec<(&str, &str)>>,
|
||||||
|
headers: &[httparse::Header],
|
||||||
|
) -> Result<<Frontend<FeStorage, FeConfig> as FrontendImpl<TcpStream>>::Response, Box<dyn Error>> {
|
||||||
|
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::<Vec<&str>>().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<FeStorage, FeConfig> {
|
||||||
|
type Item = Incoming<'static>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrontendImpl<TcpStream> for Frontend<FeStorage, FeConfig> {
|
||||||
|
type FeConfig = FeConfig;
|
||||||
|
type Request = TcpStream;
|
||||||
|
type Response = Vec<u8>;
|
||||||
|
|
||||||
|
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<Self::Response, 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![];
|
||||||
|
|
||||||
|
// 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::<Vec<(&str, &str)>>(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if path.is_absolute() {
|
||||||
|
// context-valid lexical normalization without da feature
|
||||||
|
let path = Utf8UnixPathBuf::from_iter(path.components().try_fold(Vec::<Utf8UnixComponent>::new(), |mut acc, item| -> Result<Vec<Utf8UnixComponent<'_>>, Box<dyn Error>> {
|
||||||
|
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<dyn Error>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<dyn Error>)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal parsing error; obvious bad request
|
||||||
|
(Err(e), _) => Err(Box::new(e) as Box<dyn Error>),
|
||||||
|
}?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_error(&mut self, res: Result<Self::Response, Box<dyn Error>>) -> Vec<u8> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,10 +18,12 @@
|
|||||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
* 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 futures::{self, channel::mpsc, executor::ThreadPool};
|
||||||
use server::Pool;
|
|
||||||
|
mod manager;
|
||||||
|
use manager::Pool;
|
||||||
|
|
||||||
|
|
||||||
mod http_fe;
|
mod http_fe;
|
||||||
@@ -29,12 +31,32 @@ use http_fe::FrontendImpl;
|
|||||||
|
|
||||||
mod gem_fe;
|
mod gem_fe;
|
||||||
|
|
||||||
mod yapper;
|
// mod util;
|
||||||
|
// use crate::;
|
||||||
|
use mintee::util::yapper::eyap;
|
||||||
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
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 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::<todo!()>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 silt <silt@tebibyte.media>
|
* Copyright (c) 2025, 2026 silt <silt@tebibyte.media>
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* This file is part of Mintee.
|
* This file is part of Mintee.
|
||||||
@@ -24,8 +24,9 @@ use std::{
|
|||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
thread::{self, JoinHandle},
|
thread::{self, JoinHandle},
|
||||||
};
|
};
|
||||||
|
use futures::executor;
|
||||||
|
|
||||||
use crate::{eyap, yap};
|
use mintee::util::yapper::{yap, eyap};
|
||||||
|
|
||||||
pub struct Frontend<S, C> {
|
pub struct Frontend<S, C> {
|
||||||
/// Holds data necessary for and private to the implementor.
|
/// Holds data necessary for and private to the implementor.
|
||||||
@@ -35,15 +36,15 @@ pub struct Frontend<S, C> {
|
|||||||
pub config: C,
|
pub config: C,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FrontendImpl: Iterator {
|
pub trait FrontendImpl<Request>: Iterator where Request: Read, Self::Response: Into<Vec<u8>> {
|
||||||
type FeConfig: ?Sized;
|
type FeConfig: ?Sized;
|
||||||
type RequestSubject: Read;
|
type Request: Read;
|
||||||
type ReplySubject: Write;
|
type Response: Write;
|
||||||
fn init(storage: Self::FeConfig) -> Self;
|
fn init(storage: Self::FeConfig) -> Self;
|
||||||
fn handle_request(&self, subj: Self::RequestSubject) -> Result<(), Box<dyn Error>>;
|
fn handle_request(&self, subj: Request) -> Result<Self::Response, Box<dyn Error>>;
|
||||||
fn send_reply(&self, subj: Self::ReplySubject) -> Result<(), Box<dyn Error>>;
|
// fn send_reply(&self, subj: Self::Response) -> Result<(), Box<dyn Error>>;
|
||||||
// NOTE: handle_request.or_else(handle_error)
|
// NOTE: handle_request().or_else(handle_error())
|
||||||
fn handle_error(&self, res: Result<(), Box<dyn Error>>);
|
fn handle_error(&mut self, res: Result<Self::Response, Box<dyn Error>>) -> Self::Response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: split frontend management code and Frontend trait stuff into diff files
|
// TODO: split frontend management code and Frontend trait stuff into diff files
|
||||||
332
src/http_fe.rs
332
src/http_fe.rs
@@ -1,332 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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}, 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<Self, Self::Error> {
|
|
||||||
use HttpMethod::*;
|
|
||||||
match val {
|
|
||||||
"GET" => Ok(GET),
|
|
||||||
"POST" => Ok(POST),
|
|
||||||
"HEAD" => Ok(HEAD),
|
|
||||||
_ => Err(HttpError {
|
|
||||||
kind: ResponseCode::MethodNotAllowed,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HttpMethod> 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<ResponseCode> 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<ResponseCode> 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::<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 {
|
|
||||||
_ => ResponseCode::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> {
|
|
||||||
fn router(
|
|
||||||
method: HttpMethod,
|
|
||||||
path: PathBuf,
|
|
||||||
params: Option<Vec<(&str, &str)>>,
|
|
||||||
headers: &[httparse::Header],
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
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::<Vec<&str>>().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<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![];
|
|
||||||
|
|
||||||
// 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::<Vec<(&str, &str)>>(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if path.is_absolute() {
|
|
||||||
// context-valid lexical normalization without da feature
|
|
||||||
let path = PathBuf::from_iter(path.components().try_fold(Vec::<Component>::new(), |mut acc, item| -> Result<Vec<Component<'_>>, Box<dyn Error>> {
|
|
||||||
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<dyn Error>),
|
|
||||||
}
|
|
||||||
})?);
|
|
||||||
Self::router(method.try_into()?, path, params, headers)
|
|
||||||
} else {
|
|
||||||
Err(Box::new(HttpError::new(ResponseCode::BadRequest)) as Box<dyn Error>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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<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!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
src/lib.rs
Normal file
1
src/lib.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod util;
|
||||||
1
src/util/mod.rs
Normal file
1
src/util/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod yapper;
|
||||||
114
src/util/yapper.rs
Normal file
114
src/util/yapper.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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!();
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user