wooly-vault/src/challenge/mod.rs

175 lines
5.6 KiB
Rust

//! 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 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 {
title: String,
hints: Vec<String>,
solution: String,
description: String,
}
impl Descriptions {
/// 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.
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.
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.
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.
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,
{
/// 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;
/// 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 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?;
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<()> {
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?,
_ => {
return Err(anyhow!(
"no challenge with index {index} does currently exist"
))
}
}
Ok(())
}