diff --git a/src/gem_fe.rs b/src/gem_fe.rs new file mode 100644 index 0000000..8212e55 --- /dev/null +++ b/src/gem_fe.rs @@ -0,0 +1,19 @@ +/* + * 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/. + */ diff --git a/src/http_fe.rs b/src/http_fe.rs index e6ffb48..0b2ba28 100644 --- a/src/http_fe.rs +++ b/src/http_fe.rs @@ -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 { + 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, @@ -46,10 +82,16 @@ pub enum HttpErrorKind { HttpVersionNotSupported, } -impl From for usize { - fn from(val: HttpErrorKind) -> Self { - use HttpErrorKind::*; +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, @@ -64,10 +106,16 @@ impl From for usize { } } -impl From for &str { - fn from(val: HttpErrorKind) -> Self { - use HttpErrorKind::*; +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", @@ -84,7 +132,13 @@ impl From 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 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 {} +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>; @@ -178,6 +264,7 @@ impl FrontendImpl for Frontend { 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); @@ -189,15 +276,43 @@ impl FrontendImpl for Frontend { 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::>(), + ), + ) + }, + ); + 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 { - kind: HttpErrorKind::BadRequest, - }) as Box) + Err(Box::new(HttpError::new(ResponseCode::BadRequest)) as Box) } // Fatal parsing error; obvious bad request diff --git a/src/server.rs b/src/server.rs index c44d80f..8c0a941 100644 --- a/src/server.rs +++ b/src/server.rs @@ -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};