feat(meta): make datastructures ready for multiple challenges
cargo devel CI / cargo CI (push) Successful in 2m3s Details

This commit is contained in:
Christoph J. Scherr 2024-09-08 02:28:49 +02:00
parent 515e61dc45
commit f945eea904
11 changed files with 241 additions and 194 deletions

View File

@ -29,11 +29,8 @@ tokio = { version = "1.40.0", features = [
"rt",
"sync",
] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
warp = { version = "0.3.7", optional = true }
[features]
default = ["admin-interface"]
admin-interface = ["dep:warp", "dep:minijinja"]
default = ["meta-interface"]
meta-interface = ["dep:warp", "dep:minijinja"]

View File

@ -1,146 +0,0 @@
use std::fmt::Display;
use std::sync::Arc;
use anyhow::Result;
use libpt::log::error;
use libpt::log::info;
use libpt::log::tracing;
use libpt::log::warn;
use minijinja::context;
use minijinja::Environment;
use thiserror::Error;
use warp::http::StatusCode;
use warp::reject;
use warp::reply::Response;
use warp::Filter;
use crate::config::Config;
use crate::vault::VaultRef;
use super::Descriptions;
#[derive(Clone)]
pub struct Service<'tp> {
vault: VaultRef,
config: Config,
env: Environment<'tp>,
text: Descriptions,
}
impl<'tp> Service<'tp> {
fn new(
vault: VaultRef,
config: Config,
env: Environment<'tp>,
text: Descriptions,
) -> Arc<Self> {
Self {
vault,
config,
env,
text,
}
.into()
}
}
pub async fn serve(text: Descriptions, vault: VaultRef, config: Config) -> Result<()> {
let mut env = Environment::new();
env.add_template("index", include_str!("../../data/www/admin.html"))?;
let service = Service::new(vault, config, env, text);
let service2 = service.clone();
let routes = warp::path::end()
.map(move || service2.clone())
.and_then(overview)
.or(warp::path("styles.css").and_then(styles))
.recover(handle_rejection)
.with(warp::trace(|info| {
// Create a span using tracing macros
tracing::info_span!(
"admin",
method = %info.method(),
path = %info.path(),
)
}));
warp::serve(routes)
.run(service.config.addr_admin.unwrap())
.await;
warn!("exited the admin interface");
Ok(())
}
#[derive(Debug, Error)]
struct TemplateError(#[from] minijinja::Error);
impl reject::Reject for TemplateError {}
impl Display for TemplateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "something went wrong with producing this page")
}
}
async fn overview(serv: Arc<Service<'_>>) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
let contestants = serv
.vault
.contestants()
.await
.into_iter()
.collect::<Vec<_>>();
let winners = serv.vault.winners().await.into_iter().collect::<Vec<_>>();
let r = Response::new(
serv.env
.get_template("index")
.map_err(TemplateError::from)?
.render(context!(
title => "Wooly-Vault",
author => env!("CARGO_PKG_AUTHORS"),
year => "2024",
challenge_idx => serv.config.challenge,
challenge_title => serv.text.title(),
challenge_description => serv.text.description(),
challenge_hints => serv.text.hints(),
challenge_solution => serv.text.solution(),
contestants => serv.vault.contestants().await.iter().collect::<Vec<_>>(),
winners => serv.vault.winners().await.iter().collect::<Vec<_>>(),
contestants => contestants,
winners => winners,
contestants_amount => contestants.len(),
winners_amount => winners.len(),
))
.map_err(TemplateError::from)?
.into(),
);
Ok(Box::new(r))
}
async fn styles() -> Result<Box<dyn warp::Reply>, warp::Rejection> {
let r = Response::new(include_str!("../../data/www/styles.css").to_string().into());
Ok(Box::new(r))
}
async fn handle_rejection(
err: reject::Rejection,
) -> Result<impl warp::reply::Reply, std::convert::Infallible> {
let code;
let message;
info!("rejecting: {err:?}");
if err.is_not_found() {
code = StatusCode::NOT_FOUND;
message = "page not found";
} else if let Some(e) = err.find::<TemplateError>() {
error!("templating error: {e}");
code = StatusCode::INTERNAL_SERVER_ERROR;
message = "could not process data to make a page";
} else {
error!("unhandled rejection: {:?}", err);
code = StatusCode::INTERNAL_SERVER_ERROR;
message = "unhandled rejection";
}
Ok(warp::reply::with_status(message, code))
}

View File

@ -8,12 +8,13 @@ use libpt::log::warn;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
use super::{Challenge, Descriptions};
use super::{Challenge, ChallengeDesc};
use crate::config::Config;
use crate::has_won;
use crate::vault::VaultRef;
/// This struct holds the configuration and vault for the challenge.
#[derive(Clone, Debug)]
pub struct C1 {
config: Config,
vault: VaultRef,
@ -31,8 +32,8 @@ impl Challenge for C1 {
Self { config, vault }
}
fn text() -> Descriptions {
Descriptions {
fn text() -> ChallengeDesc {
ChallengeDesc {
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."),

View File

@ -10,12 +10,13 @@ use libpt::log::{info, warn};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use super::{Challenge, Descriptions};
use super::{Challenge, ChallengeDesc};
use crate::config::Config;
use crate::has_won;
use crate::vault::VaultRef;
/// This struct holds the configuration and vault for the challenge.
#[derive(Clone, Debug)]
pub struct C2 {
config: Config,
vault: VaultRef,
@ -56,8 +57,8 @@ impl Challenge for C2 {
fn new(config: Config, vault: VaultRef) -> Self {
Self { config, vault }
}
fn text() -> Descriptions {
Descriptions {
fn text() -> ChallengeDesc {
ChallengeDesc {
title: "TCP dialogue".to_string(),
hints: vec![String::from(
"TCP connect to 1337 and give me a special u16",

View File

@ -29,7 +29,7 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::time::Instant;
use super::{Challenge, Descriptions};
use super::{Challenge, ChallengeDesc};
use crate::config::Config;
use crate::has_won;
use crate::vault::VaultRef;
@ -40,7 +40,7 @@ pub const NEEDED_CORRECT: usize = 16;
pub const MILLIS_PER_QUESTION: u128 = 200;
/// Signifies the operations that the randomiser can come up with.
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
enum Operation {
Add,
Sub,
@ -90,6 +90,7 @@ impl Distribution<Operation> for Standard {
}
/// This struct holds the configuration and vault for the challenge.
#[derive(Clone, Debug)]
pub struct C3 {
config: Config,
vault: VaultRef,
@ -216,8 +217,8 @@ impl Challenge for C3 {
fn new(config: Config, vault: VaultRef) -> Self {
Self { config, vault }
}
fn text() -> Descriptions {
Descriptions {
fn text() -> ChallengeDesc {
ChallengeDesc {
title: "TCP math exam".to_string(),
hints: vec![
"TCP connect to 1337 and answer the questions.".to_string(),

View File

@ -5,26 +5,24 @@
use anyhow::anyhow;
use async_trait::async_trait;
use libpt::log::{error, info};
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 {
pub struct ChallengeDesc {
title: String,
hints: Vec<String>,
solution: String,
description: String,
}
impl Descriptions {
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
@ -33,7 +31,7 @@ impl Descriptions {
/// # Returns
///
/// A vector of strings containing hints for the challenge.
fn hints(&self) -> Vec<&str> {
pub fn hints(&self) -> Vec<&str> {
self.hints.iter().map(|a| a.as_ref()).collect()
}
/// Returns the solution to the challenge.
@ -44,7 +42,7 @@ impl Descriptions {
/// # Returns
///
/// A string containing the solution to the challenge.
fn solution(&self) -> &str {
pub fn solution(&self) -> &str {
&self.solution
}
/// Returns the description to the challenge.
@ -55,7 +53,7 @@ impl Descriptions {
/// # Returns
///
/// A string containing the description to the challenge.
fn description(&self) -> &str {
pub fn description(&self) -> &str {
&self.description
}
/// Returns the title to the challenge.
@ -65,7 +63,7 @@ impl Descriptions {
/// # Returns
///
/// A string containing the title to the challenge.
fn title(&self) -> &str {
pub fn title(&self) -> &str {
&self.title
}
}
@ -76,14 +74,18 @@ impl Descriptions {
#[async_trait]
pub trait Challenge
where
Self: Sized,
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() -> Descriptions;
fn text() -> ChallengeDesc;
/// Creates a new instance of the challenge with the given configuration and vault.
///
/// # Arguments
@ -122,24 +124,13 @@ where
/// # 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?;
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(())
}
}
@ -161,10 +152,11 @@ where
/// 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).setup_and_start().await?,
2 => c2::C2::new(config, vault).setup_and_start().await?,
3 => c3::C3::new(config, vault).setup_and_start().await?,
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"

View File

@ -18,6 +18,8 @@ use self::vault::VaultRef;
pub mod challenge;
pub mod config;
#[cfg(feature = "meta-interface")]
pub mod meta;
pub mod vault;
#[inline]

View File

@ -23,5 +23,7 @@ async fn main() -> Result<()> {
select_and_start(conf.challenge, conf, v).await?;
Ok(())
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await
}
}

78
src/meta/admin.rs Normal file
View File

@ -0,0 +1,78 @@
use std::fmt::Display;
use std::sync::Arc;
use anyhow::Result;
use libpt::log::error;
use libpt::log::info;
use libpt::log::tracing;
use libpt::log::warn;
use minijinja::context;
use minijinja::Environment;
use thiserror::Error;
use warp::http::StatusCode;
use warp::reject;
use warp::reject::Rejection;
use warp::reply::Reply;
use warp::reply::Response;
use warp::Filter;
use crate::config::Config;
use crate::vault::VaultRef;
use super::errors::TemplateError;
use super::Service;
impl<'tp> Service<'tp> {
pub fn admin_routes(
this: Arc<Self>,
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone + 'tp {
let serv = this.clone();
warp::path::end()
.map(move || serv.clone())
.and_then(overview)
.with(warp::trace(|info| {
// Create a span using tracing macros
tracing::info_span!(
"admin",
method = %info.method(),
path = %info.path(),
)
}))
}
}
async fn overview(serv: Arc<Service<'_>>) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
let challenge = serv.challenges[0].clone();
let contestants = serv
.vault
.contestants()
.await
.into_iter()
.collect::<Vec<_>>();
let winners = serv.vault.winners().await.into_iter().collect::<Vec<_>>();
let r = Response::new(
serv.env
.get_template("index")
.map_err(TemplateError::from)?
.render(context!(
title => "Wooly-Vault",
author => env!("CARGO_PKG_AUTHORS"),
year => "2024",
challenge_idx => serv.config.challenge,
challenge_title => challenge.title(),
challenge_description => challenge.description(),
challenge_hints => challenge.hints(),
challenge_solution => challenge.solution(),
contestants => serv.vault.contestants().await.iter().collect::<Vec<_>>(),
winners => serv.vault.winners().await.iter().collect::<Vec<_>>(),
contestants => contestants,
winners => winners,
contestants_amount => contestants.len(),
winners_amount => winners.len(),
))
.map_err(TemplateError::from)?
.into(),
);
Ok(Box::new(r))
}

39
src/meta/errors.rs Normal file
View File

@ -0,0 +1,39 @@
use std::fmt::Display;
use libpt::log::{error, info};
use thiserror::Error;
use warp::http::StatusCode;
use warp::reject;
#[derive(Debug, Error)]
pub struct TemplateError(#[from] minijinja::Error);
impl reject::Reject for TemplateError {}
impl Display for TemplateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "something went wrong with producing this page")
}
}
pub async fn handle_rejection(
err: reject::Rejection,
) -> Result<impl warp::reply::Reply, std::convert::Infallible> {
let code;
let message;
info!("rejecting: {err:?}");
if err.is_not_found() {
code = StatusCode::NOT_FOUND;
message = "page not found";
} else if let Some(e) = err.find::<TemplateError>() {
error!("templating error: {e}");
code = StatusCode::INTERNAL_SERVER_ERROR;
message = "could not process data to make a page";
} else {
error!("unhandled rejection: {:?}", err);
code = StatusCode::INTERNAL_SERVER_ERROR;
message = "unhandled rejection";
}
Ok(warp::reply::with_status(message, code))
}

80
src/meta/mod.rs Normal file
View File

@ -0,0 +1,80 @@
use std::sync::Arc;
use anyhow::Result;
use libpt::log::tracing;
use libpt::log::warn;
use minijinja::Environment;
use warp::reject::Rejection;
use warp::reply::Reply;
use warp::reply::Response;
use warp::Filter;
use crate::challenge::ChallengeDesc;
use crate::config::Config;
use crate::vault::VaultRef;
use self::errors::handle_rejection;
pub mod admin;
pub mod errors;
#[derive(Clone)]
pub struct Service<'tp> {
vault: VaultRef,
config: Config,
env: Environment<'tp>,
challenges: Vec<ChallengeDesc>,
}
impl<'tp> Service<'tp> {
fn new(
vault: VaultRef,
config: Config,
env: Environment<'tp>,
challenges: Vec<ChallengeDesc>,
) -> Arc<Self> {
Self {
vault,
config,
env,
challenges,
}
.into()
}
}
pub async fn serve(challenges: Vec<ChallengeDesc>, vault: VaultRef, config: Config) -> Result<()> {
let mut env = Environment::new();
env.add_template("index", include_str!("../../data/www/admin.html"))?;
let service = Service::new(vault, config, env, challenges);
let routes = Service::admin_routes(service.clone())
.or(Service::ressources_routes())
.recover(handle_rejection)
.with(warp::trace(|info| {
// Create a span using tracing macros
tracing::info_span!(
"admin",
method = %info.method(),
path = %info.path(),
)
}));
warp::serve(routes)
.run(service.config.addr_admin.unwrap())
.await;
warn!("exited the admin interface");
Ok(())
}
impl Service<'_> {
fn ressources_routes() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::path("styles.css").and_then(styles)
}
}
async fn styles() -> Result<Box<dyn warp::Reply>, warp::Rejection> {
let r = Response::new(include_str!("../../data/www/styles.css").to_string().into());
Ok(Box::new(r))
}