wip http frontend code
This commit is contained in:
@@ -20,446 +20,461 @@
|
||||
|
||||
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
|
||||
error::Error,
|
||||
fmt::{self},
|
||||
io::{BufRead, BufReader, Read},
|
||||
net::TcpStream,
|
||||
ops::Deref,
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
};
|
||||
use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath, Utf8UnixPathBuf};
|
||||
|
||||
use mintee::util::yapper::{yap, eyap};
|
||||
use mintee::util::yapper::{eyap, yap};
|
||||
|
||||
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,
|
||||
pub enum HttpMethod<'a> {
|
||||
GET,
|
||||
POST,
|
||||
HEAD,
|
||||
PUT,
|
||||
DELETE,
|
||||
CONNECT,
|
||||
OPTIONS,
|
||||
TRACE,
|
||||
PATCH,
|
||||
Other(&'a str),
|
||||
}
|
||||
|
||||
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<'a> From<&'a str> for HttpMethod<'a> {
|
||||
fn from(val: &'a str) -> Self {
|
||||
use HttpMethod::*;
|
||||
match val {
|
||||
"GET" => GET,
|
||||
"POST" => POST,
|
||||
"HEAD" => HEAD,
|
||||
"PUT" => PUT,
|
||||
"DELETE" => DELETE,
|
||||
"CONNECT" => CONNECT,
|
||||
"OPTIONS" => OPTIONS,
|
||||
"TRACE" => TRACE,
|
||||
"PATCH" => PATCH,
|
||||
other => Other(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<'a> From<HttpMethod<'a>> for &'a str {
|
||||
fn from(val: HttpMethod<'a>) -> Self {
|
||||
use HttpMethod::*;
|
||||
match val {
|
||||
GET => "GET",
|
||||
POST => "POST",
|
||||
HEAD => "HEAD",
|
||||
PUT => "PUT",
|
||||
DELETE => "DELETE",
|
||||
CONNECT => "CONNECT",
|
||||
OPTIONS => "OPTIONS",
|
||||
TRACE => "TRACE",
|
||||
PATCH => "PATCH",
|
||||
Other(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<'a> From<&'a String> for HttpMethod<'a> {
|
||||
fn from(val: &'a String) -> Self {
|
||||
use HttpMethod::*;
|
||||
match &**val {
|
||||
"GET" => GET,
|
||||
"POST" => POST,
|
||||
"HEAD" => HEAD,
|
||||
"PUT" => PUT,
|
||||
"DELETE" => DELETE,
|
||||
"CONNECT" => CONNECT,
|
||||
"OPTIONS" => OPTIONS,
|
||||
"TRACE" => TRACE,
|
||||
"PATCH" => PATCH,
|
||||
_ => Other(val),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
impl<'a> From<HttpMethod<'a>> for String {
|
||||
fn from(val: HttpMethod) -> Self {
|
||||
use HttpMethod::*;
|
||||
match val {
|
||||
GET => "GET".to_owned(),
|
||||
POST => "POST".to_owned(),
|
||||
HEAD => "HEAD".to_owned(),
|
||||
PUT => "PUT".to_owned(),
|
||||
DELETE => "DELETE".to_owned(),
|
||||
CONNECT => "CONNECT".to_owned(),
|
||||
OPTIONS => "OPTIONS".to_owned(),
|
||||
TRACE => "TRACE".to_owned(),
|
||||
PATCH => "PATCH".to_owned(),
|
||||
Other(other) => other.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum ResponseStatus {
|
||||
pub enum ResponseStatus<'a> {
|
||||
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,
|
||||
MovedPermanently { location: String },
|
||||
SeeOther { location: String },
|
||||
TemporaryRedirect { location: String },
|
||||
PermanentRedirect { location: String },
|
||||
BadRequest,
|
||||
Unauthorized,
|
||||
Forbidden,
|
||||
NotFound,
|
||||
MethodNotAllowed { allow: Vec<HttpMethod<'a>> },
|
||||
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,
|
||||
}
|
||||
}
|
||||
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",
|
||||
}
|
||||
}
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
fn to_headers(&self) -> Vec<(&'static str, String)> {
|
||||
use ResponseStatus::*;
|
||||
match self {
|
||||
MovedPermanently { location } => vec![("location", location.clone())],
|
||||
SeeOther { location } => vec![("location", location.clone())],
|
||||
TemporaryRedirect { location } => vec![("location", location.clone())],
|
||||
PermanentRedirect { location } => vec![("location", location.clone())],
|
||||
MethodNotAllowed { allow } => vec![(
|
||||
"allow",
|
||||
allow.iter().map(|x| Into::<String>::into(*x)).join(", "),
|
||||
)],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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)
|
||||
}
|
||||
impl fmt::Display for ResponseStatus<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} {}", self.as_code(), self.as_description())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Response<'a> {
|
||||
pub status: ResponseStatus,
|
||||
pub status: Option<ResponseStatus<'a>>,
|
||||
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 {
|
||||
impl Response<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status: None,
|
||||
headers: Default::default(),
|
||||
body: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&Response<'a>> for Vec<u8> {
|
||||
fn from(val: &Response<'a>) -> Self {
|
||||
let status = val
|
||||
.status
|
||||
.as_ref()
|
||||
.unwrap_or(&ResponseStatus::InternalServerError);
|
||||
let headers: &Vec<(&'a str, String)> = val.headers.as_ref();
|
||||
[
|
||||
"HTTP/1.1 ".as_bytes(),
|
||||
val.status.as_code().to_string().as_bytes(),
|
||||
status.as_code().to_string().as_bytes(),
|
||||
b" ",
|
||||
val.status.as_description().as_bytes(),
|
||||
status.as_description().as_bytes(),
|
||||
b"\r\n",
|
||||
&val.headers.into_iter().fold(
|
||||
&headers.iter().chain(&status.to_headers()).fold(
|
||||
Default::default(),
|
||||
|mut acc: Vec<u8>, e: (&str, String)| -> Vec<u8> {
|
||||
|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
|
||||
}
|
||||
}
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FeConfig {
|
||||
bind_address: SocketAddr,
|
||||
read_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
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 fn init(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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Request<'a> {
|
||||
method: HttpMethod<'a>,
|
||||
path: Utf8UnixPathBuf,
|
||||
headers: Vec<(&'a str, &'a str)>,
|
||||
params: Option<Vec<(&'a str, &'a str)>>,
|
||||
}
|
||||
|
||||
trait ReqResp<'a> {
|
||||
fn new(buf: &'a [u8]) -> (Option<Request<'a>>, Response<'a>);
|
||||
fn route(&mut self) -> &Self;
|
||||
}
|
||||
|
||||
impl<'a> ReqResp<'a> for (Request<'a>, Response<'_>) {
|
||||
fn new(buf: &'a [u8]) -> (Option<Request<'a>>, Response<'a>) {
|
||||
let mut headers = [httparse::EMPTY_HEADER; 32];
|
||||
let mut req = httparse::Request::new(&mut headers);
|
||||
let mut resp = Response::default();
|
||||
|
||||
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(
|
||||
|| (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().fold(
|
||||
Vec::<Utf8UnixComponent>::new(),
|
||||
|mut acc, item| -> Vec<Utf8UnixComponent<'_>> {
|
||||
match item {
|
||||
Utf8UnixComponent::CurDir => acc,
|
||||
Utf8UnixComponent::RootDir => { acc.push(item); acc }
|
||||
Utf8UnixComponent::Normal(_) => { acc.push(item); acc }
|
||||
Utf8UnixComponent::ParentDir => { acc.pop_if(|c| c != &Utf8UnixComponent::RootDir); acc }
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
let headers = headers
|
||||
.iter()
|
||||
.filter_map(|h| str::from_utf8(h.value).map(|v| (h.name, v)).ok())
|
||||
.collect();
|
||||
|
||||
(
|
||||
Some(Request {
|
||||
method: method.into(),
|
||||
path,
|
||||
headers,
|
||||
params,
|
||||
}),
|
||||
resp,
|
||||
)
|
||||
} else {
|
||||
resp.status = Some(ResponseStatus::BadRequest);
|
||||
(None, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// Malformed request lines and HTTP/1.1 requests without a Host header
|
||||
(Ok(httparse::Status::Partial), _) | (Ok(httparse::Status::Complete(_)), _) => {
|
||||
resp.status = Some(ResponseStatus::BadRequest);
|
||||
(None, resp)
|
||||
}
|
||||
|
||||
// Fatal parsing error; obvious bad request
|
||||
(Err(e), _) => {
|
||||
eyap!(e);
|
||||
resp.status = Some(ResponseStatus::BadRequest);
|
||||
(None, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn route(&mut self) -> &Self {
|
||||
let (request, response) = self;
|
||||
use HttpMethod::*;
|
||||
use ResponseStatus::*;
|
||||
match (
|
||||
request.method,
|
||||
request.path.components().map(|c| c.as_str()).collect::<Vec<&str>>().deref(),
|
||||
&request.params,
|
||||
&request.headers,
|
||||
) {
|
||||
(method, ["/", "index.html"], _, _) => {
|
||||
if matches!(method, GET) {
|
||||
response.status = Some(ResponseStatus::Okay);
|
||||
response.headers = vec![
|
||||
("x-test1", "test1".to_string()),
|
||||
("x-test2", "test2".to_string()),
|
||||
];
|
||||
response.body = Some(b"totally cool and swag homepage");
|
||||
self
|
||||
} else {
|
||||
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||
self
|
||||
}
|
||||
}
|
||||
(method, ["/", "login"], _, _) => {
|
||||
if matches!(method, GET | POST) {
|
||||
todo!()
|
||||
} else {
|
||||
response.status = Some(MethodNotAllowed {
|
||||
allow: vec![GET, POST],
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
// oh how i long for inline const patterns
|
||||
(method, ["/", user], _, _) if let Some(user) = user.strip_prefix('~') => {
|
||||
if matches!(method, GET) {
|
||||
todo!()
|
||||
} else {
|
||||
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||
self
|
||||
}
|
||||
}
|
||||
(method, ["/", user, repo], _, _) if let Some(user) = user.strip_prefix('~') => {
|
||||
if matches!(method, GET) {
|
||||
todo!()
|
||||
} else {
|
||||
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||
self
|
||||
}
|
||||
}
|
||||
(method, ["/", project], _, _) if let Some(project) = project.strip_prefix('+') => {
|
||||
if matches!(method, GET) {
|
||||
todo!()
|
||||
} else {
|
||||
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||
self
|
||||
}
|
||||
}
|
||||
(method, ["/", project, repo], _, _)
|
||||
if let Some(project) = project.strip_prefix('+') =>
|
||||
{
|
||||
if matches!(method, GET) {
|
||||
todo!()
|
||||
} else {
|
||||
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||
self
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.1.status = Some(NotFound);
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FeStorage {
|
||||
listener: TcpListener,
|
||||
// TODO: tera template store
|
||||
// TODO: tera template cache
|
||||
}
|
||||
|
||||
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 Frontend<FeStorage, FeConfig> {}
|
||||
|
||||
impl FrontendImpl<TcpStream> for Frontend<FeStorage, FeConfig> {
|
||||
type FeConfig = FeConfig;
|
||||
type Request = TcpStream;
|
||||
type Response = Vec<u8>;
|
||||
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 init(config: FeConfig) -> Self {
|
||||
// TODO: load tera templates into FeStorage
|
||||
Frontend {
|
||||
state: self::FeStorage {},
|
||||
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)))?;
|
||||
async fn handle_request(&self, subj: Self::Request) -> Self::Response {
|
||||
subj.set_read_timeout(Some(self.config.read_timeout))
|
||||
.and_then(|_| subj.set_write_timeout(Some(self.config.write_timeout))).unwrap(/* TODO: what do we wanna do here? */);
|
||||
eyap!("handling");
|
||||
let stream_read = BufReader::new(subj);
|
||||
let buf: &mut Vec<u8> = &mut vec![];
|
||||
|
||||
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![];
|
||||
stream_read.take(8192).read_until(b'\n', buf).unwrap(/*TODO*/);
|
||||
let (request, response) = match <(Request, Response)>::new(buf) {
|
||||
(Some(request), response) => (request, response),
|
||||
(None, response) => return (&response).into(),
|
||||
};
|
||||
let mut pair = (request, response);
|
||||
let (_request, response) = pair.route();
|
||||
|
||||
// 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!()
|
||||
}
|
||||
response.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
* This file is part of Mintee.
|
||||
@@ -8,7 +8,7 @@
|
||||
* 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
|
||||
@@ -18,45 +18,51 @@
|
||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
use std::{error::Error, thread::available_parallelism, time::Duration};
|
||||
use std::{error::Error, io::Write, net::TcpListener, process::exit, sync::Arc, thread::available_parallelism, time::Duration};
|
||||
|
||||
use futures::{self, channel::mpsc, executor::ThreadPool};
|
||||
use futures::{channel::mpsc, executor::ThreadPool};
|
||||
|
||||
mod manager;
|
||||
use manager::Pool;
|
||||
|
||||
|
||||
mod http_fe;
|
||||
use http_fe::FrontendImpl;
|
||||
|
||||
mod gem_fe;
|
||||
|
||||
// mod util;
|
||||
// use crate::;
|
||||
use mintee::util::yapper::eyap;
|
||||
|
||||
|
||||
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 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::<i32>();
|
||||
|
||||
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 http_listener = TcpListener::bind("0.0.0.0:8080").unwrap_or_else(|e| {
|
||||
eyap!(&e);
|
||||
exit(1)
|
||||
});
|
||||
eyap!("http_listener bound to tcp 0.0.0.0:8080");
|
||||
|
||||
// let (tx, rx) = mpsc::unbounded::<todo!()>();
|
||||
let http_fe = Arc::new(http_fe::Frontend::init(http_fe::FeConfig::init(Duration::new(2, 0), Duration::new(2, 0))?)) ;
|
||||
eyap!("initialized http_fe");
|
||||
|
||||
|
||||
for mut conn in http_listener.incoming().map(|x| x.unwrap()) {
|
||||
let http_fe = http_fe.clone();
|
||||
pool.spawn_ok(async move {
|
||||
eyap!("incoming request from {}", conn.peer_addr().unwrap());
|
||||
let response = http_fe.handle_request(conn.try_clone().unwrap()).await;
|
||||
let a = conn.write_all(&response);
|
||||
eyap!("handled!");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -19,49 +19,23 @@
|
||||
*/
|
||||
|
||||
use std::{
|
||||
array,
|
||||
error::Error,
|
||||
io::{Read, Write},
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use futures::executor;
|
||||
|
||||
use mintee::util::yapper::{yap, eyap};
|
||||
|
||||
pub struct Frontend<S, C> {
|
||||
/// Holds data necessary for and private to the implementor.
|
||||
pub storage: S,
|
||||
/// Data necessary for and private to the implementor.
|
||||
pub state: S,
|
||||
|
||||
/// Holds data to be set during initialization.
|
||||
/// Data to be set during initialization.
|
||||
pub config: C,
|
||||
}
|
||||
|
||||
pub trait FrontendImpl<Request>: Iterator where Request: Read, Self::Response: Into<Vec<u8>> {
|
||||
pub trait FrontendImpl<Request> where Self::Request: Read, Self::Response: Into<Vec<u8>> {
|
||||
type FeConfig: ?Sized;
|
||||
type Request: Read;
|
||||
type Response: Write;
|
||||
fn init(storage: Self::FeConfig) -> Self;
|
||||
fn handle_request(&self, subj: Request) -> Result<Self::Response, Box<dyn Error>>;
|
||||
// fn send_reply(&self, subj: Self::Response) -> Result<(), Box<dyn Error>>;
|
||||
// NOTE: handle_request().or_else(handle_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
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Pool<const N: usize> {
|
||||
threads: [JoinHandle<()>; N],
|
||||
}
|
||||
|
||||
impl<const N: usize> Pool<N> {
|
||||
pub fn new() -> Result<Self, Box<dyn Error>> {
|
||||
Ok(Pool {
|
||||
threads: array::from_fn(|id| {
|
||||
thread::spawn(move || {
|
||||
eyap!("started thread #{:?}", id);
|
||||
})
|
||||
}),
|
||||
})
|
||||
}
|
||||
fn init(config: Self::FeConfig) -> Self;
|
||||
async fn handle_request(&self, subj: Request) -> Self::Response;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user