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 httparse;
use std::{ use std::{
error::Error, 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
fmt,
io::{self, BufRead, BufReader, Read},
net::{Incoming, SocketAddr, TcpListener, TcpStream, ToSocketAddrs},
process::exit,
time::Duration,
}; };
use crate::{eyap, yap}; use crate::{eyap, yap};
@ -33,7 +28,48 @@ use crate::{eyap, yap};
pub use super::server::{Frontend, FrontendImpl}; pub use super::server::{Frontend, FrontendImpl};
#[derive(Debug, Clone, Copy)] #[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, BadRequest,
Unauthorized, Unauthorized,
Forbidden, Forbidden,
@ -46,10 +82,16 @@ pub enum HttpErrorKind {
HttpVersionNotSupported, HttpVersionNotSupported,
} }
impl From<HttpErrorKind> for usize { impl From<ResponseCode> for usize {
fn from(val: HttpErrorKind) -> Self { fn from(val: ResponseCode) -> Self {
use HttpErrorKind::*; use ResponseCode::*;
match val { match val {
Okay => 200,
Created => 201,
MovedPermanently => 301,
SeeOther => 303,
TemporaryRedirect => 307,
PermanentRedirect => 308,
BadRequest => 400, BadRequest => 400,
Unauthorized => 401, Unauthorized => 401,
Forbidden => 403, Forbidden => 403,
@ -64,10 +106,16 @@ impl From<HttpErrorKind> for usize {
} }
} }
impl From<HttpErrorKind> for &str { impl From<ResponseCode> for &str {
fn from(val: HttpErrorKind) -> Self { fn from(val: ResponseCode) -> Self {
use HttpErrorKind::*; use ResponseCode::*;
match val { match val {
Okay => "OK",
Created => "Created",
MovedPermanently => "Moved Permanently",
SeeOther => "See Other",
TemporaryRedirect => "Temporary Redirect",
PermanentRedirect => "Permanent Redirect",
BadRequest => "Bad Request", BadRequest => "Bad Request",
Unauthorized => "Unauthorized", Unauthorized => "Unauthorized",
Forbidden => "Forbidden", Forbidden => "Forbidden",
@ -84,7 +132,13 @@ impl From<HttpErrorKind> for &str {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct HttpError { pub struct HttpError {
kind: HttpErrorKind, kind: ResponseCode,
}
impl HttpError {
pub fn new(kind: ResponseCode) -> Self {
Self { kind }
}
} }
impl fmt::Display for HttpError { impl fmt::Display for HttpError {
@ -110,7 +164,7 @@ impl From<httparse::Error> for HttpError {
fn from(value: httparse::Error) -> Self { fn from(value: httparse::Error) -> Self {
HttpError { HttpError {
kind: match value { kind: match value {
_ => HttpErrorKind::BadRequest, _ => ResponseCode::BadRequest,
}, },
} }
} }
@ -141,7 +195,39 @@ pub struct FeStorage {
// TODO: tera template store // 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> { impl Iterator for Frontend<FeStorage, FeConfig> {
type Item = Incoming<'static>; type Item = Incoming<'static>;
@ -178,6 +264,7 @@ impl FrontendImpl for Frontend<FeStorage, FeConfig> {
let mut req = httparse::Request::new(&mut headers); let mut req = httparse::Request::new(&mut headers);
let buf: &mut Vec<u8> = &mut vec![]; 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)?; stream_read.take(8192).read_until(b'\n', buf)?;
let res = req.parse(buf); let res = req.parse(buf);
@ -189,15 +276,43 @@ impl FrontendImpl for Frontend<FeStorage, FeConfig> {
method: Some(method), method: Some(method),
path: Some(path), path: Some(path),
version: Some(1), 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 // Malformed request lines and HTTP/1.1 requests without a Host header
(Ok(httparse::Status::Partial), _) | (Ok(httparse::Status::Complete(_)), _) => { (Ok(httparse::Status::Partial), _) | (Ok(httparse::Status::Complete(_)), _) => {
Err(Box::new(HttpError { Err(Box::new(HttpError::new(ResponseCode::BadRequest)) as Box<dyn Error>)
kind: HttpErrorKind::BadRequest,
}) as Box<dyn Error>)
} }
// Fatal parsing error; obvious bad request // Fatal parsing error; obvious bad request

View File

@ -21,11 +21,8 @@
use std::{ use std::{
array, array,
error::Error, error::Error,
io::{self, BufRead, BufReader, BufWriter, Read, Write}, io::{Read, Write},
net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs}, thread::{self, JoinHandle},
num::NonZeroUsize,
os::{linux::net::TcpStreamExt, unix::thread::JoinHandleExt},
thread::{self, JoinHandle, Thread},
}; };
use crate::{eyap, yap}; use crate::{eyap, yap};