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.
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<String> {
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}");

View File

@ -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<String> {
vec![String::from(
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 {

View File

@ -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<String> {
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}");

View File

@ -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<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.
@ -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<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.
///
/// 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(())
}
}

View File

@ -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}")
}

View File

@ -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<Vault>;
#[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<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 {
@ -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<Contestant> {
self.contestants.lock().await.clone()
}
pub async fn winners(&self) -> HashSet<Contestant> {
self.contestants.lock().await.clone()
}
}