From 058432130628d00764663ecf819a9adc6793768d Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 8 Nov 2026 03:29:35 +0100 Subject: v5.0.0: protocol v5 and the browser frontend Protocol v5 replaces newline lines with bounded [u32 len][text] frames and REQ/RES/EVT envelopes. Requests carry correlation ids and the server replays the last 256 replies per connection, so a gateway retry cannot double-debit and a late reply cannot clobber a newer request. HELLO/PONG carry server time; crash rounds announce started_at and push coalesced 10 Hz ticks. Crash now commits sha256(seed) before a round and reveals the seed when it ends, so clients recompute the crash point. Adds the webd/ gateway: one blocking thread, one ClientCore and one pinned upstream TLS connection per session, an HttpOnly session cookie, takeover, and a 60-second grace window. The browser sends Command JSON and renders View JSON; it never speaks the game protocol or stores credentials. Adds the vanilla TypeScript browser app at /play/ (login, lobby, all four games, history, notices, crash verification), the nginx /play and /ws routes, and the headless DOM smoke test. webd and the app stay out of the TUI/GUI/server build graph. The client/server version moves to 5.0.0 in lockstep; v4 peers are refused at HELLO. --- src/bin/server/handler.rs | 47 ++++++++++++++++++++++++++++++++--------------- src/bin/server/tls.rs | 27 ++++++++++++--------------- 2 files changed, 44 insertions(+), 30 deletions(-) (limited to 'src/bin/server') diff --git a/src/bin/server/handler.rs b/src/bin/server/handler.rs index dffae69..e6b623f 100644 --- a/src/bin/server/handler.rs +++ b/src/bin/server/handler.rs @@ -1,6 +1,7 @@ use std::time::Duration; use crate::tls::TlsStream; +use zagan_bet::protocol::{event_line, parse_request, reply_line}; use zagan_bet::server::core::{ConnectionState, ServerCore}; /// How long the connection waits for a request before advancing timed games. @@ -30,24 +31,35 @@ pub fn handle_client_tls(mut stream: TlsStream) { } let mut state = ConnectionState::default(); + let mut event_seq = 0u64; println!("[TLS Client connected] {client_addr}"); 'connection: loop { - let mut line = String::new(); - match stream.read_line(&mut line) { + let mut frame = String::new(); + match stream.read_frame(&mut frame) { Ok(0) => break, Ok(_) => { - let line = line.trim(); - if !line.is_empty() { - let user = state.user().unwrap_or("guest").to_string(); - println!("[{user} @ {client_addr}] Received: {line}"); - let response = core.handle_message(&mut state, line); - if let Err(error) = stream.write_line(&response) { - eprintln!("Write error: {error}"); - break; + let user = state.user().unwrap_or("guest").to_string(); + println!("[{user} @ {client_addr}] Received: {frame}"); + match parse_request(&frame) { + Ok(request) => { + let response = core.handle_request(&mut state, &request); + let reply = reply_line(request.id, &response); + if let Err(error) = stream.write_frame(&reply) { + eprintln!("Write error: {error}"); + break; + } + let user = state.user().unwrap_or("guest"); + println!("[{user} @ {client_addr}] Sent: {reply}"); + } + Err(message) => { + let reply = reply_line(0, &format!("ERROR {message}")); + if let Err(error) = stream.write_frame(&reply) { + eprintln!("Write error: {error}"); + break; + } + eprintln!("Malformed request from {client_addr}: {reply}"); } - let user = state.user().unwrap_or("guest"); - println!("[{user} @ {client_addr}] Sent: {response}"); } } Err(error) @@ -65,12 +77,17 @@ pub fn handle_client_tls(mut stream: TlsStream) { } for push in core.poll(&state) { - if let Err(error) = stream.write_line(&push) { + event_seq = event_seq.wrapping_add(1); + let event = event_line(event_seq, &push); + if let Err(error) = stream.write_frame(&event) { eprintln!("Write error: {error}"); break 'connection; } - let user = state.user().unwrap_or("guest"); - println!("[{user} @ {client_addr}] Pushed: {push}"); + // Ticks are 10 Hz per live round; logging them floods the disk. + if !push.starts_with("crash.tick ") { + let user = state.user().unwrap_or("guest"); + println!("[{user} @ {client_addr}] Pushed: {event}"); + } } } diff --git a/src/bin/server/tls.rs b/src/bin/server/tls.rs index 420b5b4..eb94637 100644 --- a/src/bin/server/tls.rs +++ b/src/bin/server/tls.rs @@ -1,7 +1,7 @@ use std::io::{Read, Write}; use std::net::TcpStream; -use zagan_bet::protocol::LineBuffer; +use zagan_bet::protocol::{FrameBuffer, write_frame}; #[derive(Clone)] pub struct TlsServer { @@ -44,34 +44,31 @@ impl TlsServer { pub struct TlsStream { stream: native_tls::TlsStream, - lines: LineBuffer, + frames: FrameBuffer, } impl TlsStream { pub fn new(stream: native_tls::TlsStream) -> Self { Self { stream, - lines: LineBuffer::new(), + frames: FrameBuffer::new(), } } - pub fn read_line(&mut self, s: &mut String) -> std::io::Result { - match self.lines.read_line(&mut self.stream)? { - Some(line) => { - s.push_str(&line); - s.push('\n'); - Ok(line.len() + 1) + /// Read one protocol frame. `Ok(0)` is EOF with no partial frame. + pub fn read_frame(&mut self, out: &mut String) -> std::io::Result { + match self.frames.read_frame(&mut self.stream)? { + Some(frame) => { + let length = frame.len(); + out.push_str(&frame); + Ok(length) } None => Ok(0), } } - pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { - use std::io::Write; - self.stream.write_all(msg.as_bytes())?; - self.stream.write_all(b"\n")?; - self.stream.flush()?; - Ok(()) + pub fn write_frame(&mut self, msg: &str) -> std::io::Result<()> { + write_frame(&mut self.stream, msg) } pub fn peer_addr(&self) -> std::io::Result { -- cgit v1.2.3