//! Challenges and their common interface. //! //! This module is the core of the Wooly Vault application, as it defines the interface that all //! challenges must implement, and contains the challenge modules themselves. use anyhow::anyhow; use async_trait::async_trait; use libpt::log::{error, info}; use crate::config::Config; use crate::vault::VaultRef; pub mod c1; pub mod c2; pub mod c3; #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct ChallengeDesc { title: String, hints: Vec, solution: String, description: String, } 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 /// it. The first hint is the most vague, afterwards the hints become more and more helpful. /// /// # Returns /// /// A vector of strings containing hints for the challenge. pub fn hints(&self) -> Vec<&str> { self.hints.iter().map(|a| a.as_ref()).collect() } /// Returns the solution to the challenge. /// /// A solution is a short description of what has to be done to solve the challenge and get the /// secret of the vault. /// /// # Returns /// /// A string containing the solution to the challenge. pub fn solution(&self) -> &str { &self.solution } /// Returns the description to the challenge. /// /// The description is usually a short introduction to the challenge, without giving anything /// of the solution away. /// /// # Returns /// /// A string containing the description to the challenge. pub fn description(&self) -> &str { &self.description } /// Returns the title to the challenge. /// /// The title is just a short name, usually consisting of one to 5 words. /// /// # Returns /// /// A string containing the title to the challenge. pub fn title(&self) -> &str { &self.title } } /// Defines the behavior of a challenge. /// /// Any type that implements this trait can be served as a challenge in the Wooly Vault application. #[async_trait] pub trait Challenge where 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() -> ChallengeDesc; /// Creates a new instance of the challenge with the given configuration and vault. /// /// # Arguments /// /// * `config` - The configuration for the challenge. /// * `vault` - The vault that holds the secret for the challenge. /// /// # Returns /// /// A new instance of the challenge. fn new(config: Config, vault: VaultRef) -> Self; /// Starts the challenge and serves it to the contestants. /// /// This method is asynchronous and returns a result indicating whether the challenge was /// successful only after the challenge has ended. /// /// # Returns /// /// A result indicating whether the challenge was successfully served. /// /// # Errors /// /// Will error when the challenge errors, for example when the network adress cannot be bound. async fn serve(&self) -> anyhow::Result<()>; /// Serves a challenge and sets up the hint and monitoring service for the admin. /// /// This method not only serves the challenge, but it also sets up a small webservice for the /// admin of the challenge, where they can see the hints, the solution, and the winners. /// /// The admin interface will only be served when [Config::addr_admin] is set by the user. /// /// Challenges should not typically implement this themselves, if they want to customize the /// admin interface, define [admin_interface](Self::admin_interface). /// The admin interface is not part of the challenge and just for monitoring. /// /// # Returns /// /// A result indicating whether the challenge and the admin interface were successfully served. 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(()) } } /// Selects a challenge by index and serves it with the given configuration and vault. /// /// # Arguments /// /// * `index` - The index of the challenge to select. /// * `config` - The configuration for the challenge. /// * `vault` - The vault that holds the secret for the challenge. /// /// # Returns /// /// A result indicating whether the challenge has successfully ended. /// /// # Errors /// /// 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).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" )) } } Ok(()) }