diff options
Diffstat (limited to 'src/public')
-rw-r--r-- | src/public/admin.rs | 129 | ||||
-rw-r--r-- | src/public/console.rs | 264 | ||||
-rw-r--r-- | src/public/file.rs | 69 | ||||
-rw-r--r-- | src/public/mod.rs | 47 | ||||
-rw-r--r-- | src/public/pages.rs | 65 |
5 files changed, 574 insertions, 0 deletions
diff --git a/src/public/admin.rs b/src/public/admin.rs new file mode 100644 index 0000000..1da2f1e --- /dev/null +++ b/src/public/admin.rs @@ -0,0 +1,129 @@ +use axum::response::Response; +use lazy_static::lazy_static; +use rand::{distributions::Alphanumeric, Rng}; +use tokio::sync::Mutex; + +use crate::{ + console::{self, sanatize}, + types::{http::ResponseCode, post::Post, session::Session, user::User}, +}; + +lazy_static! { + static ref SECRET: Mutex<String> = Mutex::new(String::new()); +} + +pub fn new_secret() -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect() +} + +pub async fn get_secret() -> String { + let mut secret = SECRET.lock().await; + if secret.is_empty() { + *secret = new_secret(); + } + secret.clone() +} + +pub async fn regen_secret() -> String { + let mut secret = SECRET.lock().await; + *secret = new_secret(); + secret.clone() +} + +pub fn generate_users() -> Response { + let users = match User::reterieve_all() { + Ok(users) => users, + Err(err) => return err, + }; + + let mut html = r#" + <tr> + <th>User ID</th> + <th>First Name</th> + <th>Last Name</th> + <th>Email</th> + <th>Password</th> + <th>Gender</th> + <th>Date</th> + <th>Day</th> + <th>Month</th> + <th>Year</th> + </tr> + "# + .to_string(); + + for user in users { + html.push_str( + &format!("<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>", + user.user_id, sanatize(&user.firstname), sanatize(&user.lastname), sanatize(&user.email), sanatize(&user.password), + sanatize(&user.gender), user.date, user.day, user.month, user.year + ) + ); + } + + ResponseCode::Success.text(&html) +} + +pub fn generate_posts() -> Response { + let posts = match Post::reterieve_all() { + Ok(posts) => posts, + Err(err) => return err, + }; + + let mut html = r#" + <tr> + <th>Post ID</th> + <th>User ID</th> + <th>Content</th> + <th>Likes</th> + <th>Comments</th> + <th>Date</th> + </tr> + "# + .to_string(); + + for post in posts { + let Ok(likes) = serde_json::to_string(&post.likes) else { continue }; + let Ok(comments) = serde_json::to_string(&post.comments) else { continue }; + + html.push_str(&format!( + "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>", + post.post_id, + post.user_id, + sanatize(&post.content), + console::beautify(&likes), + console::beautify(&comments), + post.date + )); + } + + ResponseCode::Success.text(&html) +} + +pub fn generate_sessions() -> Response { + let sessions = match Session::reterieve_all() { + Ok(sessions) => sessions, + Err(err) => return err, + }; + + let mut html = r#" + <tr> + <th>User ID</th> + <th>Token</th> + </tr> + "# + .to_string(); + + for session in sessions { + html.push_str(&format!( + "<tr><td>{}</td><td>{}</td></tr>", + session.user_id, session.token + )); + } + + ResponseCode::Success.text(&html) +} diff --git a/src/public/console.rs b/src/public/console.rs new file mode 100644 index 0000000..16bf4a3 --- /dev/null +++ b/src/public/console.rs @@ -0,0 +1,264 @@ +use axum::{ + http::{Method, Uri}, + response::Response, +}; +use lazy_static::lazy_static; +use serde::Serialize; +use serde_json::{ser::Formatter, Value}; +use std::{collections::VecDeque, io, net::IpAddr}; +use tokio::sync::Mutex; + +use crate::types::http::ResponseCode; + +struct LogMessage { + ip: IpAddr, + method: Method, + uri: Uri, + path: String, + body: String, +} + +impl ToString for LogMessage { + fn to_string(&self) -> String { + let mut ip = self.ip.to_string(); + if ip.contains("::ffff:") { + ip = ip.as_str()[7..].to_string(); + } + let color = match self.method { + Method::GET => "#3fe04f", + Method::POST => "#853fe0", + Method::PATCH => "#e0773f", + Method::PUT => "#e0cb3f", + Method::HEAD => "#3f75e0", + Method::DELETE => "#e04c3f", + Method::CONNECT => "#3fe0ad", + Method::TRACE => "#e03fc5", + Method::OPTIONS => "#423fe0", + _ => "white", + }; + format!("<div class='msg'><span class='ip'>{}</span> <span class='method' style='color: {};'>{}</span> <span class='path'>{}{}</span> <span class='body'>{}</span></div>", + ip, color, self.method, self.path, sanatize(&self.uri.to_string()), self.body) + } +} + +lazy_static! { + static ref LOG: Mutex<VecDeque<LogMessage>> = Mutex::new(VecDeque::with_capacity(200)); +} + +pub async fn log(ip: IpAddr, method: Method, uri: Uri, path: Option<String>, body: Option<String>) { + let path = path.unwrap_or_default(); + let body = body.unwrap_or_default(); + + if path == "/api/admin" { + return; + } + + tracing::info!("{} {} {}{} {}", &ip, &method, &path, &uri, &body); + + let message = LogMessage { + ip, + method, + uri, + path, + body: beautify(&body), + }; + + let mut lock = LOG.lock().await; + if lock.len() > 200 { + lock.pop_back(); + } + lock.push_front(message); +} + +struct HtmlFormatter; +impl Formatter for HtmlFormatter { + fn write_null<W>(&mut self, writer: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + writer.write_all(b"<span class='null'>null</span>") + } + + fn write_bool<W>(&mut self, writer: &mut W, value: bool) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let s = if value { + b"<span class='bool'>true</span>" as &[u8] + } else { + b"<span class='bool'>false</span>" as &[u8] + }; + writer.write_all(s) + } + + fn write_i8<W>(&mut self, writer: &mut W, value: i8) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let buff = format!("<span class='number'>{value}</span>"); + writer.write_all(buff.as_bytes()) + } + + fn write_i16<W>(&mut self, writer: &mut W, value: i16) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let buff = format!("<span class='number'>{value}</span>"); + writer.write_all(buff.as_bytes()) + } + + fn write_i32<W>(&mut self, writer: &mut W, value: i32) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let buff = format!("<span class='number'>{value}</span>"); + writer.write_all(buff.as_bytes()) + } + + fn write_i64<W>(&mut self, writer: &mut W, value: i64) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let buff = format!("<span class='number'>{value}</span>"); + writer.write_all(buff.as_bytes()) + } + + fn write_u8<W>(&mut self, writer: &mut W, value: u8) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let buff = format!("<span class='number'>{value}</span>"); + writer.write_all(buff.as_bytes()) + } + + fn write_u16<W>(&mut self, writer: &mut W, value: u16) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let buff = format!("<span class='number'>{value}</span>"); + writer.write_all(buff.as_bytes()) + } + + fn write_u32<W>(&mut self, writer: &mut W, value: u32) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let buff = format!("<span class='number'>{value}</span>"); + writer.write_all(buff.as_bytes()) + } + + fn write_u64<W>(&mut self, writer: &mut W, value: u64) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let buff = format!("<span class='number'>{value}</span>"); + writer.write_all(buff.as_bytes()) + } + + fn write_f32<W>(&mut self, writer: &mut W, value: f32) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let buff = format!("<span class='number'>{value}</span>"); + writer.write_all(buff.as_bytes()) + } + + fn write_f64<W>(&mut self, writer: &mut W, value: f64) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let buff = format!("<span class='number'>{value}</span>"); + writer.write_all(buff.as_bytes()) + } + + fn begin_string<W>(&mut self, writer: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + writer.write_all(b"<span class='string'>\"") + } + + fn end_string<W>(&mut self, writer: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + writer.write_all(b"\"</span>") + } + + fn begin_object_key<W>(&mut self, writer: &mut W, first: bool) -> io::Result<()> + where + W: ?Sized + io::Write, + { + if first { + writer.write_all(b"<span class='key'>") + } else { + writer.write_all(b"<span class='key'>,") + } + } + + fn end_object_key<W>(&mut self, writer: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + writer.write_all(b"</span>") + } +} + +pub fn sanatize(input: &str) -> String { + input + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + +pub fn beautify(body: &str) -> String { + let body = sanatize(body); + + if body.is_empty() { + return String::new(); + } + let Ok(mut json) = serde_json::from_str::<Value>(&body) else { + return body + }; + if json["password"].is_string() { + json["password"] = Value::String("********".to_owned()); + } + let mut writer: Vec<u8> = Vec::with_capacity(128); + let mut serializer = serde_json::Serializer::with_formatter(&mut writer, HtmlFormatter); + if json.serialize(&mut serializer).is_err() { + return body; + } + String::from_utf8_lossy(&writer).to_string() +} + +pub async fn generate() -> Response { + let lock = LOG.lock().await; + + let mut html = r#"<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta http-equiv="refresh" content="5"> + <link rel="stylesheet" href="/css/main.css"> + <link rel="stylesheet" href="/css/header.css"> + <link rel="stylesheet" href="/css/admin.css"> + <link rel="stylesheet" href="/css/console.css"> + <title>XSSBook - Console</title> + </head> + <body> + <div id="header"> + <span class="logo"><a href="/">xssbook</a></span> + <span class="gtext desc" style="margin-left: 6em; font-size: 2em; color: #606770">Console</span> + </div> + <div style="margin-bottom: 3.5em"></div> + "# + .to_string(); + + for message in lock.iter() { + html.push_str(&message.to_string()); + } + + html.push_str("</body></html>"); + + ResponseCode::Success.html(&html) +} diff --git a/src/public/file.rs b/src/public/file.rs new file mode 100644 index 0000000..b54ef25 --- /dev/null +++ b/src/public/file.rs @@ -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 +} diff --git a/src/public/mod.rs b/src/public/mod.rs new file mode 100644 index 0000000..cf8156d --- /dev/null +++ b/src/public/mod.rs @@ -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() +} diff --git a/src/public/pages.rs b/src/public/pages.rs new file mode 100644 index 0000000..1614d81 --- /dev/null +++ b/src/public/pages.rs @@ -0,0 +1,65 @@ +use axum::{ + response::{IntoResponse, Redirect, Response}, + routing::get, + Router, +}; + +use crate::{ + public::console, + types::{ + extract::{AuthorizedUser, Log}, + http::ResponseCode, + }, +}; + +async fn root(user: Option<AuthorizedUser>, _: Log) -> Response { + if user.is_some() { + Redirect::to("/home").into_response() + } else { + Redirect::to("/login").into_response() + } +} + +async fn login(user: Option<AuthorizedUser>, _: Log) -> Response { + if user.is_some() { + Redirect::to("/home").into_response() + } else { + super::serve("/login.html").await + } +} + +async fn home(_: Log) -> Response { + super::serve("/home.html").await +} + +async fn people(_: Log) -> Response { + super::serve("/people.html").await +} + +async fn profile(_: Log) -> Response { + super::serve("/profile.html").await +} + +async fn console() -> Response { + console::generate().await +} + +async fn admin() -> Response { + super::serve("/admin.html").await +} + +async fn wordpress(_: Log) -> Response { + ResponseCode::ImATeapot.text("Hello i am a teapot owo") +} + +pub fn router() -> Router { + Router::new() + .route("/", get(root)) + .route("/login", get(login)) + .route("/home", get(home)) + .route("/people", get(people)) + .route("/profile", get(profile)) + .route("/console", get(console)) + .route("/wp-admin", get(wordpress)) + .route("/admin", get(admin)) +} |