pushing near-usable frontend progress before attempting full refactor

This commit is contained in:
silt! 2025-08-10 01:17:44 +00:00
parent caa6f98caa
commit 2229656a20
Signed by: silt
GPG Key ID: 7FB50C8CADDFAB20
3 changed files with 157 additions and 26 deletions

19
src/gem_fe.rs Normal file
View File

@ -0,0 +1,19 @@
/*
* 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/.
*/

View File

@ -20,12 +20,7 @@
use httparse;
use std::{
error::Error,
fmt,
io::{self, BufRead, BufReader, Read},
net::{Incoming, SocketAddr, TcpListener, TcpStream, ToSocketAddrs},
process::exit,
time::Duration,
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};
@ -33,7 +28,48 @@ use crate::{eyap, yap};
pub use super::server::{Frontend, FrontendImpl};
#[derive(Debug, Clone, Copy)]
pub enum HttpErrorKind {
#[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,
@ -46,10 +82,16 @@ pub enum HttpErrorKind {
HttpVersionNotSupported,
}
impl From<HttpErrorKind> for usize {
fn from(val: HttpErrorKind) -> Self {
use HttpErrorKind::*;
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,
@ -64,10 +106,16 @@ impl From<HttpErrorKind> for usize {
}
}
impl From<HttpErrorKind> for &str {
fn from(val: HttpErrorKind) -> Self {
use HttpErrorKind::*;
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",
@ -84,7 +132,13 @@ impl From<HttpErrorKind> for &str {
#[derive(Debug, Clone, Copy)]
pub struct HttpError {
kind: HttpErrorKind,
kind: ResponseCode,
}
impl HttpError {
pub fn new(kind: ResponseCode) -> Self {
Self { kind }
}
}
impl fmt::Display for HttpError {
@ -110,7 +164,7 @@ impl From<httparse::Error> for HttpError {
fn from(value: httparse::Error) -> Self {
HttpError {
kind: match value {
_ => HttpErrorKind::BadRequest,
_ => ResponseCode::BadRequest,
},
}
}
@ -141,7 +195,39 @@ pub struct FeStorage {
// TODO: tera template store
}
impl Frontend<FeStorage, FeConfig> {}
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>;
@ -178,6 +264,7 @@ impl FrontendImpl for Frontend<FeStorage, FeConfig> {
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);
@ -189,15 +276,43 @@ impl FrontendImpl for Frontend<FeStorage, FeConfig> {
method: Some(method),
path: Some(path),
version: Some(1),
headers: _,
headers,
},
) => todo!(),
) => {
// 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 {
kind: HttpErrorKind::BadRequest,
}) as Box<dyn Error>)
Err(Box::new(HttpError::new(ResponseCode::BadRequest)) as Box<dyn Error>)
}
// Fatal parsing error; obvious bad request

View File

@ -21,11 +21,8 @@
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},
io::{Read, Write},
thread::{self, JoinHandle},
};
use crate::{eyap, yap};