diff options
| -rw-r--r-- | Cargo.lock | 265 | ||||
| -rw-r--r-- | Cargo.toml | 14 | ||||
| -rw-r--r-- | src/main.rs | 249 | ||||
| -rw-r--r-- | static/style.css | 5 |
4 files changed, 521 insertions, 12 deletions
@@ -36,6 +36,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" dependencies = [ "axum-core", + "base64", "bytes", "form_urlencoded", "futures-util", @@ -55,8 +56,10 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", + "sha1", "sync_wrapper", "tokio", + "tokio-tungstenite", "tower", "tower-layer", "tower-service", @@ -131,12 +134,33 @@ dependencies = [ ] [[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] name = "bytes" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -160,6 +184,31 @@ dependencies = [ ] [[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -169,6 +218,16 @@ dependencies = [ ] [[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -199,6 +258,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -211,6 +276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-sink", "futures-task", "pin-project-lite", "pin-utils", @@ -218,6 +284,27 @@ dependencies = [ ] [[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -258,6 +345,12 @@ dependencies = [ ] [[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] name = "httparse" version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -369,6 +462,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] name = "miniz_oxide" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -396,9 +499,12 @@ dependencies = [ "axum", "axum-extra", "axum-macros", + "bytes", "cookie", "maud", + "rand", "tokio", + "tokio-tungstenite", "tower-http", "tracing", "tracing-subscriber", @@ -466,6 +572,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -507,6 +622,36 @@ dependencies = [ ] [[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -579,6 +724,17 @@ dependencies = [ ] [[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -639,6 +795,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -708,6 +884,31 @@ dependencies = [ ] [[package]] +name = "tokio-tungstenite" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -735,7 +936,14 @@ dependencies = [ "http", "http-body", "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -813,12 +1021,48 @@ dependencies = [ ] [[package]] +name = "tungstenite" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + +[[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -930,3 +1174,24 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] @@ -5,12 +5,20 @@ edition = "2021" [dependencies] anyhow = "1.0.95" -axum = "0.8.1" +axum = { version = "0.8.1", features = ["ws"] } axum-extra = "0.10.0" axum-macros = "0.5.0" +bytes = "1.9.0" cookie = { version = "0.18.1", features = ["percent-encode"] } maud = "0.26.0" -tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "process", "time"] } -tower-http = { version = "0.6.2", features = ["catch-panic", "trace"] } +rand = "0.8.5" +tokio = { version = "1.42.0", features = [ + "macros", + "rt-multi-thread", + "process", + "time", +] } +tokio-tungstenite = "0.26.1" +tower-http = { version = "0.6.2", features = ["catch-panic", "fs", "trace"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["tracing"] } diff --git a/src/main.rs b/src/main.rs index e078a32..0cc864f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,32 @@ -use std::net::SocketAddr; +use std::{collections::HashMap, net::SocketAddr, sync::Arc}; use anyhow::{Context, Result}; use axum::{ + body::Body, + extract::{ + ws::{Message, WebSocket}, + Path, State, WebSocketUpgrade, + }, http::StatusCode, response::{IntoResponse, Response}, - routing::get, + routing::{get, post}, Router, }; +use maud::{html, Markup, PreEscaped, DOCTYPE}; +use rand::random; +use tokio::sync::{ + broadcast::{self, Receiver, Sender}, + Mutex, +}; use tower_http::{ catch_panic::CatchPanicLayer, - trace::{self, TraceLayer}, + services::ServeDir, + trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}, }; -use tracing::{error, info, Level}; +use tracing::{error, info, trace, Level}; use tracing_subscriber::FmtSubscriber; +/// If a handler panics we run this function and return the [Response]. fn handle_panic(err: Box<dyn std::any::Any + Send + 'static>) -> Response { let details = if let Some(s) = err.downcast_ref::<String>() { s.clone() @@ -27,6 +40,213 @@ fn handle_panic(err: Box<dyn std::any::Any + Send + 'static>) -> Response { (StatusCode::INTERNAL_SERVER_ERROR).into_response() } +struct Game { + id: u64, + + is_started: bool, + + /// Sender to broadcast things + broadcast_tx: Sender<String>, + /// Broadcast receiver to send to WebSocket + broadcast_rx: Receiver<String>, +} + +impl Game { + fn get_html(&self) -> Markup { + let gid = self.id; + if !self.is_started { + return html! { + h1 { "Game" } + p { "Current game id is " (gid)} + div #messages {}; + button hx-post=(format!("/game/{gid}/start")) { "Start" } + }; + } else { + return html! { + h1 { "Playing Game" } + p { "Were playing here" } + }; + } + } +} + +struct ServerState { + games: HashMap<u64, Arc<Mutex<Game>>>, +} + +impl ServerState { + fn new_game(&mut self, gid: u64) { + let (tx, rx) = broadcast::channel(16); + let g = Game { + id: gid, + is_started: false, + broadcast_rx: rx, + broadcast_tx: tx, + }; + self.games.insert(gid, Arc::new(Mutex::new(g))); + } +} + +type Server = Arc<Mutex<ServerState>>; + +async fn route_index(State(st): State<Server>) -> Response { + let game_ids = st.lock().await.games.keys().cloned().collect::<Vec<_>>(); + + let game_list = if game_ids.is_empty() { + html! { "No current games"} + } else { + html! { + h3 { "Games" } + ul { + @for id in &game_ids { + li { a href=(format!("/game/{id}")) { (id) } } + } + } + } + }; + + let html = html! { + h1 { "Good game "} + (game_list) + + button hx-post="/game" { "Create game" } + }; + + html_response(html! { + (DOCTYPE) + head { + meta charset="utf-8"; + title { "Good Game" }; + meta name="viewport" content="width=device-width, initial-scale=1" {}; + script src="https://unpkg.com/htmx.org@2.0.4" {}; + link rel="stylesheet" type="text/css" href="static/style.css"; + } + body { + (html) + } + }) +} + +fn html_response(html: PreEscaped<String>) -> Response { + Response::builder() + .status(StatusCode::OK) + .body(Body::from(html.into_string())) + .expect("Failed to set body") +} + +async fn post_game(State(st): State<Server>) -> Response { + let gid = random::<u64>(); + + let _ = { + let mut s = st.lock().await; + s.new_game(gid); + }; + + Response::builder() + .header("hx-redirect", format!("/game/{gid}")) + .body(Body::empty()) + .expect("failed to build response") +} + +async fn start_game(Path(gid): Path<u64>, State(st): State<Server>) -> Response { + let lock = { + if let Some(g) = st.lock().await.games.get(&gid) { + g.clone() + } else { + return StatusCode::NOT_FOUND.into_response(); + } + }; + + let mut g = lock.lock().await; + + g.is_started = true; + let html = g.get_html(); + + g.broadcast_tx + .send(html! { section #content { (html) } }.into_string()) + .expect("failed to send"); + + StatusCode::OK.into_response() +} + +async fn get_game(Path(gid): Path<u64>, State(st): State<Server>) -> Response { + let game = { + if let Some(g) = st.lock().await.games.get(&gid) { + g.clone() + } else { + return (StatusCode::NOT_FOUND).into_response(); + } + }; + + let html = game.lock().await.get_html(); + + html_response(html! { + (DOCTYPE) + head { + meta charset="utf-8"; + title { "Good Game" }; + meta name="viewport" content="width=device-width, initial-scale=1" {}; + script src="https://unpkg.com/htmx.org@2.0.4" {}; + script src="https://unpkg.com/htmx-ext-ws@2.0.1/ws.js" {}; + + link rel="stylesheet" type="text/css" href="/static/style.css"; + } + body hx-ext="ws" ws-connect=(format!("/game/{gid}/ws")) { + footer { } + section #content { + (html) + } + footer { span { "Good Game" } } + } + }) +} + +async fn game_ws(ws: WebSocketUpgrade, Path(gid): Path<u64>, State(st): State<Server>) -> Response { + let game = { + let s = st.lock().await; + if let Some(g) = s.games.get(&gid) { + g.clone() + } else { + return StatusCode::NOT_FOUND.into_response(); + } + }; + + ws.on_upgrade(move |socket| handle_socket(socket, game)) + .into_response() +} + +async fn handle_socket(mut socket: WebSocket, game: Arc<Mutex<Game>>) { + if socket + .send(Message::Ping(bytes::Bytes::from("wat"))) + .await + .is_ok() + { + trace!("Pinged ws"); + } else { + println!("Could not send ping!"); + return; + } + { + game.lock() + .await + .broadcast_tx + .send( + html! { + div #messages hx-swap-oob="beforeend" { p { "Someone joined" } } + } + .into_string(), + ) + .unwrap(); + } + + let mut rx = { game.lock().await.broadcast_rx.resubscribe() }; + while let Ok(a) = rx.recv().await { + trace!(a, "send "); + let res = socket.send(a.into()).await; + trace!(?res); + } +} + #[tokio::main] async fn main() -> Result<()> { let subscriber = FmtSubscriber::builder() @@ -35,16 +255,27 @@ async fn main() -> Result<()> { tracing::subscriber::set_global_default(subscriber) .expect("set global default subscriber failed"); + let mut server_state = ServerState { + games: HashMap::new(), + }; + server_state.new_game(123); + let app = Router::new() - .route("/", get(|| async { "wat" })) + .route("/", get(route_index)) + .route("/game", post(post_game)) + .route("/game/{gid}", get(get_game)) + .route("/game/{gid}/start", post(start_game)) + .route("/game/{gid}/ws", get(game_ws)) + .nest_service("/static", ServeDir::new("static")) .layer(CatchPanicLayer::custom(handle_panic)) .layer( TraceLayer::new_for_http() - .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) - .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), - ); + .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) + .on_response(DefaultOnResponse::new().level(Level::INFO)), + ) + .with_state(Arc::new(Mutex::new(server_state))); - let addr = "0.0.0.0:4800"; + let addr = "192.168.0.106:4800"; info!("Listening on {addr}"); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve( diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..e61cc02 --- /dev/null +++ b/static/style.css @@ -0,0 +1,5 @@ +:root { +} + +body { +} |
