From 80b9d213c3f0ff5f6c7ce9246a08d2ffeb691883 Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Sat, 7 Sep 2024 19:37:52 +0200 Subject: [PATCH] feat(vault): keep track of contestants --- src/challenge/c1.rs | 24 ++++++------ src/challenge/c2.rs | 28 +++++++------- src/challenge/c3.rs | 31 ++++++++------- src/challenge/mod.rs | 90 ++++++++++++++++++++++++++++++++------------ src/lib.rs | 9 ++++- src/vault.rs | 59 ++++++++++++++++++++++++++++- 6 files changed, 172 insertions(+), 69 deletions(-) diff --git a/src/challenge/c1.rs b/src/challenge/c1.rs index ff81dfb..c5555f4 100644 --- a/src/challenge/c1.rs +++ b/src/challenge/c1.rs @@ -4,11 +4,11 @@ //! establish a TCP connection to the server to receive the secret. use async_trait::async_trait; -use libpt::log::{info, warn}; +use libpt::log::warn; use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; -use super::Challenge; +use super::{Challenge, Descriptions}; use crate::config::Config; use crate::has_won; use crate::vault::VaultRef; @@ -28,20 +28,19 @@ impl Challenge for C1 { self.vault.clone() } fn new(config: Config, vault: VaultRef) -> Self { - info!("Solution: {}", Self::solution()); Self { config, vault } } - fn hints() -> Vec { - vec![String::from("TCP connect to 1337.")] + fn text() -> Descriptions { + Descriptions { + 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."), + description: String::from("Do you know how to TCP?"), + } } - fn solution() -> String { - String::from("Connect by TCP, then the secret will be sent to you.") - } - - async fn serve(self) -> anyhow::Result<()> { - info!("serving challenge 1"); + async fn serve(&self) -> anyhow::Result<()> { let listener = TcpListener::bind(self.config.addr).await?; loop { @@ -53,7 +52,8 @@ impl Challenge for C1 { continue; } }; - has_won(&addr); + self.vault.add_contestant(addr.into()).await; + has_won(&addr, &self.vault, &addr.into()).await; tokio::spawn(async move { if let Err(e) = stream.write_all(vault.secret().as_bytes()).await { warn!("could not write to peer {addr}: {e}"); diff --git a/src/challenge/c2.rs b/src/challenge/c2.rs index ca948a4..8ca8607 100644 --- a/src/challenge/c2.rs +++ b/src/challenge/c2.rs @@ -10,7 +10,7 @@ use libpt::log::{info, warn}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; -use super::Challenge; +use super::{Challenge, Descriptions}; use crate::config::Config; use crate::has_won; use crate::vault::VaultRef; @@ -32,7 +32,7 @@ impl C2 { stream: &mut TcpStream, addr: &std::net::SocketAddr, ) -> Result<()> { - has_won(addr); + has_won(addr, vault, &addr.into()).await; if let Err(e) = stream.write_all(vault.secret().as_bytes()).await { warn!("could not write to peer {addr}: {e}"); return Err(e.into()); @@ -54,22 +54,20 @@ impl Challenge for C2 { self.vault.clone() } fn new(config: Config, vault: VaultRef) -> Self { - info!("Solution: {}", Self::solution()); Self { config, vault } } - - fn hints() -> Vec { - vec![String::from( - "TCP connect to 1337 and give me a special u16", - )] + fn text() -> Descriptions { + Descriptions { + title: "TCP dialogue".to_string(), + hints: vec![String::from( + "TCP connect to 1337 and give me a special u16", + )], + solution: String::from("Connect by TCP, send 1337 as bytes (not text)."), + description: String::from("Do you know how to TCP, but slightly cooler than for C1?"), + } } - fn solution() -> String { - String::from("Connect by TCP, send 1337 as bytes (not text).") - } - - async fn serve(self) -> anyhow::Result<()> { - info!("serving challenge 2"); + async fn serve(&self) -> anyhow::Result<()> { let listener = TcpListener::bind(self.config.addr).await?; loop { @@ -81,7 +79,7 @@ impl Challenge for C2 { continue; } }; - info!("new peer: {addr}"); + self.vault.add_contestant(addr.into()).await; tokio::spawn(async move { let mut buf: u16; loop { diff --git a/src/challenge/c3.rs b/src/challenge/c3.rs index 0f3ba49..47f5daf 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; +use super::{Challenge, Descriptions}; use crate::config::Config; use crate::has_won; use crate::vault::VaultRef; @@ -189,7 +189,7 @@ impl C3 { stream: &mut TcpStream, addr: &std::net::SocketAddr, ) -> Result<()> { - has_won(addr); + has_won(addr, vault, &addr.into()).await; if let Err(e) = stream .write_all(("You win: ".to_owned() + vault.secret()).as_bytes()) .await @@ -214,23 +214,22 @@ impl Challenge for C3 { self.vault.clone() } fn new(config: Config, vault: VaultRef) -> Self { - info!("Solution: {}", Self::solution()); Self { config, vault } } - - fn hints() -> Vec { - vec![ - "TCP connect to 1337 and answer the questions".to_string(), - "The questions keep chaning".to_string(), - "You should try to solve the questions in an automated way".to_string(), - ] + fn text() -> Descriptions { + Descriptions { + title: "TCP math exam".to_string(), + hints: vec![ + "TCP connect to 1337 and answer the questions.".to_string(), + "The questions keep chaning.".to_string(), + "You should try to solve the questions in an automated way.".to_string(), + ], + solution: String::from("Connect by TCP, programmatically answer the questions."), + description: String::from("How fast can you do arithmetics?"), + } } - fn solution() -> String { - String::from("Connect by TCP, send 1337 as bytes (not text).") - } - - async fn serve(self) -> anyhow::Result<()> { + async fn serve(&self) -> anyhow::Result<()> { info!("serving challenge 3"); let listener = TcpListener::bind(self.config.addr).await?; @@ -243,7 +242,7 @@ impl Challenge for C3 { continue; } }; - info!("new peer: {}", peer.1); + self.vault.add_contestant(peer.1.into()).await; tokio::spawn(async move { if let Err(e) = Self::handler(peer, &vault).await { warn!("error while handling peer: {e}"); diff --git a/src/challenge/mod.rs b/src/challenge/mod.rs index dbc5bc2..35b4775 100644 --- a/src/challenge/mod.rs +++ b/src/challenge/mod.rs @@ -16,6 +16,60 @@ pub mod c1; pub mod c2; pub mod c3; +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct Descriptions { + title: String, + hints: Vec, + 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. @@ -28,6 +82,8 @@ where 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 @@ -39,24 +95,6 @@ where /// /// A new instance of the challenge. fn new(config: Config, vault: VaultRef) -> Self; - /// 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() -> Vec; - /// 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() -> String; /// Starts the challenge and serves it to the contestants. /// /// This method is asynchronous and returns a result indicating whether the challenge was @@ -69,7 +107,7 @@ where /// # Errors /// /// Will error when the challenge errors, for example when the network adress cannot be bound. - async fn serve(self) -> anyhow::Result<()>; + 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 @@ -78,8 +116,8 @@ where /// 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::adming_interface). - /// The hadmin interface is not part of the challenge and just for monitoring. + /// admin interface, define [admin_interface](Self::admin_interface). + /// The admin interface is not part of the challenge and just for monitoring. /// /// # Returns /// @@ -89,15 +127,19 @@ where if self.config().addr_admin.is_some() { let vault = self.vault(); let config = self.config(); - tokio::spawn(Self::admin_interface(vault, 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(vault: VaultRef, config: Config) -> anyhow::Result<()> { - admin::serve(vault, config).await?; + async fn admin_interface( + text: Descriptions, + vault: VaultRef, + config: Config, + ) -> anyhow::Result<()> { + admin::serve(text, vault, config).await?; Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 8e4fa85..a88d802 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,11 +14,18 @@ //! Wooly Vault is programmed asynchronously with [tokio] to be able to handle many contestants at //! once if needed. +use self::vault::VaultRef; + pub mod challenge; pub mod config; pub mod vault; #[inline] -pub(crate) fn has_won(addr: &std::net::SocketAddr) { +pub(crate) async fn has_won( + addr: &std::net::SocketAddr, + vault: &VaultRef, + contestant: &vault::Contestant, +) { + vault.add_winner(contestant.clone()).await; libpt::log::info!("Sending the secret to {addr}") } diff --git a/src/vault.rs b/src/vault.rs index bf22d31..8f1b065 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -3,20 +3,57 @@ //! This module provides a [`Vault`] struct that holds a secret and allows it to be shared safely //! across the application using an [`Arc`] (Atomic Reference Counted) pointer. +use std::collections::HashSet; +use std::fmt::{Debug, Display}; +use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; +use libpt::log::info; +use tokio::sync::Mutex; + /// A type alias for an [`Arc`] pointer to a [`Vault`] instance. /// /// This type is used to share a [`Vault`] instance across multiple parts of the application. pub type VaultRef = Arc; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Contestant { + ip: IpAddr, +} + /// A struct that holds a secret and provides methods for accessing it. /// /// The [`Vault`] struct is designed to be shared safely across the application using an [`Arc`] pointer. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct Vault { /// The secret stored in the vault secret: String, + contestants: Arc>>, + winners: Arc>>, +} + +impl From for Contestant { + fn from(value: IpAddr) -> Self { + Self { ip: value } + } +} + +impl From for Contestant { + fn from(value: SocketAddr) -> Self { + Self { ip: value.ip() } + } +} + +impl From<&SocketAddr> for Contestant { + fn from(value: &SocketAddr) -> Self { + Self { ip: value.ip() } + } +} + +impl Display for Contestant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } } impl Vault { @@ -30,6 +67,8 @@ impl Vault { pub fn new(secret: &str) -> VaultRef { let v = Self { secret: secret.to_string(), + contestants: Default::default(), + winners: Default::default(), }; Arc::new(v) } @@ -42,4 +81,22 @@ impl Vault { pub fn secret(&self) -> &str { &self.secret } + + pub async fn add_contestant(&self, contestant: Contestant) -> bool { + info!("new contestant: {contestant}"); + self.contestants.lock().await.insert(contestant) + } + + pub async fn add_winner(&self, contestant: Contestant) -> bool { + info!("new winner: {contestant}"); + self.winners.lock().await.insert(contestant) + } + + pub async fn contestants(&self) -> HashSet { + self.contestants.lock().await.clone() + } + + pub async fn winners(&self) -> HashSet { + self.contestants.lock().await.clone() + } }