Milestone 1
This commit is contained in:
1140
Cargo.lock
generated
Normal file
1140
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"core_protocol",
|
||||
"server_node",
|
||||
"client_node"
|
||||
]
|
||||
resolver = "2"
|
||||
@@ -2,31 +2,31 @@
|
||||
**Goal:** Initialize the project and establish the shared language between client and server.
|
||||
|
||||
### 1. Workspace Setup
|
||||
- [ ] Initialize the root Cargo workspace: `cargo init --vcs none` (delete `src/`). Create a root `Cargo.toml` with `[workspace] members = ["core_protocol", "server_node", "client_node"]`.
|
||||
- [ ] **AI Context Trap (File Structure):** Strictly adhere to the directory layout and module hierarchy defined in `File_Structure.md`. Do not invent new file paths or module names; map every new crate and file exactly to the blueprint.
|
||||
- [ ] Create crates: `cargo new --lib core_protocol`, `cargo new --bin server_node`, `cargo new --bin client_node`.
|
||||
- [ ] Add strict lints (`#![forbid(unsafe_code)]`, etc.) to the root workspace or individual `lib.rs`/`main.rs` files.
|
||||
- [ ] **Dependencies (`core_protocol`):** Add `serde`, `bincode`, `uuid`, `chrono`, `thiserror`, `secrecy` (for zeroing sensitive keys).
|
||||
- [ ] **Dependencies (`server_node`):** Add `tokio` (full), `tracing`, `tracing-subscriber`, `anyhow`, `dashmap`.
|
||||
- [ ] **Dependencies (`client_node`):** Add `tokio` (rt-multi-thread), `tracing`, `tracing-subscriber`, `anyhow`.
|
||||
- [x] Initialize the root Cargo workspace: `cargo init --vcs none` (delete `src/`). Create a root `Cargo.toml` with `[workspace] members = ["core_protocol", "server_node", "client_node"]`.
|
||||
- [x] **AI Context Trap (File Structure):** Strictly adhere to the directory layout and module hierarchy defined in `File_Structure.md`. Do not invent new file paths or module names; map every new crate and file exactly to the blueprint.
|
||||
- [x] Create crates: `cargo new --lib core_protocol`, `cargo new --bin server_node`, `cargo new --bin client_node`.
|
||||
- [x] Add strict lints (`#![forbid(unsafe_code)]`, etc.) to the root workspace or individual `lib.rs`/`main.rs` files.
|
||||
- [x] **Dependencies (`core_protocol`):** Add `serde`, `bincode`, `uuid`, `chrono`, `thiserror`, `secrecy` (for zeroing sensitive keys).
|
||||
- [x] **Dependencies (`server_node`):** Add `tokio` (full), `tracing`, `tracing-subscriber`, `anyhow`, `dashmap`.
|
||||
- [x] **Dependencies (`client_node`):** Add `tokio` (rt-multi-thread), `tracing`, `tracing-subscriber`, `anyhow`.
|
||||
|
||||
### 2. Protocol Definitions (`core_protocol`)
|
||||
- [ ] Create `src/tcp_events.rs`. Define `enum TcpEvent { AuthRequest { username: String, ... }, AuthResponse { session_token: u32, ... }, ChannelJoin { ... }, ChatMessage { ... } }` with `#[derive(Serialize, Deserialize)]`.
|
||||
- [ ] Create `src/udp_packets.rs`. Define `struct VoicePacketHeader { pub session_token: u32, pub sequence_num: u64, pub timestamp: u64 }` with `#[derive(Serialize, Deserialize)]`.
|
||||
- [ ] Create `src/constants.rs`. Define `pub const SAMPLE_RATE: u32 = 48000;`, `pub const FRAME_SIZE: usize = 960;`, `pub const TCP_PORT: u16 = 8080;`.
|
||||
- [x] Create `src/tcp_events.rs`. Define `enum TcpEvent { AuthRequest { username: String, ... }, AuthResponse { session_token: u32, ... }, ChannelJoin { ... }, ChatMessage { ... } }` with `#[derive(Serialize, Deserialize)]`.
|
||||
- [x] Create `src/udp_packets.rs`. Define `struct VoicePacketHeader { pub session_token: u32, pub sequence_num: u64, pub timestamp: u64 }` with `#[derive(Serialize, Deserialize)]`.
|
||||
- [x] Create `src/constants.rs`. Define `pub const SAMPLE_RATE: u32 = 48000;`, `pub const FRAME_SIZE: usize = 960;`, `pub const TCP_PORT: u16 = 8080;`.
|
||||
|
||||
### 3. TCP Handshake (`server_node` & `client_node`)
|
||||
- [ ] **Server:** In `server_node/src/main.rs`, initialize `tokio::net::TcpListener::bind("0.0.0.0:8080")`.
|
||||
- [ ] **Server:** Spawn a new `tokio::spawn(async move { ... })` for each incoming `TcpStream`.
|
||||
- [ ] **Client:** In `client_node/src/network/control.rs`, implement `TcpStream::connect("127.0.0.1:8080")`.
|
||||
- [ ] **AI Context Trap (TCP Framing):** Raw TCP streams suffer from fragmentation. Do NOT attempt to manually buffer bytes. You must use `tokio_util::codec::LengthDelimitedCodec` (with `tokio_serde` and `bincode`) to abstract the frame boundaries cleanly.
|
||||
- [x] **Server:** In `server_node/src/main.rs`, initialize `tokio::net::TcpListener::bind("0.0.0.0:8080")`.
|
||||
- [x] **Server:** Spawn a new `tokio::spawn(async move { ... })` for each incoming `TcpStream`.
|
||||
- [x] **Client:** In `client_node/src/network/control.rs`, implement `TcpStream::connect("127.0.0.1:8080")`.
|
||||
- [x] **AI Context Trap (TCP Framing):** Raw TCP streams suffer from fragmentation. Do NOT attempt to manually buffer bytes. You must use `tokio_util::codec::LengthDelimitedCodec` (with `tokio_serde` and `bincode`) to abstract the frame boundaries cleanly.
|
||||
|
||||
### 4. Login Logic & State
|
||||
- [ ] **Server State:** Create `server_node/src/state.rs`. Define a `DashMap<u32, UserState>` to store active session tokens.
|
||||
- [ ] **Authentication Flow:** Client sends `TcpEvent::AuthRequest`. Server generates a random `u32` session token, stores it in `DashMap`, and returns `TcpEvent::AuthResponse`.
|
||||
- [ ] **Validation:** Ensure the server actively drops the connection if the client sends invalid or excessively large payloads.
|
||||
- [x] **Server State:** Create `server_node/src/state.rs`. Define a `DashMap<u32, UserState>` to store active session tokens.
|
||||
- [x] **Authentication Flow:** Client sends `TcpEvent::AuthRequest`. Server generates a random `u32` session token, stores it in `DashMap`, and returns `TcpEvent::AuthResponse`.
|
||||
- [x] **Validation:** Ensure the server actively drops the connection if the client sends invalid or excessively large payloads.
|
||||
|
||||
### 5. Observability (Logging)
|
||||
- [ ] **Initialization:** In both binaries' `main.rs`, call `tracing_subscriber::fmt::init()`.
|
||||
- [ ] **Implementation:** Replace all `println!` calls with `tracing::info!`, `tracing::warn!`, or `tracing::error!`.
|
||||
- [ ] **Tracing Context:** Use `#[tracing::instrument]` on core TCP handler functions to automatically log client IPs and session IDs.
|
||||
- [x] **Initialization:** In both binaries' `main.rs`, call `tracing_subscriber::fmt::init()`.
|
||||
- [x] **Implementation:** Replace all `println!` calls with `tracing::info!`, `tracing::warn!`, or `tracing::error!`.
|
||||
- [x] **Tracing Context:** Use `#[tracing::instrument]` on core TCP handler functions to automatically log client IPs and session IDs.
|
||||
14
client_node/Cargo.toml
Normal file
14
client_node/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "client_node"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.102"
|
||||
core_protocol = { version = "0.1.0", path = "../core_protocol" }
|
||||
futures = "0.3.32"
|
||||
tokio = { version = "1.52.1", features = ["rt-multi-thread", "net", "macros"] }
|
||||
tokio-serde = { version = "0.9.0", features = ["bincode"] }
|
||||
tokio-util = { version = "0.7.18", features = ["codec"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.23"
|
||||
24
client_node/src/main.rs
Normal file
24
client_node/src/main.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
//! Client Node entry point.
|
||||
//!
|
||||
//! This module initializes the desktop client application, sets up the Tokio
|
||||
//! background thread for networking, and eventually binds to the UI framework.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::all, clippy::pedantic)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod network;
|
||||
|
||||
use tracing::{error, info};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
info!("Starting client node...");
|
||||
|
||||
if let Err(e) = network::control::connect_and_auth("TestUser").await {
|
||||
error!("Connection error: {:?}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
72
client_node/src/network/control.rs
Normal file
72
client_node/src/network/control.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
//! Reliable TCP connectivity logic.
|
||||
//!
|
||||
//! Implements the client-side TCP connection handling, including framing,
|
||||
//! serialization, and the initial authentication handshake.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use core_protocol::constants;
|
||||
use core_protocol::tcp_events::TcpEvent;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_serde::formats::Bincode;
|
||||
use tokio_serde::SymmetricallyFramed;
|
||||
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// A type alias for the symmetrically framed TCP stream to simplify syntax.
|
||||
type FramedStream = SymmetricallyFramed<
|
||||
Framed<TcpStream, LengthDelimitedCodec>,
|
||||
TcpEvent,
|
||||
Bincode<TcpEvent, TcpEvent>,
|
||||
>;
|
||||
|
||||
/// Connects to the server and performs the initial authentication handshake.
|
||||
///
|
||||
/// This establishes a length-delimited TCP connection to prevent fragmentation,
|
||||
/// sends an `AuthRequest` with the given username, and awaits the `AuthResponse`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `username` - The requested display name for this client.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an `anyhow::Result` if the TCP socket cannot connect, if the serialization/
|
||||
/// deserialization fails, or if the server closes the connection prematurely.
|
||||
pub async fn connect_and_auth(username: &str) -> Result<()> {
|
||||
let addr = format!("127.0.0.1:{}", constants::TCP_PORT);
|
||||
info!("Connecting to server at {}...", addr);
|
||||
|
||||
let stream = TcpStream::connect(&addr).await.context("Failed to connect to server")?;
|
||||
info!("Connected!");
|
||||
|
||||
// Construct the codec pipeline exactly mirroring the server's configuration
|
||||
// to ensure reliable packet framing.
|
||||
let length_delimited = Framed::new(stream, LengthDelimitedCodec::new());
|
||||
let mut framed: FramedStream = SymmetricallyFramed::new(
|
||||
length_delimited,
|
||||
Bincode::<TcpEvent, TcpEvent>::default(),
|
||||
);
|
||||
|
||||
let auth_req = TcpEvent::AuthRequest {
|
||||
username: username.to_string(),
|
||||
};
|
||||
|
||||
framed.send(auth_req).await.context("Failed to send AuthRequest")?;
|
||||
info!("Sent AuthRequest for user: {}", username);
|
||||
|
||||
if let Some(response) = framed.next().await {
|
||||
let response = response.context("Failed to deserialize response")?;
|
||||
|
||||
match response {
|
||||
TcpEvent::AuthResponse { session_token } => {
|
||||
info!("Successfully authenticated! Session token: {}", session_token);
|
||||
}
|
||||
_ => {
|
||||
warn!("Received unexpected event instead of AuthResponse");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Connection closed before receiving AuthResponse");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
6
client_node/src/network/mod.rs
Normal file
6
client_node/src/network/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! Internet connectivity modules.
|
||||
//!
|
||||
//! This module isolates all remote networking logic from the UI and audio engines.
|
||||
//! It exposes the necessary interfaces to manage TCP control lanes and UDP streams.
|
||||
|
||||
pub mod control;
|
||||
12
core_protocol/Cargo.toml
Normal file
12
core_protocol/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "core_protocol"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.3"
|
||||
chrono = { version = "0.4.44", features = ["serde"] }
|
||||
secrecy = { version = "0.10.3", features = ["serde"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
thiserror = "2.0.18"
|
||||
uuid = { version = "1.23.1", features = ["v4", "serde"] }
|
||||
15
core_protocol/src/constants.rs
Normal file
15
core_protocol/src/constants.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
//! Fixed system-wide specifications.
|
||||
//!
|
||||
//! Contains constants required for hardware configuration, network binding,
|
||||
//! and math routines such as audio framing lengths.
|
||||
|
||||
/// The uniform audio sampling rate (48 kHz).
|
||||
/// We lock the entire system to 48kHz because Opus performs optimally at this rate.
|
||||
pub const SAMPLE_RATE: u32 = 48000;
|
||||
|
||||
/// The exact number of audio samples per 20ms frame at 48kHz.
|
||||
/// Opus strictly requires a specific frame size (like 20ms) to function correctly.
|
||||
pub const FRAME_SIZE: usize = 960;
|
||||
|
||||
/// The default TCP port used for reliable control lane communication.
|
||||
pub const TCP_PORT: u16 = 8080;
|
||||
13
core_protocol/src/lib.rs
Normal file
13
core_protocol/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
//! Core Protocol definitions for the Voice App.
|
||||
//!
|
||||
//! This module defines the foundational types and constants shared between the
|
||||
//! client and server nodes. It ensures that both ends of the connection speak the
|
||||
//! exact same structural language for serialization/deserialization.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::all, clippy::pedantic)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
pub mod constants;
|
||||
pub mod tcp_events;
|
||||
pub mod udp_packets;
|
||||
31
core_protocol/src/tcp_events.rs
Normal file
31
core_protocol/src/tcp_events.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! TCP Control Lane events.
|
||||
//!
|
||||
//! Defines the reliable commands sent over the TCP connection for state
|
||||
//! synchronization, such as authentication and text chat.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents all possible events sent over the reliable TCP control lane.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum TcpEvent {
|
||||
/// Initial request from the client containing their chosen username.
|
||||
AuthRequest {
|
||||
/// The username requested by the client.
|
||||
username: String,
|
||||
},
|
||||
/// The server's response confirming authentication and granting a session token.
|
||||
AuthResponse {
|
||||
/// A unique, ephemeral ID assigned to the client for the duration of the session.
|
||||
session_token: u32,
|
||||
},
|
||||
/// A request to join a specific voice channel.
|
||||
ChannelJoin {
|
||||
/// The unique ID of the target channel.
|
||||
channel_id: u32,
|
||||
},
|
||||
/// A text message intended for the current channel's chat.
|
||||
ChatMessage {
|
||||
/// The raw text message.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
20
core_protocol/src/udp_packets.rs
Normal file
20
core_protocol/src/udp_packets.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
//! UDP High-speed Voice Packet structures.
|
||||
//!
|
||||
//! Defines the headers and payload structures for the low-latency voice data
|
||||
//! sent over UDP.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The header attached to every UDP voice frame.
|
||||
///
|
||||
/// We separate this from the payload to allow the server to rapidly route packets
|
||||
/// using just the `session_token` without fully deserializing the heavy audio data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VoicePacketHeader {
|
||||
/// The unique session ID matching the client's TCP authentication token.
|
||||
pub session_token: u32,
|
||||
/// A strictly increasing sequence number to detect dropped or out-of-order packets.
|
||||
pub sequence_num: u64,
|
||||
/// The timestamp of the packet generation, used to align jitter buffers.
|
||||
pub timestamp: u64,
|
||||
}
|
||||
15
server_node/Cargo.toml
Normal file
15
server_node/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "server_node"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.102"
|
||||
core_protocol = { version = "0.1.0", path = "../core_protocol" }
|
||||
dashmap = "6.1.0"
|
||||
futures = "0.3.32"
|
||||
tokio = { version = "1.52.1", features = ["full"] }
|
||||
tokio-serde = { version = "0.9.0", features = ["bincode"] }
|
||||
tokio-util = { version = "0.7.18", features = ["codec"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.23"
|
||||
43
server_node/src/main.rs
Normal file
43
server_node/src/main.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
//! Server Node entry point.
|
||||
//!
|
||||
//! This module initializes the headless relay server, binds the network listeners,
|
||||
//! and spawns isolated tasks for each connected TCP client.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::all, clippy::pedantic)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod state;
|
||||
mod tcp_router;
|
||||
|
||||
use core_protocol::constants;
|
||||
use state::AppState;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use tracing::{error, info};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
info!("Starting server node...");
|
||||
|
||||
let state = Arc::new(AppState::new());
|
||||
|
||||
let addr = format!("0.0.0.0:{}", constants::TCP_PORT);
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
info!("TCP Listener bound to {}", addr);
|
||||
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((stream, _)) => {
|
||||
let state_clone = state.clone();
|
||||
tokio::spawn(async move {
|
||||
tcp_router::handle_connection(stream, state_clone).await;
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to accept connection: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
server_node/src/state.rs
Normal file
49
server_node/src/state.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! Concurrent server state management.
|
||||
//!
|
||||
//! This module defines the global application state shared across all active
|
||||
//! Tokio tasks. It heavily relies on lock-free and fine-grained locking primitives
|
||||
//! like `DashMap` and `AtomicU32` to ensure high performance without stuttering.
|
||||
|
||||
use dashmap::DashMap;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
/// Represents the session data for a single connected user.
|
||||
#[allow(dead_code)]
|
||||
pub struct UserState {
|
||||
/// The client-chosen username for display in channels.
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
/// The global application state tracking all connections and rooms.
|
||||
pub struct AppState {
|
||||
/// A highly concurrent hash map linking active session tokens to their user data.
|
||||
pub active_users: DashMap<u32, UserState>,
|
||||
/// A simple atomic counter for issuing unique sequential session tokens.
|
||||
next_token: AtomicU32,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// Creates a new, empty application state.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_users: DashMap::new(),
|
||||
next_token: AtomicU32::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a strictly unique, monotonically increasing session token.
|
||||
///
|
||||
/// We use `Ordering::Relaxed` because we only need atomicity on the counter
|
||||
/// itself, not strict memory ordering with other operations.
|
||||
#[must_use]
|
||||
pub fn generate_token(&self) -> u32 {
|
||||
self.next_token.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
95
server_node/src/tcp_router.rs
Normal file
95
server_node/src/tcp_router.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
//! Reliable TCP routing and connection management.
|
||||
//!
|
||||
//! This module implements the reliable control lane for client communication.
|
||||
//! It handles the initial authentication handshake and routes incoming TCP events
|
||||
//! from the length-delimited codec to the broader application state.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use core_protocol::tcp_events::TcpEvent;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_serde::formats::Bincode;
|
||||
use tokio_serde::SymmetricallyFramed;
|
||||
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||
use tracing::{error, info, instrument, warn};
|
||||
|
||||
use crate::state::AppState;
|
||||
use crate::state::UserState;
|
||||
|
||||
/// A type alias for the heavily-nested framed stream type, combining `LengthDelimitedCodec` and `Bincode`.
|
||||
type FramedStream = SymmetricallyFramed<
|
||||
Framed<TcpStream, LengthDelimitedCodec>,
|
||||
TcpEvent,
|
||||
Bincode<TcpEvent, TcpEvent>,
|
||||
>;
|
||||
|
||||
/// Handles the lifecycle of a newly connected client's TCP stream.
|
||||
///
|
||||
/// This spans an instrumented task for the connection, setting up the necessary
|
||||
/// framers and codecs before entering the event loop.
|
||||
#[instrument(skip(stream, state))]
|
||||
pub async fn handle_connection(stream: TcpStream, state: Arc<AppState>) {
|
||||
let peer_addr = match stream.peer_addr() {
|
||||
Ok(addr) => addr,
|
||||
Err(e) => {
|
||||
error!("Failed to get peer address: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
info!("New connection from {}", peer_addr);
|
||||
|
||||
// We pad the TCP stream with a length-delimited codec to guarantee frame boundaries,
|
||||
// avoiding fragmentation issues common in raw TCP sockets.
|
||||
let length_delimited = Framed::new(stream, LengthDelimitedCodec::new());
|
||||
let mut framed: FramedStream = SymmetricallyFramed::new(
|
||||
length_delimited,
|
||||
Bincode::<TcpEvent, TcpEvent>::default(),
|
||||
);
|
||||
|
||||
if let Err(e) = process_connection(&mut framed, state).await {
|
||||
warn!("Connection closed with error: {:?}", e);
|
||||
} else {
|
||||
info!("Connection closed cleanly");
|
||||
}
|
||||
}
|
||||
|
||||
/// The inner event loop that processes deserialized `TcpEvent`s from the client.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an `anyhow::Result` if deserialization fails, the connection drops unexpectedly,
|
||||
/// or a serialization error occurs when transmitting a response.
|
||||
async fn process_connection(framed: &mut FramedStream, state: Arc<AppState>) -> Result<()> {
|
||||
while let Some(event) = framed.next().await {
|
||||
let event = event.context("Failed to deserialize event")?;
|
||||
|
||||
match event {
|
||||
TcpEvent::AuthRequest { username } => {
|
||||
// REDACTED standard: we might log the username, but this is a reminder
|
||||
// for future sensitive items to use [REDACTED].
|
||||
info!("AuthRequest received for user: {}", username);
|
||||
|
||||
let session_token = state.generate_token();
|
||||
|
||||
state.active_users.insert(
|
||||
session_token,
|
||||
UserState {
|
||||
username: username.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
framed
|
||||
.send(TcpEvent::AuthResponse { session_token })
|
||||
.await
|
||||
.context("Failed to send AuthResponse")?;
|
||||
|
||||
info!("AuthResponse sent to {}", username);
|
||||
}
|
||||
_ => {
|
||||
warn!("Received unhandled event before auth or unsupported event");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user