diff --git a/Cargo.toml b/Cargo.toml
index 0c962fa..249355d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,7 @@ libpt = { version = "0.7.1", features = ["cli", "log"] }
rand = "0.8.5"
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.128"
+tide = { version = "0.16.0", optional = true }
tokio = { version = "1.40.0", features = [
"macros",
"net",
@@ -27,3 +28,7 @@ tokio = { version = "1.40.0", features = [
"rt",
"sync",
] }
+
+[features]
+default = ["admin-interface"]
+admin-interface = ["dep:tide"]
diff --git a/data/www/admin.html b/data/www/admin.html
new file mode 100644
index 0000000..c167208
--- /dev/null
+++ b/data/www/admin.html
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+
+
+
+
+
+ Starter Template ยท Bootstrap v5.3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Toggle theme
+
+
+
+
+
+
+
+ Light
+
+
+
+
+
+
+
+
+
+
+ Dark
+
+
+
+
+
+
+
+
+
+
+ Auto
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bootstrap
+
+
+
+
+
+
+
+
+
+ {TITLE} Admin Interface
+ Quickly and easily get started with Bootstrap's compiled, production-ready files with
+ this barebones example featuring some basic HTML and helpful links. Download all our examples to get started.
+
+
+
+
+
+
+
+
+
Starter projects
+
Ready to go beyond the starter template? Check out these open source projects that you can quickly
+ duplicate to a new GitHub repository.
+
+
+
+
+
Guides
+
Read more detailed instructions and documentation on using or contributing to Bootstrap.
+
+
+
+
+
+ Created by the Bootstrap team · © 2024
+
+
+
+
+
+
+
diff --git a/data/www/styles.css b/data/www/styles.css
new file mode 100644
index 0000000..c36032a
--- /dev/null
+++ b/data/www/styles.css
@@ -0,0 +1,76 @@
+ .bd-placeholder-img {
+ font-size: 1.125rem;
+ text-anchor: middle;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ }
+
+ @media (min-width: 768px) {
+ .bd-placeholder-img-lg {
+ font-size: 3.5rem;
+ }
+ }
+
+ .b-example-divider {
+ width: 100%;
+ height: 3rem;
+ background-color: rgba(0, 0, 0, .1);
+ border: solid rgba(0, 0, 0, .15);
+ border-width: 1px 0;
+ box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
+ }
+
+ .b-example-vr {
+ flex-shrink: 0;
+ width: 1.5rem;
+ height: 100vh;
+ }
+
+ .bi {
+ vertical-align: -.125em;
+ fill: currentColor;
+ }
+
+ .nav-scroller {
+ position: relative;
+ z-index: 2;
+ height: 2.75rem;
+ overflow-y: hidden;
+ }
+
+ .nav-scroller .nav {
+ display: flex;
+ flex-wrap: nowrap;
+ padding-bottom: 1rem;
+ margin-top: -1px;
+ overflow-x: auto;
+ text-align: center;
+ white-space: nowrap;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .btn-bd-primary {
+ --bd-violet-bg: #712cf9;
+ --bd-violet-rgb: 112.520718, 44.062154, 249.437846;
+
+ --bs-btn-font-weight: 600;
+ --bs-btn-color: var(--bs-white);
+ --bs-btn-bg: var(--bd-violet-bg);
+ --bs-btn-border-color: var(--bd-violet-bg);
+ --bs-btn-hover-color: var(--bs-white);
+ --bs-btn-hover-bg: #6528e0;
+ --bs-btn-hover-border-color: #6528e0;
+ --bs-btn-focus-shadow-rgb: var(--bd-violet-rgb);
+ --bs-btn-active-color: var(--bs-btn-hover-color);
+ --bs-btn-active-bg: #5a23c8;
+ --bs-btn-active-border-color: #5a23c8;
+ }
+
+ .bd-mode-toggle {
+ z-index: 1500;
+ }
+
+ .bd-mode-toggle .dropdown-menu .active .bi {
+ display: block !important;
+ }
diff --git a/src/challenge/admin.rs b/src/challenge/admin.rs
new file mode 100644
index 0000000..7ccee5f
--- /dev/null
+++ b/src/challenge/admin.rs
@@ -0,0 +1,41 @@
+use anyhow::Result;
+use serde;
+use tide::http::mime;
+use tide::prelude::*;
+use tide::Request;
+use tide::Response;
+
+use crate::config::Config;
+use crate::vault::VaultRef;
+
+#[derive(Clone)]
+pub struct Interface {
+ vault: VaultRef,
+ config: Config,
+}
+
+pub async fn serve(vault: VaultRef, config: Config) -> Result<()> {
+ let mut app = tide::with_state(Interface { vault, config });
+ app.at("/").get(overview);
+ app.at("/styles.css").get(styles);
+ app.listen("127.0.0.1:8000").await?;
+ Ok(())
+}
+
+async fn overview(req: Request) -> tide::Result {
+ let r = Response::builder(200)
+ .content_type(mime::HTML)
+ .body(format!(
+ include_str!("../../data/www/admin.html"),
+ TITLE = "Wooly-Vault"
+ ));
+
+ Ok(r.into())
+}
+
+async fn styles(_req: Request) -> tide::Result {
+ let r = Response::builder(200)
+ .content_type(mime::CSS)
+ .body(include_str!("../../data/www/styles.css"));
+ Ok(r.into())
+}
diff --git a/src/challenge/c1.rs b/src/challenge/c1.rs
index f6737b4..ff81dfb 100644
--- a/src/challenge/c1.rs
+++ b/src/challenge/c1.rs
@@ -21,6 +21,12 @@ pub struct C1 {
#[async_trait]
impl Challenge for C1 {
+ fn config(&self) -> Config {
+ self.config.clone()
+ }
+ fn vault(&self) -> VaultRef {
+ self.vault.clone()
+ }
fn new(config: Config, vault: VaultRef) -> Self {
info!("Solution: {}", Self::solution());
Self { config, vault }
diff --git a/src/challenge/c2.rs b/src/challenge/c2.rs
index 39aa30b..ca948a4 100644
--- a/src/challenge/c2.rs
+++ b/src/challenge/c2.rs
@@ -47,6 +47,12 @@ impl C2 {
#[async_trait]
impl Challenge for C2 {
+ fn config(&self) -> Config {
+ self.config.clone()
+ }
+ fn vault(&self) -> VaultRef {
+ self.vault.clone()
+ }
fn new(config: Config, vault: VaultRef) -> Self {
info!("Solution: {}", Self::solution());
Self { config, vault }
diff --git a/src/challenge/c3.rs b/src/challenge/c3.rs
index 55ae1f9..0f3ba49 100644
--- a/src/challenge/c3.rs
+++ b/src/challenge/c3.rs
@@ -207,6 +207,12 @@ impl C3 {
#[async_trait]
impl Challenge for C3 {
+ fn config(&self) -> Config {
+ self.config.clone()
+ }
+ fn vault(&self) -> VaultRef {
+ self.vault.clone()
+ }
fn new(config: Config, vault: VaultRef) -> Self {
info!("Solution: {}", Self::solution());
Self { config, vault }
diff --git a/src/challenge/mod.rs b/src/challenge/mod.rs
index ed9f886..dbc5bc2 100644
--- a/src/challenge/mod.rs
+++ b/src/challenge/mod.rs
@@ -9,6 +9,9 @@ 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;
@@ -21,6 +24,10 @@ pub trait Challenge
where
Self: Sized,
{
+ /// Getter for the [vault](VaultRef).
+ fn vault(&self) -> VaultRef;
+ /// Getter for the [Config].
+ fn config(&self) -> Config;
/// Creates a new instance of the challenge with the given configuration and vault.
///
/// # Arguments
@@ -34,7 +41,7 @@ where
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 host thinks they need
+ /// 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
@@ -57,12 +64,42 @@ where
///
/// # Returns
///
- /// A result indicating whether the challenge was successful ended.
+ /// 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::adming_interface).
+ /// The hadmin 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(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?;
+ Ok(())
+ }
}
/// Selects a challenge by index and serves it with the given configuration and vault.
@@ -83,9 +120,9 @@ where
/// served errors.
pub async fn select_and_start(index: u16, config: Config, vault: VaultRef) -> anyhow::Result<()> {
match index {
- 1 => c1::C1::new(config, vault).serve().await?,
- 2 => c2::C2::new(config, vault).serve().await?,
- 3 => c3::C3::new(config, vault).serve().await?,
+ 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"
diff --git a/src/config.rs b/src/config.rs
index d2aad68..1270e28 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -50,6 +50,9 @@ pub struct Config {
pub challenge: u16,
/// Network address to host the challenge on
pub addr: SocketAddr,
+ /// Network address to host the challenge on
+ #[arg(short = 'a', long = "admin")]
+ pub addr_admin: Option,
#[command(flatten)]
pub verbosity: VerbosityLevel,
}
@@ -60,6 +63,7 @@ impl Default for Config {
challenge: 1,
addr: SocketAddr::from_str("127.0.0.1:1337").unwrap(),
verbosity: VerbosityLevel::default(),
+ addr_admin: Option::default(),
}
}
}
@@ -69,6 +73,7 @@ impl Debug for Config {
f.debug_struct("Config")
.field("addr", &self.addr)
.field("verbosity", &self.verbosity.level())
+ .field("admin", &self.addr_admin)
.finish()
}
}