summaryrefslogtreecommitdiff
path: root/src/public
diff options
context:
space:
mode:
authorTyler Murphy <tylermurphy534@gmail.com>2023-02-01 20:34:22 -0500
committerTyler Murphy <tylermurphy534@gmail.com>2023-02-01 20:34:22 -0500
commit2026a8f4579b1db0f6e5e7b11ac33c13969adb6c (patch)
treedaa13419b7227462775e325a4f5f60f2ed33c1da /src/public
parentremove b64 imgs (diff)
downloadxssbook-2026a8f4579b1db0f6e5e7b11ac33c13969adb6c.tar.gz
xssbook-2026a8f4579b1db0f6e5e7b11ac33c13969adb6c.tar.bz2
xssbook-2026a8f4579b1db0f6e5e7b11ac33c13969adb6c.zip
static serve refactor
Diffstat (limited to 'src/public')
-rw-r--r--src/public/admin.rs129
-rw-r--r--src/public/console.rs264
-rw-r--r--src/public/file.rs69
-rw-r--r--src/public/mod.rs47
-rw-r--r--src/public/pages.rs65
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('&', "&amp;")
+ .replace('<', "&lt;")
+ .replace('>', "&gt;")
+}
+
+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))
+}