From 8bd156b5fcb46b1d0de6f772692248e61950784a Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Sat, 7 Sep 2024 17:10:02 +0200 Subject: [PATCH] feat: groundwork for the admin interface --- Cargo.toml | 5 + data/www/admin.html | 244 +++++++++++++++++++++++++++++++++++++++++ data/www/styles.css | 76 +++++++++++++ src/challenge/admin.rs | 41 +++++++ src/challenge/c1.rs | 6 + src/challenge/c2.rs | 6 + src/challenge/c3.rs | 6 + src/challenge/mod.rs | 47 +++++++- src/config.rs | 5 + 9 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 data/www/admin.html create mode 100644 data/www/styles.css create mode 100644 src/challenge/admin.rs 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Bootstrap + + + + + +
+
+ + + + + Starter template + +
+ +
+

{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() } }