diff --git a/Cargo.toml b/Cargo.toml index f824396..80fd006 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,11 +29,8 @@ tokio = { version = "1.40.0", features = [ "rt", "sync", ] } -tracing = "0.1.40" -tracing-appender = "0.2.3" -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } warp = { version = "0.3.7", optional = true } [features] -default = ["admin-interface"] -admin-interface = ["dep:warp", "dep:minijinja"] +default = ["meta-interface"] +meta-interface = ["dep:warp", "dep:minijinja"] diff --git a/src/challenge/admin.rs b/src/challenge/admin.rs deleted file mode 100644 index 6c933cf..0000000 --- a/src/challenge/admin.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::fmt::Display; -use std::sync::Arc; - -use anyhow::Result; -use libpt::log::error; -use libpt::log::info; -use libpt::log::tracing; -use libpt::log::warn; -use minijinja::context; -use minijinja::Environment; -use thiserror::Error; -use warp::http::StatusCode; -use warp::reject; -use warp::reply::Response; -use warp::Filter; - -use crate::config::Config; -use crate::vault::VaultRef; - -use super::Descriptions; - -#[derive(Clone)] -pub struct Service<'tp> { - vault: VaultRef, - config: Config, - env: Environment<'tp>, - text: Descriptions, -} -impl<'tp> Service<'tp> { - fn new( - vault: VaultRef, - config: Config, - env: Environment<'tp>, - text: Descriptions, - ) -> Arc { - Self { - vault, - config, - env, - text, - } - .into() - } -} - -pub async fn serve(text: Descriptions, vault: VaultRef, config: Config) -> Result<()> { - let mut env = Environment::new(); - env.add_template("index", include_str!("../../data/www/admin.html"))?; - - let service = Service::new(vault, config, env, text); - let service2 = service.clone(); - - let routes = warp::path::end() - .map(move || service2.clone()) - .and_then(overview) - .or(warp::path("styles.css").and_then(styles)) - .recover(handle_rejection) - .with(warp::trace(|info| { - // Create a span using tracing macros - tracing::info_span!( - "admin", - method = %info.method(), - path = %info.path(), - ) - })); - - warp::serve(routes) - .run(service.config.addr_admin.unwrap()) - .await; - - warn!("exited the admin interface"); - Ok(()) -} - -#[derive(Debug, Error)] -struct TemplateError(#[from] minijinja::Error); - -impl reject::Reject for TemplateError {} -impl Display for TemplateError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "something went wrong with producing this page") - } -} - -async fn overview(serv: Arc>) -> Result, warp::Rejection> { - let contestants = serv - .vault - .contestants() - .await - .into_iter() - .collect::>(); - let winners = serv.vault.winners().await.into_iter().collect::>(); - let r = Response::new( - serv.env - .get_template("index") - .map_err(TemplateError::from)? - .render(context!( - title => "Wooly-Vault", - author => env!("CARGO_PKG_AUTHORS"), - year => "2024", - challenge_idx => serv.config.challenge, - challenge_title => serv.text.title(), - challenge_description => serv.text.description(), - challenge_hints => serv.text.hints(), - challenge_solution => serv.text.solution(), - contestants => serv.vault.contestants().await.iter().collect::>(), - winners => serv.vault.winners().await.iter().collect::>(), - contestants => contestants, - winners => winners, - contestants_amount => contestants.len(), - winners_amount => winners.len(), - )) - .map_err(TemplateError::from)? - .into(), - ); - - Ok(Box::new(r)) -} - -async fn styles() -> Result, warp::Rejection> { - let r = Response::new(include_str!("../../data/www/styles.css").to_string().into()); - Ok(Box::new(r)) -} - -async fn handle_rejection( - err: reject::Rejection, -) -> Result { - let code; - let message; - info!("rejecting: {err:?}"); - - if err.is_not_found() { - code = StatusCode::NOT_FOUND; - message = "page not found"; - } else if let Some(e) = err.find::() { - error!("templating error: {e}"); - code = StatusCode::INTERNAL_SERVER_ERROR; - message = "could not process data to make a page"; - } else { - error!("unhandled rejection: {:?}", err); - code = StatusCode::INTERNAL_SERVER_ERROR; - message = "unhandled rejection"; - } - - Ok(warp::reply::with_status(message, code)) -} diff --git a/src/challenge/c1.rs b/src/challenge/c1.rs index c5555f4..9bb0164 100644 --- a/src/challenge/c1.rs +++ b/src/challenge/c1.rs @@ -8,12 +8,13 @@ use libpt::log::warn; use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; -use super::{Challenge, Descriptions}; +use super::{Challenge, ChallengeDesc}; use crate::config::Config; use crate::has_won; use crate::vault::VaultRef; /// This struct holds the configuration and vault for the challenge. +#[derive(Clone, Debug)] pub struct C1 { config: Config, vault: VaultRef, @@ -31,8 +32,8 @@ impl Challenge for C1 { Self { config, vault } } - fn text() -> Descriptions { - Descriptions { + fn text() -> ChallengeDesc { + ChallengeDesc { title: "dumb TCP".to_string(), hints: vec![String::from("TCP connect to 1337.")], solution: String::from("Connect by TCP, then the secret will be sent to you."), diff --git a/src/challenge/c2.rs b/src/challenge/c2.rs index 8ca8607..3c86456 100644 --- a/src/challenge/c2.rs +++ b/src/challenge/c2.rs @@ -10,12 +10,13 @@ use libpt::log::{info, warn}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; -use super::{Challenge, Descriptions}; +use super::{Challenge, ChallengeDesc}; use crate::config::Config; use crate::has_won; use crate::vault::VaultRef; /// This struct holds the configuration and vault for the challenge. +#[derive(Clone, Debug)] pub struct C2 { config: Config, vault: VaultRef, @@ -56,8 +57,8 @@ impl Challenge for C2 { fn new(config: Config, vault: VaultRef) -> Self { Self { config, vault } } - fn text() -> Descriptions { - Descriptions { + fn text() -> ChallengeDesc { + ChallengeDesc { title: "TCP dialogue".to_string(), hints: vec![String::from( "TCP connect to 1337 and give me a special u16", diff --git a/src/challenge/c3.rs b/src/challenge/c3.rs index 47f5daf..3ae95e0 100644 --- a/src/challenge/c3.rs +++ b/src/challenge/c3.rs @@ -29,7 +29,7 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{TcpListener, TcpStream}; use tokio::time::Instant; -use super::{Challenge, Descriptions}; +use super::{Challenge, ChallengeDesc}; use crate::config::Config; use crate::has_won; use crate::vault::VaultRef; @@ -40,7 +40,7 @@ pub const NEEDED_CORRECT: usize = 16; pub const MILLIS_PER_QUESTION: u128 = 200; /// Signifies the operations that the randomiser can come up with. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] enum Operation { Add, Sub, @@ -90,6 +90,7 @@ impl Distribution for Standard { } /// This struct holds the configuration and vault for the challenge. +#[derive(Clone, Debug)] pub struct C3 { config: Config, vault: VaultRef, @@ -216,8 +217,8 @@ impl Challenge for C3 { fn new(config: Config, vault: VaultRef) -> Self { Self { config, vault } } - fn text() -> Descriptions { - Descriptions { + fn text() -> ChallengeDesc { + ChallengeDesc { title: "TCP math exam".to_string(), hints: vec![ "TCP connect to 1337 and answer the questions.".to_string(), diff --git a/src/challenge/mod.rs b/src/challenge/mod.rs index 35b4775..2c1dfe6 100644 --- a/src/challenge/mod.rs +++ b/src/challenge/mod.rs @@ -5,26 +5,24 @@ use anyhow::anyhow; use async_trait::async_trait; +use libpt::log::{error, info}; use crate::config::Config; use crate::vault::VaultRef; -#[cfg(feature = "admin-interface")] -pub mod admin; - pub mod c1; pub mod c2; pub mod c3; #[derive(Clone, PartialEq, Eq, Hash, Debug)] -pub struct Descriptions { +pub struct ChallengeDesc { title: String, hints: Vec, solution: String, description: String, } -impl Descriptions { +impl ChallengeDesc { /// Returns a list of hints for the challenge. /// /// A hint is a short text to be given to the contestants in case the admin thinks they need @@ -33,7 +31,7 @@ impl Descriptions { /// # Returns /// /// A vector of strings containing hints for the challenge. - fn hints(&self) -> Vec<&str> { + pub fn hints(&self) -> Vec<&str> { self.hints.iter().map(|a| a.as_ref()).collect() } /// Returns the solution to the challenge. @@ -44,7 +42,7 @@ impl Descriptions { /// # Returns /// /// A string containing the solution to the challenge. - fn solution(&self) -> &str { + pub fn solution(&self) -> &str { &self.solution } /// Returns the description to the challenge. @@ -55,7 +53,7 @@ impl Descriptions { /// # Returns /// /// A string containing the description to the challenge. - fn description(&self) -> &str { + pub fn description(&self) -> &str { &self.description } /// Returns the title to the challenge. @@ -65,7 +63,7 @@ impl Descriptions { /// # Returns /// /// A string containing the title to the challenge. - fn title(&self) -> &str { + pub fn title(&self) -> &str { &self.title } } @@ -76,14 +74,18 @@ impl Descriptions { #[async_trait] pub trait Challenge where - Self: Sized, + Self: Sized + 'static, + Self: Send, + Self: Sync, + Self: Clone, + Self: std::fmt::Debug, { /// Getter for the [vault](VaultRef). fn vault(&self) -> VaultRef; /// Getter for the [Config]. fn config(&self) -> Config; /// Get the various texts for this challenge. - fn text() -> Descriptions; + fn text() -> ChallengeDesc; /// Creates a new instance of the challenge with the given configuration and vault. /// /// # Arguments @@ -122,24 +124,13 @@ where /// # Returns /// /// A result indicating whether the challenge and the admin interface were successfully served. - async fn setup_and_start(self) -> anyhow::Result<()> { - #[cfg(feature = "admin-interface")] - if self.config().addr_admin.is_some() { - let vault = self.vault(); - let config = self.config(); - tokio::spawn(Self::admin_interface(Self::text(), vault, config)); - } - - let challenge_handle = self.serve(); - Ok(challenge_handle.await?) - } - #[cfg(feature = "admin-interface")] - async fn admin_interface( - text: Descriptions, - vault: VaultRef, - config: Config, - ) -> anyhow::Result<()> { - admin::serve(text, vault, config).await?; + async fn start(&self) -> anyhow::Result<()> { + let c = self.clone(); + tokio::spawn(async move { + if let Err(e) = c.serve().await { + error!("challenge {} has crashed! {e:#?}", Self::text().title()); + }; + }); Ok(()) } } @@ -161,10 +152,11 @@ where /// Returns an error if no challenge with the given index exists, or if the challenge that is being /// served errors. pub async fn select_and_start(index: u16, config: Config, vault: VaultRef) -> anyhow::Result<()> { + info!("select+start"); match index { - 1 => c1::C1::new(config, vault).setup_and_start().await?, - 2 => c2::C2::new(config, vault).setup_and_start().await?, - 3 => c3::C3::new(config, vault).setup_and_start().await?, + 1 => c1::C1::new(config, vault).start().await?, + 2 => c2::C2::new(config, vault).start().await?, + 3 => c3::C3::new(config, vault).start().await?, _ => { return Err(anyhow!( "no challenge with index {index} does currently exist" diff --git a/src/lib.rs b/src/lib.rs index a88d802..c25518b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,8 @@ use self::vault::VaultRef; pub mod challenge; pub mod config; +#[cfg(feature = "meta-interface")] +pub mod meta; pub mod vault; #[inline] diff --git a/src/main.rs b/src/main.rs index b7fe776..7544f90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,5 +23,7 @@ async fn main() -> Result<()> { select_and_start(conf.challenge, conf, v).await?; - Ok(()) + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await + } } diff --git a/src/meta/admin.rs b/src/meta/admin.rs new file mode 100644 index 0000000..e58c8c1 --- /dev/null +++ b/src/meta/admin.rs @@ -0,0 +1,78 @@ +use std::fmt::Display; +use std::sync::Arc; + +use anyhow::Result; +use libpt::log::error; +use libpt::log::info; +use libpt::log::tracing; +use libpt::log::warn; +use minijinja::context; +use minijinja::Environment; +use thiserror::Error; +use warp::http::StatusCode; +use warp::reject; +use warp::reject::Rejection; +use warp::reply::Reply; +use warp::reply::Response; +use warp::Filter; + +use crate::config::Config; +use crate::vault::VaultRef; + +use super::errors::TemplateError; +use super::Service; + +impl<'tp> Service<'tp> { + pub fn admin_routes( + this: Arc, + ) -> impl Filter + Clone + 'tp { + let serv = this.clone(); + warp::path::end() + .map(move || serv.clone()) + .and_then(overview) + .with(warp::trace(|info| { + // Create a span using tracing macros + tracing::info_span!( + "admin", + method = %info.method(), + path = %info.path(), + ) + })) + } +} + +async fn overview(serv: Arc>) -> Result, warp::Rejection> { + let challenge = serv.challenges[0].clone(); + let contestants = serv + .vault + .contestants() + .await + .into_iter() + .collect::>(); + let winners = serv.vault.winners().await.into_iter().collect::>(); + let r = Response::new( + serv.env + .get_template("index") + .map_err(TemplateError::from)? + .render(context!( + title => "Wooly-Vault", + author => env!("CARGO_PKG_AUTHORS"), + year => "2024", + challenge_idx => serv.config.challenge, + challenge_title => challenge.title(), + challenge_description => challenge.description(), + challenge_hints => challenge.hints(), + challenge_solution => challenge.solution(), + contestants => serv.vault.contestants().await.iter().collect::>(), + winners => serv.vault.winners().await.iter().collect::>(), + contestants => contestants, + winners => winners, + contestants_amount => contestants.len(), + winners_amount => winners.len(), + )) + .map_err(TemplateError::from)? + .into(), + ); + + Ok(Box::new(r)) +} diff --git a/src/meta/errors.rs b/src/meta/errors.rs new file mode 100644 index 0000000..3373140 --- /dev/null +++ b/src/meta/errors.rs @@ -0,0 +1,39 @@ +use std::fmt::Display; + +use libpt::log::{error, info}; +use thiserror::Error; +use warp::http::StatusCode; +use warp::reject; + +#[derive(Debug, Error)] +pub struct TemplateError(#[from] minijinja::Error); + +impl reject::Reject for TemplateError {} +impl Display for TemplateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "something went wrong with producing this page") + } +} + +pub async fn handle_rejection( + err: reject::Rejection, +) -> Result { + let code; + let message; + info!("rejecting: {err:?}"); + + if err.is_not_found() { + code = StatusCode::NOT_FOUND; + message = "page not found"; + } else if let Some(e) = err.find::() { + error!("templating error: {e}"); + code = StatusCode::INTERNAL_SERVER_ERROR; + message = "could not process data to make a page"; + } else { + error!("unhandled rejection: {:?}", err); + code = StatusCode::INTERNAL_SERVER_ERROR; + message = "unhandled rejection"; + } + + Ok(warp::reply::with_status(message, code)) +} diff --git a/src/meta/mod.rs b/src/meta/mod.rs new file mode 100644 index 0000000..1102c57 --- /dev/null +++ b/src/meta/mod.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use anyhow::Result; +use libpt::log::tracing; +use libpt::log::warn; +use minijinja::Environment; +use warp::reject::Rejection; +use warp::reply::Reply; +use warp::reply::Response; +use warp::Filter; + +use crate::challenge::ChallengeDesc; +use crate::config::Config; +use crate::vault::VaultRef; + +use self::errors::handle_rejection; + +pub mod admin; +pub mod errors; + +#[derive(Clone)] +pub struct Service<'tp> { + vault: VaultRef, + config: Config, + env: Environment<'tp>, + challenges: Vec, +} +impl<'tp> Service<'tp> { + fn new( + vault: VaultRef, + config: Config, + env: Environment<'tp>, + challenges: Vec, + ) -> Arc { + Self { + vault, + config, + env, + challenges, + } + .into() + } +} + +pub async fn serve(challenges: Vec, vault: VaultRef, config: Config) -> Result<()> { + let mut env = Environment::new(); + env.add_template("index", include_str!("../../data/www/admin.html"))?; + + let service = Service::new(vault, config, env, challenges); + + let routes = Service::admin_routes(service.clone()) + .or(Service::ressources_routes()) + .recover(handle_rejection) + .with(warp::trace(|info| { + // Create a span using tracing macros + tracing::info_span!( + "admin", + method = %info.method(), + path = %info.path(), + ) + })); + + warp::serve(routes) + .run(service.config.addr_admin.unwrap()) + .await; + + warn!("exited the admin interface"); + Ok(()) +} + +impl Service<'_> { + fn ressources_routes() -> impl Filter + Clone { + warp::path("styles.css").and_then(styles) + } +} + +async fn styles() -> Result, warp::Rejection> { + let r = Response::new(include_str!("../../data/www/styles.css").to_string().into()); + Ok(Box::new(r)) +}