static serve refactor

This commit is contained in:
Tyler Murphy 2023-02-01 20:34:22 -05:00
parent 028026bfdc
commit 2026a8f457
16 changed files with 192 additions and 173 deletions

View file

@ -80,7 +80,7 @@ body {
} }
.icons { .icons {
background-image: url('/images/icons.png'); background-image: url('/image/icons.png');
display: inline-block; display: inline-block;
width: 18px; width: 18px;
height: 18px; height: 18px;

View file

@ -33,11 +33,11 @@ function remove(id) {
} }
function pfp(id) { function pfp(id) {
return `<img src="/cdn/avatar?user_id=${id}">` return `<img src="/image/avatar?user_id=${id}">`
} }
function banner(id) { function banner(id) {
return `<img src="/cdn/banner?user_id=${id}" onerror="this.remove()" >` return `<img src="/image/banner?user_id=${id}" onerror="this.remove()" >`
} }
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',

View file

@ -5,7 +5,8 @@ use serde::Deserialize;
use tower_cookies::{Cookie, Cookies}; use tower_cookies::{Cookie, Cookies};
use crate::{ use crate::{
admin, database, database,
public::admin,
types::{ types::{
extract::{AdminUser, Check, CheckResult, Json}, extract::{AdminUser, Check, CheckResult, Json},
http::ResponseCode, http::ResponseCode,

View file

@ -1,54 +0,0 @@
use axum::{extract::Query, response::Response, routing::get, Router, http::StatusCode};
use serde::Deserialize;
use crate::types::http::ResponseCode;
#[derive(Deserialize)]
struct AvatarRequest {
user_id: u64,
}
async fn avatar(params: Option<Query<AvatarRequest>>) -> Response {
let Some(params) = params else {
return ResponseCode::BadRequest.text("Missing query paramaters");
};
let custom = format!("/image/custom/avatar/{}.png", params.user_id);
let default = format!("/image/default/{}.png", params.user_id % 25);
let file = ResponseCode::Success.file(&custom).await;
if file.status() != StatusCode::OK {
return ResponseCode::Success.file(&default).await
}
file
}
#[derive(Deserialize)]
struct BannerRequest {
user_id: u64,
}
async fn banner(params: Option<Query<BannerRequest>>) -> Response {
let Some(params) = params else {
return ResponseCode::BadRequest.text("Missing query paramaters");
};
let custom = format!("/image/custom/banner/{}.png", params.user_id);
let file = ResponseCode::Success.file(&custom).await;
if file.status() != StatusCode::OK {
return ResponseCode::NotFound.text("User does not have a custom banner")
}
file
}
pub fn router() -> Router {
Router::new()
.route("/avatar", get(avatar))
.route("/banner", get(banner))
}

View file

@ -1,14 +1,17 @@
use crate::types::extract::RouterURI; use crate::types::extract::RouterURI;
use axum::{Extension, Router, BoxError, error_handling::HandleErrorLayer}; use axum::{error_handling::HandleErrorLayer, BoxError, Extension, Router};
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer, errors::display_error, key_extractor::SmartIpKeyExtractor}; use tower_governor::{
errors::display_error, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor,
GovernorLayer,
};
pub mod admin; mod admin;
pub mod auth; mod auth;
pub mod pages; mod posts;
pub mod posts; mod users;
pub mod users;
pub mod image; pub use auth::RegistrationRequet;
pub fn router() -> Router { pub fn router() -> Router {
let governor_conf = Box::new( let governor_conf = Box::new(

View file

@ -3,7 +3,11 @@ use crate::types::{
http::ResponseCode, http::ResponseCode,
user::User, user::User,
}; };
use axum::{response::Response, routing::{post, put}, Router}; use axum::{
response::Response,
routing::{post, put},
Router,
};
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -64,7 +68,6 @@ async fn load_self(AuthorizedUser(user): AuthorizedUser) -> Response {
} }
async fn avatar(AuthorizedUser(user): AuthorizedUser, Png(img): Png) -> Response { async fn avatar(AuthorizedUser(user): AuthorizedUser, Png(img): Png) -> Response {
let path = format!("./public/image/custom/avatar/{}.png", user.user_id); let path = format!("./public/image/custom/avatar/{}.png", user.user_id);
if img.save(path).is_err() { if img.save(path).is_err() {
@ -75,7 +78,6 @@ async fn avatar(AuthorizedUser(user): AuthorizedUser, Png(img): Png) -> Response
} }
async fn banner(AuthorizedUser(user): AuthorizedUser, Png(img): Png) -> Response { async fn banner(AuthorizedUser(user): AuthorizedUser, Png(img): Png) -> Response {
let path = format!("./public/image/custom/banner/{}.png", user.user_id); let path = format!("./public/image/custom/banner/{}.png", user.user_id);
if img.save(path).is_err() { if img.save(path).is_err() {

View file

@ -2,7 +2,7 @@ use rusqlite::{OptionalExtension, Row};
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tracing::instrument; use tracing::instrument;
use crate::{api::auth::RegistrationRequet, database, types::user::User}; use crate::{api::RegistrationRequet, database, types::user::User};
pub fn init() -> Result<(), rusqlite::Error> { pub fn init() -> Result<(), rusqlite::Error> {
let sql = " let sql = "

View file

@ -1,38 +1,25 @@
use axum::{ use axum::{
body::HttpBody, body::HttpBody,
http::{Request, StatusCode}, extract::DefaultBodyLimit,
http::Request,
middleware::{self, Next}, middleware::{self, Next},
response::Response, response::Response,
RequestExt, Router, extract::DefaultBodyLimit, RequestExt, Router,
}; };
use std::{net::SocketAddr, process::exit, fs}; use public::console;
use std::{fs, net::SocketAddr, process::exit};
use tower_cookies::CookieManagerLayer; use tower_cookies::CookieManagerLayer;
use tracing::{error, info, metadata::LevelFilter}; use tracing::{error, info, metadata::LevelFilter};
use tracing_subscriber::{ use tracing_subscriber::{
filter::filter_fn, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer, filter::filter_fn, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer,
}; };
use types::{http::ResponseCode, extract::RequestIp}; use types::extract::RequestIp;
use crate::api::{pages, image};
mod admin;
mod api; mod api;
mod console;
mod database; mod database;
mod public;
mod types; mod types;
async fn serve<B>(req: Request<B>, next: Next<B>) -> Response
where
B: Send + Sync + 'static + HttpBody,
{
let uri = req.uri();
let file = ResponseCode::Success.file(&uri.to_string()).await;
if file.status() != StatusCode::OK {
return next.run(req).await;
}
file
}
async fn log<B>(mut req: Request<B>, next: Next<B>) -> Response async fn log<B>(mut req: Request<B>, next: Next<B>) -> Response
where where
B: Send + Sync + 'static + HttpBody, B: Send + Sync + 'static + HttpBody,
@ -47,7 +34,7 @@ where
} }
async fn not_found() -> Response { async fn not_found() -> Response {
ResponseCode::NotFound.file("/404.html").await public::serve("/404.html").await
} }
#[tokio::main] #[tokio::main]
@ -69,16 +56,16 @@ async fn main() {
}; };
fs::create_dir_all("./public/image/custom").expect("Coudn't make custom data directory"); fs::create_dir_all("./public/image/custom").expect("Coudn't make custom data directory");
fs::create_dir_all("./public/image/custom/avatar").expect("Coudn't make custom avatar directory"); fs::create_dir_all("./public/image/custom/avatar")
fs::create_dir_all("./public/image/custom/banner").expect("Coudn't make custom banner directory"); .expect("Coudn't make custom avatar directory");
fs::create_dir_all("./public/image/custom/banner")
.expect("Coudn't make custom banner directory");
let app = Router::new() let app = Router::new()
.fallback(not_found) .fallback(not_found)
.layer(middleware::from_fn(log)) .layer(middleware::from_fn(log))
.layer(middleware::from_fn(serve)) .nest("/", public::router())
.nest("/", pages::router())
.nest("/api", api::router()) .nest("/api", api::router())
.nest("/cdn", image::router())
.layer(CookieManagerLayer::new()) .layer(CookieManagerLayer::new())
.layer(DefaultBodyLimit::max(512_000)); .layer(DefaultBodyLimit::max(512_000));

69
src/public/file.rs Normal file
View file

@ -0,0 +1,69 @@
use axum::{
extract::{Path, Query},
http::StatusCode,
response::Response,
};
use serde::Deserialize;
use crate::types::http::ResponseCode;
use super::console;
pub async fn js(Path(path): Path<String>) -> Response {
let path = format!("/js/{}", path);
super::serve(&path).await
}
pub async fn css(Path(path): Path<String>) -> Response {
let path = format!("/css/{}", path);
super::serve(&path).await
}
pub async fn fonts(Path(path): Path<String>) -> Response {
let path = format!("/fonts/{}", path);
super::serve(&path).await
}
pub async fn image(Path(path): Path<String>) -> Response {
let path = format!("/image/{}", path);
super::serve(&path).await
}
#[derive(Deserialize)]
pub struct AvatarRequest {
user_id: u64,
}
pub async fn avatar(params: Option<Query<AvatarRequest>>) -> Response {
let Some(params) = params else {
return ResponseCode::BadRequest.text("Missing query paramaters");
};
let custom = format!("/image/custom/avatar/{}.png", params.user_id);
let default = format!("/image/default/{}.png", params.user_id % 25);
let file = super::serve(&custom).await;
if file.status() != StatusCode::OK {
return super::serve(&default).await;
}
file
}
#[derive(Deserialize)]
pub struct BannerRequest {
user_id: u64,
}
pub async fn banner(params: Option<Query<BannerRequest>>) -> Response {
let Some(params) = params else {
return ResponseCode::BadRequest.text("Missing query paramaters");
};
let custom = format!("/image/custom/banner/{}.png", params.user_id);
let file = super::serve(&custom).await;
if file.status() != StatusCode::OK {
return ResponseCode::NotFound.text("User does not have a custom banner");
}
file
}

47
src/public/mod.rs Normal file
View file

@ -0,0 +1,47 @@
use axum::{
body::Body,
http::{Request, StatusCode},
response::{IntoResponse, Response},
routing::get,
Router,
};
use tower::ServiceExt;
use tower_http::services::ServeFile;
use crate::types::http::ResponseCode;
pub mod admin;
pub mod console;
pub mod file;
pub mod pages;
pub fn router() -> Router {
Router::new()
.nest("/", pages::router())
.route("/js/*path", get(file::js))
.route("/css/*path", get(file::css))
.route("/fonts/*path", get(file::fonts))
.route("/image/*path", get(file::image))
.route("/image/avatar", get(file::avatar))
.route("/image/banner", get(file::banner))
}
pub async fn serve(path: &str) -> Response {
if !path.chars().any(|c| c == '.') {
return ResponseCode::BadRequest.text("Invalid file path");
}
let path = format!("public{path}");
let file = ServeFile::new(path);
let Ok(res) = file.oneshot(Request::new(Body::empty())).await else {
tracing::error!("Error while fetching file");
return ResponseCode::InternalServerError.text("Error while fetching file");
};
if res.status() != StatusCode::OK {
return ResponseCode::NotFound.text("File not found");
}
res.into_response()
}

View file

@ -5,7 +5,7 @@ use axum::{
}; };
use crate::{ use crate::{
console, public::console,
types::{ types::{
extract::{AuthorizedUser, Log}, extract::{AuthorizedUser, Log},
http::ResponseCode, http::ResponseCode,
@ -24,32 +24,20 @@ async fn login(user: Option<AuthorizedUser>, _: Log) -> Response {
if user.is_some() { if user.is_some() {
Redirect::to("/home").into_response() Redirect::to("/home").into_response()
} else { } else {
ResponseCode::Success.file("/login.html").await super::serve("/login.html").await
} }
} }
async fn home(user: Option<AuthorizedUser>, _: Log) -> Response { async fn home(_: Log) -> Response {
if user.is_none() { super::serve("/home.html").await
Redirect::to("/login").into_response()
} else {
ResponseCode::Success.file("/home.html").await
}
} }
async fn people(user: Option<AuthorizedUser>, _: Log) -> Response { async fn people(_: Log) -> Response {
if user.is_none() { super::serve("/people.html").await
Redirect::to("/login").into_response()
} else {
ResponseCode::Success.file("/people.html").await
}
} }
async fn profile(user: Option<AuthorizedUser>, _: Log) -> Response { async fn profile(_: Log) -> Response {
if user.is_none() { super::serve("/profile.html").await
Redirect::to("/login").into_response()
} else {
ResponseCode::Success.file("/profile.html").await
}
} }
async fn console() -> Response { async fn console() -> Response {
@ -57,7 +45,7 @@ async fn console() -> Response {
} }
async fn admin() -> Response { async fn admin() -> Response {
ResponseCode::Success.file("/admin.html").await super::serve("/admin.html").await
} }
async fn wordpress(_: Log) -> Response { async fn wordpress(_: Log) -> Response {

View file

@ -1,20 +1,24 @@
use std::{io::{Read, Cursor}, net::{IpAddr, SocketAddr}}; use std::{
io::{Cursor, Read},
net::{IpAddr, SocketAddr},
};
use axum::{ use axum::{
async_trait, async_trait,
body::HttpBody, body::HttpBody,
extract::{FromRequest, FromRequestParts, ConnectInfo}, extract::{ConnectInfo, FromRequest, FromRequestParts},
http::{request::Parts, Request}, http::{request::Parts, Request},
response::Response, response::Response,
BoxError, RequestExt, BoxError, RequestExt,
}; };
use bytes::Bytes; use bytes::Bytes;
use image::{io::Reader, ImageFormat, DynamicImage}; use image::{io::Reader, DynamicImage, ImageFormat};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tower_cookies::Cookies; use tower_cookies::Cookies;
use crate::{ use crate::{
admin, console, public::admin,
public::console,
types::{ types::{
http::{ResponseCode, Result}, http::{ResponseCode, Result},
session::Session, session::Session,
@ -32,41 +36,43 @@ where
type Rejection = Response; type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self> { async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self> {
let headers = &parts.headers; let headers = &parts.headers;
let forwardedfor = headers.get("x-forwarded-for") let forwardedfor = headers
.and_then(|h| h.to_str().ok()) .get("x-forwarded-for")
.and_then(|h| .and_then(|h| h.to_str().ok())
h.split(',') .and_then(|h| {
.rev() h.split(',')
.find_map(|s| s.trim().parse::<IpAddr>().ok()) .rev()
); .find_map(|s| s.trim().parse::<IpAddr>().ok())
});
if let Some(forwardedfor) = forwardedfor { if let Some(forwardedfor) = forwardedfor {
return Ok(RequestIp(forwardedfor)) return Ok(RequestIp(forwardedfor));
} }
let realip = headers.get("x-real-ip") let realip = headers
.and_then(|hv| hv.to_str().ok()) .get("x-real-ip")
.and_then(|s| s.parse::<IpAddr>().ok()); .and_then(|hv| hv.to_str().ok())
.and_then(|s| s.parse::<IpAddr>().ok());
if let Some(realip) = realip { if let Some(realip) = realip {
return Ok(RequestIp(realip)) return Ok(RequestIp(realip));
} }
let realip = headers.get("x-real-ip") let realip = headers
.and_then(|hv| hv.to_str().ok()) .get("x-real-ip")
.and_then(|s| s.parse::<IpAddr>().ok()); .and_then(|hv| hv.to_str().ok())
.and_then(|s| s.parse::<IpAddr>().ok());
if let Some(realip) = realip { if let Some(realip) = realip {
return Ok(RequestIp(realip)) return Ok(RequestIp(realip));
} }
let info = parts.extensions.get::<ConnectInfo<SocketAddr>>(); let info = parts.extensions.get::<ConnectInfo<SocketAddr>>();
if let Some(info) = info { if let Some(info) = info {
return Ok(RequestIp(info.0.ip())) return Ok(RequestIp(info.0.ip()));
} }
Err(ResponseCode::Forbidden.text("You have no ip")) Err(ResponseCode::Forbidden.text("You have no ip"))
@ -163,7 +169,6 @@ where
type Rejection = Response; type Rejection = Response;
async fn from_request(req: Request<B>, state: &S) -> Result<Self> { async fn from_request(req: Request<B>, state: &S) -> Result<Self> {
let bytes = match read_body(req, state).await { let bytes = match read_body(req, state).await {
Ok(body) => body, Ok(body) => body,
Err(err) => return Err(err), Err(err) => return Err(err),
@ -171,7 +176,7 @@ where
let mut reader = Reader::new(Cursor::new(bytes)); let mut reader = Reader::new(Cursor::new(bytes));
reader.set_format(ImageFormat::Png); reader.set_format(ImageFormat::Png);
let Ok(img) = reader.decode() else { let Ok(img) = reader.decode() else {
return Err(ResponseCode::BadRequest.text("Failed to decode png image")) return Err(ResponseCode::BadRequest.text("Failed to decode png image"))
}; };
@ -238,7 +243,6 @@ where
B::Error: Into<BoxError>, B::Error: Into<BoxError>,
S: Send + Sync, S: Send + Sync,
{ {
let Ok(RequestIp(ip)) = req.extract_parts::<RequestIp>().await else { let Ok(RequestIp(ip)) = req.extract_parts::<RequestIp>().await else {
tracing::error!("Failed to read client ip"); tracing::error!("Failed to read client ip");
return Err(ResponseCode::InternalServerError.text("Failed to read client ip")); return Err(ResponseCode::InternalServerError.text("Failed to read client ip"));
@ -255,14 +259,7 @@ where
return Err(ResponseCode::BadRequest.text("Request can be at most 512kb")); return Err(ResponseCode::BadRequest.text("Request can be at most 512kb"));
}; };
console::log( console::log(ip, method, uri, Some(path.to_string()), None).await;
ip,
method,
uri,
Some(path.to_string()),
None,
)
.await;
Ok(bytes.bytes().flatten().collect()) Ok(bytes.bytes().flatten().collect())
} }

View file

@ -1,11 +1,8 @@
use axum::{ use axum::{
body::Body,
headers::HeaderName, headers::HeaderName,
http::{HeaderValue, Request, StatusCode}, http::{HeaderValue, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use tower::ServiceExt;
use tower_http::services::ServeFile;
use tracing::instrument; use tracing::instrument;
#[derive(Debug)] #[derive(Debug)]
@ -58,24 +55,6 @@ impl ResponseCode {
); );
res res
} }
#[instrument()]
pub async fn file(self, path: &str) -> Response {
if !path.chars().any(|c| c == '.') {
return Self::BadRequest.text("Folders cannot be served");
}
let path = format!("public{path}");
let svc = ServeFile::new(path);
let Ok(mut res) = svc.oneshot(Request::new(Body::empty())).await else {
tracing::error!("Error while fetching file");
return Self::InternalServerError.text("Error while fetching file");
};
if res.status() != StatusCode::OK {
return Self::NotFound.text("File not found");
}
*res.status_mut() = self.code();
res.into_response()
}
} }
pub type Result<T> = std::result::Result<T, Response>; pub type Result<T> = std::result::Result<T, Response>;

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::instrument; use tracing::instrument;
use crate::api::auth::RegistrationRequet; use crate::api::RegistrationRequet;
use crate::database; use crate::database;
use crate::types::http::{ResponseCode, Result}; use crate::types::http::{ResponseCode, Result};