feat(vault): keep track of contestants
cargo devel CI / cargo CI (push) Successful in 1m53s Details

This commit is contained in:
Christoph J. Scherr 2024-09-07 19:37:52 +02:00
parent f1ef0c26ec
commit 80b9d213c3
6 changed files with 172 additions and 69 deletions

View File

@ -4,11 +4,11 @@
//! establish a TCP connection to the server to receive the secret. //! establish a TCP connection to the server to receive the secret.
use async_trait::async_trait; use async_trait::async_trait;
use libpt::log::{info, warn}; use libpt::log::warn;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use super::Challenge; use super::{Challenge, Descriptions};
use crate::config::Config; use crate::config::Config;
use crate::has_won; use crate::has_won;
use crate::vault::VaultRef; use crate::vault::VaultRef;
@ -28,20 +28,19 @@ impl Challenge for C1 {
self.vault.clone() self.vault.clone()
} }
fn new(config: Config, vault: VaultRef) -> Self { fn new(config: Config, vault: VaultRef) -> Self {
info!("Solution: {}", Self::solution());
Self { config, vault } Self { config, vault }
} }
fn hints() -> Vec<String> { fn text() -> Descriptions {
vec![String::from("TCP connect to 1337.")] 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 { async fn serve(&self) -> anyhow::Result<()> {
String::from("Connect by TCP, then the secret will be sent to you.")
}
async fn serve(self) -> anyhow::Result<()> {
info!("serving challenge 1");
let listener = TcpListener::bind(self.config.addr).await?; let listener = TcpListener::bind(self.config.addr).await?;
loop { loop {
@ -53,7 +52,8 @@ impl Challenge for C1 {
continue; continue;
} }
}; };
has_won(&addr); self.vault.add_contestant(addr.into()).await;
has_won(&addr, &self.vault, &addr.into()).await;
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = stream.write_all(vault.secret().as_bytes()).await { if let Err(e) = stream.write_all(vault.secret().as_bytes()).await {
warn!("could not write to peer {addr}: {e}"); warn!("could not write to peer {addr}: {e}");

View File

@ -10,7 +10,7 @@ use libpt::log::{info, warn};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use super::Challenge; use super::{Challenge, Descriptions};
use crate::config::Config; use crate::config::Config;
use crate::has_won; use crate::has_won;
use crate::vault::VaultRef; use crate::vault::VaultRef;
@ -32,7 +32,7 @@ impl C2 {
stream: &mut TcpStream, stream: &mut TcpStream,
addr: &std::net::SocketAddr, addr: &std::net::SocketAddr,
) -> Result<()> { ) -> Result<()> {
has_won(addr); has_won(addr, vault, &addr.into()).await;
if let Err(e) = stream.write_all(vault.secret().as_bytes()).await { if let Err(e) = stream.write_all(vault.secret().as_bytes()).await {
warn!("could not write to peer {addr}: {e}"); warn!("could not write to peer {addr}: {e}");
return Err(e.into()); return Err(e.into());
@ -54,22 +54,20 @@ impl Challenge for C2 {
self.vault.clone() self.vault.clone()
} }
fn new(config: Config, vault: VaultRef) -> Self { fn new(config: Config, vault: VaultRef) -> Self {
info!("Solution: {}", Self::solution());
Self { config, vault } Self { config, vault }
} }
fn text() -> Descriptions {
fn hints() -> Vec<String> { Descriptions {
vec![String::from( title: "TCP dialogue".to_string(),
hints: vec![String::from(
"TCP connect to 1337 and give me a special u16", "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 { async fn serve(&self) -> anyhow::Result<()> {
String::from("Connect by TCP, send 1337 as bytes (not text).")
}
async fn serve(self) -> anyhow::Result<()> {
info!("serving challenge 2");
let listener = TcpListener::bind(self.config.addr).await?; let listener = TcpListener::bind(self.config.addr).await?;
loop { loop {
@ -81,7 +79,7 @@ impl Challenge for C2 {
continue; continue;
} }
}; };
info!("new peer: {addr}"); self.vault.add_contestant(addr.into()).await;
tokio::spawn(async move { tokio::spawn(async move {
let mut buf: u16; let mut buf: u16;
loop { loop {

View File

@ -29,7 +29,7 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio::time::Instant; use tokio::time::Instant;
use super::Challenge; use super::{Challenge, Descriptions};
use crate::config::Config; use crate::config::Config;
use crate::has_won; use crate::has_won;
use crate::vault::VaultRef; use crate::vault::VaultRef;
@ -189,7 +189,7 @@ impl C3 {
stream: &mut TcpStream, stream: &mut TcpStream,
addr: &std::net::SocketAddr, addr: &std::net::SocketAddr,
) -> Result<()> { ) -> Result<()> {
has_won(addr); has_won(addr, vault, &addr.into()).await;
if let Err(e) = stream if let Err(e) = stream
.write_all(("You win: ".to_owned() + vault.secret()).as_bytes()) .write_all(("You win: ".to_owned() + vault.secret()).as_bytes())
.await .await
@ -214,23 +214,22 @@ impl Challenge for C3 {
self.vault.clone() self.vault.clone()
} }
fn new(config: Config, vault: VaultRef) -> Self { fn new(config: Config, vault: VaultRef) -> Self {
info!("Solution: {}", Self::solution());
Self { config, vault } Self { config, vault }
} }
fn text() -> Descriptions {
fn hints() -> Vec<String> { Descriptions {
vec![ title: "TCP math exam".to_string(),
"TCP connect to 1337 and answer the questions".to_string(), hints: vec![
"The questions keep chaning".to_string(), "TCP connect to 1337 and answer the questions.".to_string(),
"You should try to solve the questions in an automated way".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 { async fn serve(&self) -> anyhow::Result<()> {
String::from("Connect by TCP, send 1337 as bytes (not text).")
}
async fn serve(self) -> anyhow::Result<()> {
info!("serving challenge 3"); info!("serving challenge 3");
let listener = TcpListener::bind(self.config.addr).await?; let listener = TcpListener::bind(self.config.addr).await?;
@ -243,7 +242,7 @@ impl Challenge for C3 {
continue; continue;
} }
}; };
info!("new peer: {}", peer.1); self.vault.add_contestant(peer.1.into()).await;
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = Self::handler(peer, &vault).await { if let Err(e) = Self::handler(peer, &vault).await {
warn!("error while handling peer: {e}"); warn!("error while handling peer: {e}");

View File

@ -16,6 +16,60 @@ pub mod c1;
pub mod c2; pub mod c2;
pub mod c3; 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. /// Defines the behavior of a challenge.
/// ///
/// Any type that implements this trait can be served as a challenge in the Wooly Vault application. /// 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; fn vault(&self) -> VaultRef;
/// Getter for the [Config]. /// Getter for the [Config].
fn config(&self) -> 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. /// Creates a new instance of the challenge with the given configuration and vault.
/// ///
/// # Arguments /// # Arguments
@ -39,24 +95,6 @@ where
/// ///
/// A new instance of the challenge. /// A new instance of the challenge.
fn new(config: Config, vault: VaultRef) -> Self; 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<String>;
/// 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. /// Starts the challenge and serves it to the contestants.
/// ///
/// This method is asynchronous and returns a result indicating whether the challenge was /// This method is asynchronous and returns a result indicating whether the challenge was
@ -69,7 +107,7 @@ where
/// # Errors /// # Errors
/// ///
/// Will error when the challenge errors, for example when the network adress cannot be bound. /// 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. /// 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 /// 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. /// 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 /// Challenges should not typically implement this themselves, if they want to customize the
/// admin interface, define [admin_interface](Self::adming_interface). /// admin interface, define [admin_interface](Self::admin_interface).
/// The hadmin interface is not part of the challenge and just for monitoring. /// The admin interface is not part of the challenge and just for monitoring.
/// ///
/// # Returns /// # Returns
/// ///
@ -89,15 +127,19 @@ where
if self.config().addr_admin.is_some() { if self.config().addr_admin.is_some() {
let vault = self.vault(); let vault = self.vault();
let config = self.config(); 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(); let challenge_handle = self.serve();
Ok(challenge_handle.await?) Ok(challenge_handle.await?)
} }
#[cfg(feature = "admin-interface")] #[cfg(feature = "admin-interface")]
async fn admin_interface(vault: VaultRef, config: Config) -> anyhow::Result<()> { async fn admin_interface(
admin::serve(vault, config).await?; text: Descriptions,
vault: VaultRef,
config: Config,
) -> anyhow::Result<()> {
admin::serve(text, vault, config).await?;
Ok(()) Ok(())
} }
} }

View File

@ -14,11 +14,18 @@
//! Wooly Vault is programmed asynchronously with [tokio] to be able to handle many contestants at //! Wooly Vault is programmed asynchronously with [tokio] to be able to handle many contestants at
//! once if needed. //! once if needed.
use self::vault::VaultRef;
pub mod challenge; pub mod challenge;
pub mod config; pub mod config;
pub mod vault; pub mod vault;
#[inline] #[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}") libpt::log::info!("Sending the secret to {addr}")
} }

View File

@ -3,20 +3,57 @@
//! This module provides a [`Vault`] struct that holds a secret and allows it to be shared safely //! 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. //! 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 std::sync::Arc;
use libpt::log::info;
use tokio::sync::Mutex;
/// A type alias for an [`Arc`] pointer to a [`Vault`] instance. /// 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. /// This type is used to share a [`Vault`] instance across multiple parts of the application.
pub type VaultRef = Arc<Vault>; pub type VaultRef = Arc<Vault>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Contestant {
ip: IpAddr,
}
/// A struct that holds a secret and provides methods for accessing it. /// 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. /// 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 { pub struct Vault {
/// The secret stored in the vault /// The secret stored in the vault
secret: String, secret: String,
contestants: Arc<Mutex<HashSet<Contestant>>>,
winners: Arc<Mutex<HashSet<Contestant>>>,
}
impl From<IpAddr> for Contestant {
fn from(value: IpAddr) -> Self {
Self { ip: value }
}
}
impl From<SocketAddr> 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 { impl Vault {
@ -30,6 +67,8 @@ impl Vault {
pub fn new(secret: &str) -> VaultRef { pub fn new(secret: &str) -> VaultRef {
let v = Self { let v = Self {
secret: secret.to_string(), secret: secret.to_string(),
contestants: Default::default(),
winners: Default::default(),
}; };
Arc::new(v) Arc::new(v)
} }
@ -42,4 +81,22 @@ impl Vault {
pub fn secret(&self) -> &str { pub fn secret(&self) -> &str {
&self.secret &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<Contestant> {
self.contestants.lock().await.clone()
}
pub async fn winners(&self) -> HashSet<Contestant> {
self.contestants.lock().await.clone()
}
} }