From 66f57edfacfa62a389d115342e4af81a94c1f950 Mon Sep 17 00:00:00 2001 From: dank_meme01 <42031238+dankmeme01@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:43:51 +0100 Subject: [PATCH] last change dump for now --- README.md | 5 +- mod.json | 2 +- server/Cargo.toml | 8 + server/game/src/{ => data}/bytebufferext.rs | 55 +- server/game/src/data/mod.rs | 5 + .../src/data/packets/client/connection.rs | 4 +- server/game/src/data/packets/client/game.rs | 6 +- server/game/src/data/packets/mod.rs | 57 +- .../src/data/packets/server/connection.rs | 8 +- server/game/src/data/packets/server/game.rs | 5 +- server/game/src/data/types/audio_frame.rs | 2 +- server/game/src/data/types/cocos.rs | 2 +- server/game/src/data/types/crypto.rs | 2 +- server/game/src/data/types/gd.rs | 2 +- server/game/src/main.rs | 3 +- server/game/src/server_thread.rs | 728 ------------------ server/game/src/server_thread/error.rs | 67 ++ .../src/server_thread/handlers/connection.rs | 158 ++++ .../game/src/server_thread/handlers/game.rs | 181 +++++ server/game/src/server_thread/handlers/mod.rs | 57 ++ server/game/src/server_thread/mod.rs | 296 +++++++ server/readme.md | 29 +- src/data/bytebuffer.hpp | 4 +- src/data/packets/packet.hpp | 18 +- src/net/game_socket.cpp | 35 +- 25 files changed, 926 insertions(+), 813 deletions(-) rename server/game/src/{ => data}/bytebufferext.rs (89%) delete mode 100644 server/game/src/server_thread.rs create mode 100644 server/game/src/server_thread/error.rs create mode 100644 server/game/src/server_thread/handlers/connection.rs create mode 100644 server/game/src/server_thread/handlers/game.rs create mode 100644 server/game/src/server_thread/handlers/mod.rs create mode 100644 server/game/src/server_thread/mod.rs diff --git a/README.md b/README.md index 9a6fb6c0..24891e0b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Known issues: * i am silly * voice chat is a bit silly +* needs more unsafe code to be even more blazinger faster 🚀🚀🚀 ## Hosting a server @@ -38,7 +39,7 @@ cmake --build build --config Release ## Credit -ca7x3, Firee, Croozington, Coloride, Cvolton, mat, alk, maki, xTymon - thank you for being awesome, whether it's because you helped me, suggested ideas, or if I just found you awesome in general :D +ca7x3, Firee, Croozington, Coloride, Cvolton, mat, alk, maki, xTymon - thank you for being awesome, whether it's because you helped me, suggested ideas, helped with testing, or if I just found you awesome in general :D camila314 - thank you for [UIBuilder](https://github.com/camila314/uibuilder) @@ -46,7 +47,7 @@ RobTop - thank you for releasing this awesome game :) ## Open source acknowledgments -* [Geode](https://github.com/geode-sdk/geode) - the one thing that made all of this possible :) +* [Geode](https://geode-sdk.org/) - the one thing that made all of this possible :) * [UIBuilder](https://github.com/camila314/uibuilder) - something you should 100% use when creating GD mods * [Opus](https://github.com/xiph/opus) - audio codec used for audio compression * [libsodium](https://github.com/jedisct1/libsodium) - library used for data encryption \ No newline at end of file diff --git a/mod.json b/mod.json index 0c9c9e7c..47a27bee 100644 --- a/mod.json +++ b/mod.json @@ -1,6 +1,6 @@ { "geode": "1.3.5", - "version": "v1.0.0-beta.1", + "version": "v1.0.0-alpha.1", "id": "dankmeme.globed2", "name": "Globed", "developer": "dankmeme", diff --git a/server/Cargo.toml b/server/Cargo.toml index b792e104..318e0695 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,3 +1,11 @@ [workspace] members = ["central", "game", "shared"] resolver = "2" + +[profile.release] +# my observation thus far with LTO: +# compile times -> ~100% increase +# executable size -> ~30% decrease +# performance -> too lazy to benchmark but probably a very minor improvement +# so.. good enough to keep! +lto = "fat" diff --git a/server/game/src/bytebufferext.rs b/server/game/src/data/bytebufferext.rs similarity index 89% rename from server/game/src/bytebufferext.rs rename to server/game/src/data/bytebufferext.rs index 39735045..516a43b6 100644 --- a/server/game/src/bytebufferext.rs +++ b/server/game/src/data/bytebufferext.rs @@ -1,4 +1,7 @@ -use crate::data::types::cocos; +use crate::data::{ + packets::{PacketHeader, PacketMetadata}, + types::cocos, +}; use anyhow::{anyhow, Result}; use bytebuffer::{ByteBuffer, ByteReader}; @@ -26,12 +29,14 @@ pub const MAX_ENCODED_STRING_SIZE: usize = 512 + size_of_types!(u32); // 512 cha macro_rules! encode_impl { ($typ:ty, $buf:ident, $self:ident, $encode:expr) => { - impl crate::bytebufferext::Encodable for $typ { + impl crate::data::bytebufferext::Encodable for $typ { + #[inline] fn encode(&$self, $buf: &mut bytebuffer::ByteBuffer) { $encode } - fn encode_fast(&$self, $buf: &mut crate::bytebufferext::FastByteBuffer) { + #[inline] + fn encode_fast(&$self, $buf: &mut crate::data::bytebufferext::FastByteBuffer) { $encode } } @@ -40,11 +45,13 @@ macro_rules! encode_impl { macro_rules! decode_impl { ($typ:ty, $buf:ident, $decode:expr) => { - impl crate::bytebufferext::Decodable for $typ { + impl crate::data::bytebufferext::Decodable for $typ { + #[inline] fn decode($buf: &mut bytebuffer::ByteBuffer) -> anyhow::Result<Self> { $decode } + #[inline] fn decode_from_reader($buf: &mut bytebuffer::ByteReader) -> anyhow::Result<Self> { $decode } @@ -54,7 +61,7 @@ macro_rules! decode_impl { macro_rules! encode_unimpl { ($typ:ty) => { - impl crate::bytebufferext::Encodable for $typ { + impl crate::data::bytebufferext::Encodable for $typ { fn encode(&self, _: &mut bytebuffer::ByteBuffer) { panic!( "Tried to call {}::encode when Encodable was not implemented for this type", @@ -62,7 +69,7 @@ macro_rules! encode_unimpl { ); } - fn encode_fast(&self, _: &mut crate::bytebufferext::FastByteBuffer) { + fn encode_fast(&self, _: &mut crate::data::bytebufferext::FastByteBuffer) { panic!( "Tried to call {}::encode_fast when Encodable was not implemented for this type", stringify!($typ) @@ -74,7 +81,7 @@ macro_rules! encode_unimpl { macro_rules! decode_unimpl { ($typ:ty) => { - impl crate::bytebufferext::Decodable for $typ { + impl crate::data::bytebufferext::Decodable for $typ { fn decode(_: &mut bytebuffer::ByteBuffer) -> anyhow::Result<Self> { Err(anyhow::anyhow!("decoding unimplemented for {}", stringify!($typ))) } @@ -88,7 +95,7 @@ macro_rules! decode_unimpl { macro_rules! size_calc_impl { ($typ:ty, $calc:expr) => { - impl crate::bytebufferext::EncodableWithKnownSize for $typ { + impl crate::data::bytebufferext::EncodableWithKnownSize for $typ { const ENCODED_SIZE: usize = $calc; } }; @@ -124,6 +131,7 @@ pub trait ByteBufferExtWrite { fn write_value_vec<T: Encodable>(&mut self, val: &[T]); fn write_enum<E: Into<B>, B: Encodable>(&mut self, val: E); + fn write_packet_header<T: PacketMetadata>(&mut self); fn write_color3(&mut self, val: cocos::Color3B); fn write_color4(&mut self, val: cocos::Color4B); @@ -142,6 +150,7 @@ pub trait ByteBufferExtRead { fn read_value_vec<T: Decodable>(&mut self) -> Result<Vec<T>>; fn read_enum<E: TryFrom<B>, B: Decodable>(&mut self) -> Result<E>; + fn read_packet_header(&mut self) -> Result<PacketHeader>; fn read_color3(&mut self) -> Result<cocos::Color3B>; fn read_color4(&mut self) -> Result<cocos::Color4B>; @@ -270,19 +279,23 @@ impl ByteBufferExt for ByteBuffer { macro_rules! impl_extwrite { ($encode_fn:ident) => { + #[inline] fn write_bool(&mut self, val: bool) { self.write_u8(u8::from(val)); } + #[inline] fn write_byte_array(&mut self, val: &[u8]) { self.write_u32(val.len() as u32); self.write_bytes(val); } + #[inline] fn write_value<T: Encodable>(&mut self, val: &T) { val.$encode_fn(self); } + #[inline] fn write_optional_value<T: Encodable>(&mut self, val: Option<&T>) { self.write_bool(val.is_some()); if let Some(val) = val { @@ -290,10 +303,12 @@ macro_rules! impl_extwrite { } } + #[inline] fn write_value_array<T: Encodable, const N: usize>(&mut self, val: &[T; N]) { val.iter().for_each(|v| self.write_value(v)); } + #[inline] fn write_value_vec<T: Encodable>(&mut self, val: &[T]) { self.write_u32(val.len() as u32); for elem in val { @@ -301,18 +316,27 @@ macro_rules! impl_extwrite { } } + #[inline] fn write_enum<E: Into<B>, B: Encodable>(&mut self, val: E) { self.write_value(&val.into()); } + #[inline] + fn write_packet_header<T: PacketMetadata>(&mut self) { + self.write_value(&PacketHeader::from_packet::<T>()); + } + + #[inline] fn write_color3(&mut self, val: cocos::Color3B) { self.write_value(&val); } + #[inline] fn write_color4(&mut self, val: cocos::Color4B) { self.write_value(&val); } + #[inline] fn write_point(&mut self, val: cocos::Point) { self.write_value(&val); } @@ -321,19 +345,23 @@ macro_rules! impl_extwrite { macro_rules! impl_extread { ($decode_fn:ident) => { + #[inline] fn read_bool(&mut self) -> Result<bool> { Ok(self.read_u8()? != 0u8) } + #[inline] fn read_byte_array(&mut self) -> Result<Vec<u8>> { let length = self.read_u32()? as usize; Ok(self.read_bytes(length)?) } + #[inline] fn read_value<T: Decodable>(&mut self) -> Result<T> { T::$decode_fn(self) } + #[inline] fn read_optional_value<T: Decodable>(&mut self) -> Result<Option<T>> { Ok(match self.read_bool()? { false => None, @@ -341,10 +369,12 @@ macro_rules! impl_extread { }) } + #[inline] fn read_value_array<T: Decodable, const N: usize>(&mut self) -> Result<[T; N]> { array_init::try_array_init(|_| self.read_value::<T>()) } + #[inline] fn read_value_vec<T: Decodable>(&mut self) -> Result<Vec<T>> { let mut out = Vec::new(); let length = self.read_u32()? as usize; @@ -355,6 +385,7 @@ macro_rules! impl_extread { Ok(out) } + #[inline] fn read_enum<E: TryFrom<B>, B: Decodable>(&mut self) -> Result<E> { let val = self.read_value::<B>()?; let val: Result<E, _> = val.try_into(); @@ -362,14 +393,22 @@ macro_rules! impl_extread { val.map_err(|_| anyhow!("failed to decode enum")) } + #[inline] + fn read_packet_header(&mut self) -> Result<PacketHeader> { + self.read_value() + } + + #[inline] fn read_color3(&mut self) -> Result<cocos::Color3B> { self.read_value() } + #[inline] fn read_color4(&mut self) -> Result<cocos::Color4B> { self.read_value() } + #[inline] fn read_point(&mut self) -> Result<cocos::Point> { self.read_value() } diff --git a/server/game/src/data/mod.rs b/server/game/src/data/mod.rs index 63984062..e5b12629 100644 --- a/server/game/src/data/mod.rs +++ b/server/game/src/data/mod.rs @@ -1,2 +1,7 @@ +pub mod bytebufferext; pub mod packets; pub mod types; + +pub use bytebufferext::*; +pub use packets::*; +pub use types::*; diff --git a/server/game/src/data/packets/client/connection.rs b/server/game/src/data/packets/client/connection.rs index a9a5210d..15fbc72f 100644 --- a/server/game/src/data/packets/client/connection.rs +++ b/server/game/src/data/packets/client/connection.rs @@ -1,6 +1,4 @@ -use crate::bytebufferext::*; -use crate::data::packets::*; -use crate::data::types::CryptoPublicKey; +use crate::data::*; /* PingPacket - 10000 */ diff --git a/server/game/src/data/packets/client/game.rs b/server/game/src/data/packets/client/game.rs index dfd581a9..73be04bf 100644 --- a/server/game/src/data/packets/client/game.rs +++ b/server/game/src/data/packets/client/game.rs @@ -1,8 +1,4 @@ -use crate::bytebufferext::*; -use crate::data::packets::*; -use crate::data::types::EncodedAudioFrame; -use crate::data::types::PlayerData; -use crate::data::types::PlayerIconData; +use crate::data::*; /* SyncIconsPacket - 11000 */ diff --git a/server/game/src/data/packets/mod.rs b/server/game/src/data/packets/mod.rs index c7f24d17..3239e35e 100644 --- a/server/game/src/data/packets/mod.rs +++ b/server/game/src/data/packets/mod.rs @@ -1,7 +1,10 @@ pub mod client; pub mod server; -use crate::bytebufferext::{Decodable, Encodable}; +pub use client::*; +pub use server::*; + +use crate::data::bytebufferext::*; type PacketId = u16; @@ -29,29 +32,23 @@ macro_rules! packet { impl crate::data::packets::PacketMetadata for $packet_type { const PACKET_ID: crate::data::packets::PacketId = $packet_id; const ENCRYPTED: bool = $encrypted; + const NAME: &'static str = stringify!($packet_type); + } + + impl $packet_type { + pub const fn header() -> crate::data::packets::PacketHeader { + crate::data::packets::PacketHeader::from_packet::<Self>() + } } }; + ($packet_type:ident, $packet_id:expr, $encrypted:expr, { $($field:ident: $field_type:ty),* $(,)? }) => { #[derive(Clone)] pub struct $packet_type { $(pub $field: $field_type),* } - impl crate::data::packets::Packet for $packet_type { - fn get_packet_id(&self) -> crate::data::packets::PacketId { - $packet_id - } - - fn get_encrypted(&self) -> bool { - $encrypted - } - } - - impl crate::data::packets::PacketMetadata for $packet_type { - const PACKET_ID: crate::data::packets::PacketId = $packet_id; - const ENCRYPTED: bool = $encrypted; - const NAME: &'static str = stringify!($packet_type); - } + packet!($packet_type, $packet_id, $encrypted); }; } @@ -93,4 +90,30 @@ pub trait PacketMetadata { const NAME: &'static str; } -pub const PACKET_HEADER_LEN: usize = std::mem::size_of::<PacketId>() + std::mem::size_of::<bool>(); +pub struct PacketHeader { + pub packet_id: u16, + pub encrypted: bool, +} + +impl PacketHeader { + #[inline] + pub const fn from_packet<P: PacketMetadata>() -> Self { + Self { + packet_id: P::PACKET_ID, + encrypted: P::ENCRYPTED, + } + } + + pub const SIZE: usize = std::mem::size_of::<PacketId>() + std::mem::size_of::<bool>(); +} + +encode_impl!(PacketHeader, buf, self, { + buf.write_u16(self.packet_id); + buf.write_bool(self.encrypted); +}); + +decode_impl!(PacketHeader, buf, { + let packet_id = buf.read_u16()?; + let encrypted = buf.read_bool()?; + Ok(Self { packet_id, encrypted }) +}); diff --git a/server/game/src/data/packets/server/connection.rs b/server/game/src/data/packets/server/connection.rs index 73d73c81..89e4d040 100644 --- a/server/game/src/data/packets/server/connection.rs +++ b/server/game/src/data/packets/server/connection.rs @@ -1,10 +1,4 @@ -use crate::{ - bytebufferext::*, - data::{ - packets::{empty_server_packet, packet}, - types::CryptoPublicKey, - }, -}; +use crate::data::*; /* PingResponsePacket - 20000 */ diff --git a/server/game/src/data/packets/server/game.rs b/server/game/src/data/packets/server/game.rs index e37f915b..b79d1db0 100644 --- a/server/game/src/data/packets/server/game.rs +++ b/server/game/src/data/packets/server/game.rs @@ -1,7 +1,4 @@ -use crate::bytebufferext::*; -use crate::data::packets::*; -use crate::data::types::EncodedAudioFrame; -use crate::data::types::PlayerAccountData; +use crate::data::*; /* PlayerProfilesPacket - 21000 */ diff --git a/server/game/src/data/types/audio_frame.rs b/server/game/src/data/types/audio_frame.rs index 19ec31ab..6e308b8f 100644 --- a/server/game/src/data/types/audio_frame.rs +++ b/server/game/src/data/types/audio_frame.rs @@ -1,4 +1,4 @@ -use crate::bytebufferext::{decode_impl, encode_impl, ByteBufferExtRead, ByteBufferExtWrite}; +use crate::data::bytebufferext::*; const VOICE_OPUS_FRAMES_IN_AUDIO_FRAME: usize = 20; diff --git a/server/game/src/data/types/cocos.rs b/server/game/src/data/types/cocos.rs index 0ff6c43a..cf1f2f4a 100644 --- a/server/game/src/data/types/cocos.rs +++ b/server/game/src/data/types/cocos.rs @@ -1,6 +1,6 @@ use anyhow::bail; -use crate::bytebufferext::{decode_impl, encode_impl}; +use crate::data::bytebufferext::*; #[derive(Copy, Clone, Default)] pub struct Color3B { diff --git a/server/game/src/data/types/crypto.rs b/server/game/src/data/types/crypto.rs index fb572689..eda96a40 100644 --- a/server/game/src/data/types/crypto.rs +++ b/server/game/src/data/types/crypto.rs @@ -2,7 +2,7 @@ use std::io::Read; use crypto_box::{PublicKey, KEY_SIZE}; -use crate::bytebufferext::*; +use crate::data::bytebufferext::*; #[derive(Clone)] pub struct CryptoPublicKey { diff --git a/server/game/src/data/types/gd.rs b/server/game/src/data/types/gd.rs index 04ccad4d..1f1589e8 100644 --- a/server/game/src/data/types/gd.rs +++ b/server/game/src/data/types/gd.rs @@ -1,6 +1,6 @@ use globed_shared::SpecialUser; -use crate::bytebufferext::*; +use crate::data::bytebufferext::*; use super::Color3B; diff --git a/server/game/src/main.rs b/server/game/src/main.rs index 20f07287..fe1bb7cb 100644 --- a/server/game/src/main.rs +++ b/server/game/src/main.rs @@ -17,7 +17,6 @@ use state::ServerState; use server::GameServer; -pub mod bytebufferext; pub mod data; pub mod logger; pub mod managers; @@ -110,7 +109,7 @@ async fn main() -> Result<(), Box<dyn Error>> { true, ) } else { - info!("Retreiving config from the central server.."); + info!("Retrieving config from the central server.."); let response = config .http_client diff --git a/server/game/src/server_thread.rs b/server/game/src/server_thread.rs deleted file mode 100644 index b4d1936a..00000000 --- a/server/game/src/server_thread.rs +++ /dev/null @@ -1,728 +0,0 @@ -use std::{ - fmt::Display, - io, - net::SocketAddrV4, - sync::{ - atomic::{AtomicBool, AtomicI32, Ordering}, - Arc, - }, - time::{Duration, SystemTime, SystemTimeError}, -}; - -use parking_lot::Mutex as SyncMutex; - -use bytebuffer::{ByteBuffer, ByteReader}; -use crypto_box::{ - aead::{Aead, AeadCore, OsRng}, - ChaChaBox, -}; -use globed_shared::PROTOCOL_VERSION; -use log::{debug, warn}; -use tokio::sync::{mpsc, Mutex}; - -use crate::{ - bytebufferext::*, - data::{ - packets::{client::*, server::*, Packet, PacketMetadata, PACKET_HEADER_LEN}, - types::*, - }, - server::GameServer, -}; - -// TODO adjust this to PlayerData size in the future plus some headroom -pub const SMALL_PACKET_LIMIT: usize = 128; -const CHANNEL_BUFFER_SIZE: usize = 8; - -pub enum ServerThreadMessage { - Packet(Vec<u8>), - SmallPacket([u8; SMALL_PACKET_LIMIT]), - BroadcastVoice(Arc<VoiceBroadcastPacket>), - TerminationNotice(String), -} - -pub struct GameServerThread { - game_server: &'static GameServer, - - rx: Mutex<mpsc::Receiver<ServerThreadMessage>>, - tx: mpsc::Sender<ServerThreadMessage>, - awaiting_termination: AtomicBool, - pub authenticated: AtomicBool, - crypto_box: SyncMutex<Option<ChaChaBox>>, - - peer: SocketAddrV4, - pub account_id: AtomicI32, - pub level_id: AtomicI32, - pub account_data: SyncMutex<PlayerAccountData>, - - last_voice_packet: SyncMutex<SystemTime>, -} - -macro_rules! gs_handler { - ($self:ident,$name:ident,$pktty:ty,$pkt:ident,$code:expr) => { - async fn $name(&$self, buf: &mut ByteReader<'_>) -> Result<()> { - let $pkt = <$pktty>::decode_from_reader(buf)?; - #[cfg(debug_assertions)] - log::debug!("Handling packet {}", <$pktty>::NAME); - $code - } - }; -} - -macro_rules! gs_handler_sync { - ($self:ident,$name:ident,$pktty:ty,$pkt:ident,$code:expr) => { - fn $name(&$self, buf: &mut ByteReader<'_>) -> Result<()> { - let $pkt = <$pktty>::decode_from_reader(buf)?; - #[cfg(debug_assertions)] - log::debug!("Handling packet {}", <$pktty>::NAME); - $code - } - }; -} - -macro_rules! gs_disconnect { - ($self:ident, $msg:expr) => { - $self.terminate(); - $self - .send_packet_fast(&ServerDisconnectPacket { message: $msg }) - .await?; - return Ok(()); - }; -} - -#[allow(unused_macros)] -macro_rules! gs_notice { - ($self:expr, $msg:expr) => { - $self.send_packet(&ServerNoticePacket { message: $msg }).await?; - }; -} - -macro_rules! gs_needauth { - ($self:ident) => { - if !$self.authenticated.load(Ordering::Relaxed) { - gs_disconnect!($self, "unauthorized, please try connecting again".to_string()); - } - }; -} - -enum PacketHandlingError { - Other(String), - WrongCryptoBoxState, - EncryptionError(String), - DecryptionError(String), - IOError(io::Error), - MalformedMessage, - MalformedLoginAttempt, - MalformedCiphertext, - NoHandler(u16), - WebRequestError(reqwest::Error), - UnexpectedPlayerData, - SystemTimeError(SystemTimeError), - SocketSendFailed(io::Error), - SocketWouldBlock, - UnexpectedCentralResponse, -} - -type Result<T> = core::result::Result<T, PacketHandlingError>; - -impl GameServerThread { - /* public api for the main server */ - - pub fn new(peer: SocketAddrV4, game_server: &'static GameServer) -> Self { - let (tx, rx) = mpsc::channel::<ServerThreadMessage>(CHANNEL_BUFFER_SIZE); - Self { - tx, - rx: Mutex::new(rx), - peer, - crypto_box: SyncMutex::new(None), - account_id: AtomicI32::new(0), - level_id: AtomicI32::new(0), - authenticated: AtomicBool::new(false), - game_server, - awaiting_termination: AtomicBool::new(false), - account_data: SyncMutex::new(PlayerAccountData::default()), - last_voice_packet: SyncMutex::new(SystemTime::now()), - } - } - - pub async fn run(&self) { - let mut rx = self.rx.lock().await; - - loop { - if self.awaiting_termination.load(Ordering::Relaxed) { - break; - } - - match tokio::time::timeout(Duration::from_secs(60), rx.recv()).await { - Ok(Some(message)) => match self.handle_message(message).await { - Ok(()) => {} - Err(err) => warn!("[@{}]: {}", self.peer, err.to_string()), - }, - Ok(None) | Err(_) => break, // sender closed | timeout - }; - } - } - - pub fn push_new_message(&self, data: ServerThreadMessage) -> anyhow::Result<()> { - self.tx.try_send(data)?; - Ok(()) - } - - pub fn terminate(&self) { - self.awaiting_termination.store(true, Ordering::Relaxed); - } - - /* private utilities */ - - async fn send_packet<P: Packet>(&self, packet: &P) -> Result<()> { - #[cfg(debug_assertions)] - log::debug!("Sending {}", P::NAME); - - let serialized = self.serialize_packet(packet)?; - self.send_buffer(serialized.as_bytes()).await - } - - // fast packet sending with zero heap allocation - // packet must implement EncodableWithKnownSize to be fast sendable - // it also must **NOT** be encrypted. - // on average 2-3x faster than send_packet, even worst case should be faster by a bit - async fn send_packet_fast<P: Packet + EncodableWithKnownSize>(&self, packet: &P) -> Result<()> { - assert!( - !packet.get_encrypted(), - "Attempting to fast encode an encrypted packet ({})", - P::NAME - ); - - let to_send: Result<Option<Vec<u8>>> = alloca::with_alloca(PACKET_HEADER_LEN + P::ENCODED_SIZE, move |data| { - // safety: 'data' will have garbage data but that is considered safe for all our intents and purposes - // as `FastByteBuffer::as_bytes()` will only return what was already written. - let data = unsafe { - let ptr = data.as_mut_ptr().cast::<u8>(); - let len = std::mem::size_of_val(data); - std::slice::from_raw_parts_mut(ptr, len) - }; - - let mut buf = FastByteBuffer::new(data); - buf.write_u16(packet.get_packet_id()); - buf.write_bool(false); - buf.write_value(packet); - - let send_data = buf.as_bytes(); - match self.send_buffer_immediate(send_data) { - Err(PacketHandlingError::SocketWouldBlock) => Ok(Some(send_data.to_vec())), - Err(e) => Err(e), - Ok(()) => Ok(None), - } - }); - - if let Some(to_send) = to_send? { - self.send_buffer(&to_send).await?; - } - - Ok(()) - } - - async fn send_buffer(&self, buffer: &[u8]) -> Result<()> { - self.game_server - .socket - .send_to(buffer, self.peer) - .await - .map(|_size| ()) - .map_err(PacketHandlingError::SocketSendFailed) - } - - // attempt to send a buffer immediately to the socket, but if it requires blocking then nuh uh - fn send_buffer_immediate(&self, buffer: &[u8]) -> Result<()> { - self.game_server - .socket - .try_send_to(buffer, std::net::SocketAddr::V4(self.peer)) - .map(|_| ()) - .map_err(|e| { - if e.kind() == std::io::ErrorKind::WouldBlock { - PacketHandlingError::SocketWouldBlock - } else { - PacketHandlingError::SocketSendFailed(e) - } - }) - } - - fn serialize_packet<P: Packet>(&self, packet: &P) -> Result<ByteBuffer> { - let mut buf = ByteBuffer::new(); - buf.write_u16(P::PACKET_ID); - buf.write_bool(P::ENCRYPTED); - - if !P::ENCRYPTED { - packet.encode(&mut buf); - return Ok(buf); - } - - let cbox = self.crypto_box.lock(); - - // should never happen - #[cfg(debug_assertions)] - if !cbox.is_some() { - return Err(PacketHandlingError::WrongCryptoBoxState); - } - - let mut cltxtbuf = ByteBuffer::new(); - packet.encode(&mut cltxtbuf); - - let cbox = cbox.as_ref().unwrap(); - let nonce = ChaChaBox::generate_nonce(&mut OsRng); - - let encrypted = cbox - .encrypt(&nonce, cltxtbuf.as_bytes()) - .map_err(|e| PacketHandlingError::EncryptionError(e.to_string()))?; - - #[cfg(not(rust_analyzer))] // i am so sorry - buf.write_bytes(&nonce); - buf.write_bytes(&encrypted); - - Ok(buf) - } - - async fn handle_message(&self, message: ServerThreadMessage) -> Result<()> { - match message { - ServerThreadMessage::Packet(data) => self.handle_packet(&data).await?, - ServerThreadMessage::SmallPacket(data) => self.handle_packet(&data).await?, - ServerThreadMessage::BroadcastVoice(voice_packet) => self.send_packet(&*voice_packet).await?, - ServerThreadMessage::TerminationNotice(message) => { - self.terminate(); - self.send_packet(&ServerDisconnectPacket { message }).await?; - } - } - - Ok(()) - } - - /* packet handlers */ - - async fn handle_packet(&self, message: &[u8]) -> Result<()> { - #[cfg(debug_assertions)] - if message.len() < PACKET_HEADER_LEN { - return Err(PacketHandlingError::MalformedMessage); - } - - let mut data = ByteReader::from_bytes(message); - - let packet_id = data.read_u16()?; - let encrypted = data.read_bool()?; - - // minor optimization - if packet_id == PlayerDataPacket::PACKET_ID { - return self.handle_player_data(&mut data).await; - } - - // also for optimization, reject the voice packet immediately if the player is blocked from vc - if packet_id == VoicePacket::PACKET_ID { - let accid = self.account_id.load(Ordering::Relaxed); - if self.game_server.chat_blocked(accid) { - debug!("blocking voice packet from {accid}"); - return Ok(()); - } - } - - // reject cleartext credentials - if packet_id == LoginPacket::PACKET_ID && !encrypted { - return Err(PacketHandlingError::MalformedLoginAttempt); - } - - let cleartext_vec: Vec<u8>; - if encrypted { - if message.len() < 24 + PACKET_HEADER_LEN { - return Err(PacketHandlingError::MalformedCiphertext); - } - - let cbox = self.crypto_box.lock(); - if cbox.is_none() { - return Err(PacketHandlingError::WrongCryptoBoxState); - } - - let cbox = cbox.as_ref().unwrap(); - - let nonce = &message[PACKET_HEADER_LEN..PACKET_HEADER_LEN + 24]; - let ciphertext = &message[PACKET_HEADER_LEN + 24..]; - - cleartext_vec = cbox - .decrypt(nonce.into(), ciphertext) - .map_err(|e| PacketHandlingError::DecryptionError(e.to_string()))?; - data = ByteReader::from_bytes(&cleartext_vec); - } - - match packet_id { - /* connection related */ - PingPacket::PACKET_ID => self.handle_ping(&mut data).await, - CryptoHandshakeStartPacket::PACKET_ID => self.handle_crypto_handshake(&mut data).await, - KeepalivePacket::PACKET_ID => self.handle_keepalive(&mut data).await, - LoginPacket::PACKET_ID => self.handle_login(&mut data).await, - DisconnectPacket::PACKET_ID => self.handle_disconnect(&mut data), - - /* game related */ - SyncIconsPacket::PACKET_ID => self.handle_sync_icons(&mut data).await, - RequestProfilesPacket::PACKET_ID => self.handle_request_profiles(&mut data).await, - LevelJoinPacket::PACKET_ID => self.handle_level_join(&mut data).await, - LevelLeavePacket::PACKET_ID => self.handle_level_leave(&mut data).await, - PlayerDataPacket::PACKET_ID => self.handle_player_data(&mut data).await, - RequestPlayerListPacket::PACKET_ID => self.handle_request_player_list(&mut data).await, - - VoicePacket::PACKET_ID => self.handle_voice(&mut data).await, - x => Err(PacketHandlingError::NoHandler(x)), - } - } - - /* connection related */ - - gs_handler!(self, handle_ping, PingPacket, packet, { - self.send_packet_fast(&PingResponsePacket { - id: packet.id, - player_count: self.game_server.state.player_count.load(Ordering::Relaxed), - }) - .await - }); - - gs_handler!(self, handle_crypto_handshake, CryptoHandshakeStartPacket, packet, { - match packet.protocol { - p if p > PROTOCOL_VERSION => { - gs_disconnect!( - self, - format!( - "Outdated server! You are running protocol v{p} while the server is still on v{PROTOCOL_VERSION}.", - ) - ); - } - p if p < PROTOCOL_VERSION => { - gs_disconnect!( - self, - format!( - "Outdated client! Please update the mod in order to connect to the server. Client protocol version: v{p}, server: v{PROTOCOL_VERSION}", - ) - ); - } - _ => {} - } - - { - let mut cbox = self.crypto_box.lock(); - - // as ServerThread is now tied to the SocketAddrV4 and not account id like in globed v0 - // erroring here is not a concern, even if the user's game crashes without a disconnect packet, - // they would have a new randomized port when they restart and this would never fail. - if cbox.is_some() { - return Err(PacketHandlingError::WrongCryptoBoxState); - } - - let new_box = ChaChaBox::new(&packet.key.pubkey, &self.game_server.secret_key); - *cbox = Some(new_box); - } - - self.send_packet_fast(&CryptoHandshakeResponsePacket { - key: CryptoPublicKey { - pubkey: self.game_server.secret_key.public_key().clone(), - }, - }) - .await - }); - - gs_handler!(self, handle_keepalive, KeepalivePacket, _packet, { - gs_needauth!(self); - - self.send_packet_fast(&KeepaliveResponsePacket { - player_count: self.game_server.state.player_count.load(Ordering::Relaxed), - }) - .await - }); - - gs_handler!(self, handle_login, LoginPacket, packet, { - if self.game_server.standalone { - debug!("Bypassing login for {}", packet.account_id); - self.game_server.check_already_logged_in(packet.account_id)?; - self.authenticated.store(true, Ordering::Relaxed); - self.account_id.store(packet.account_id, Ordering::Relaxed); - self.game_server.state.player_count.fetch_add(1u32, Ordering::Relaxed); - { - let mut account_data = self.account_data.lock(); - account_data.account_id = packet.account_id; - account_data.name = format!("Player{}", packet.account_id); - } - self.send_packet_fast(&LoggedInPacket {}).await?; - return Ok(()); - } - - // lets verify the given token - - let url = format!("{}gs/verify", self.game_server.config.central_url); - - let response = self - .game_server - .config - .http_client - .post(url) - .query(&[ - ("account_id", packet.account_id.to_string()), - ("token", packet.token.clone()), - ("pw", self.game_server.config.central_pw.clone()), - ]) - .send() - .await? - .error_for_status()? - .text() - .await?; - - if !response.starts_with("status_ok:") { - self.terminate(); - self.send_packet(&LoginFailedPacket { - message: format!("authentication failed: {response}"), - }) - .await?; - - return Ok(()); - } - - let player_name = response - .split_once(':') - .ok_or(PacketHandlingError::UnexpectedCentralResponse)? - .1; - - self.game_server.check_already_logged_in(packet.account_id)?; - self.authenticated.store(true, Ordering::Relaxed); - self.account_id.store(packet.account_id, Ordering::Relaxed); - self.game_server.state.player_count.fetch_add(1u32, Ordering::Relaxed); // increment player count - - { - let mut account_data = self.account_data.lock(); - account_data.account_id = packet.account_id; - account_data.name = player_name.to_string(); - - let special_user_data = self - .game_server - .central_conf - .lock() - .special_users - .get(&packet.account_id) - .cloned(); - - if let Some(sud) = special_user_data { - account_data.special_user_data = Some(sud.try_into()?); - } - } - - debug!("Login successful from {player_name} ({})", packet.account_id); - - self.send_packet_fast(&LoggedInPacket {}).await?; - - Ok(()) - }); - - gs_handler_sync!(self, handle_disconnect, DisconnectPacket, _packet, { - self.terminate(); - Ok(()) - }); - - /* game related */ - - gs_handler!(self, handle_sync_icons, SyncIconsPacket, packet, { - gs_needauth!(self); - - let mut account_data = self.account_data.lock(); - account_data.icons.clone_from(&packet.icons); - Ok(()) - }); - - gs_handler!(self, handle_request_profiles, RequestProfilesPacket, packet, { - gs_needauth!(self); - - self.send_packet(&PlayerProfilesPacket { - profiles: self.game_server.gather_profiles(&packet.ids), - }) - .await - }); - - gs_handler!(self, handle_level_join, LevelJoinPacket, packet, { - gs_needauth!(self); - - let account_id = self.account_id.load(Ordering::Relaxed); - let old_level = self.level_id.swap(packet.level_id, Ordering::Relaxed); - - let mut pm = self.game_server.state.player_manager.lock(); - - if old_level != 0 { - pm.remove_from_level(old_level, account_id); - } - - pm.add_to_level(packet.level_id, account_id); - - Ok(()) - }); - - gs_handler!(self, handle_level_leave, LevelLeavePacket, _packet, { - gs_needauth!(self); - - let level_id = self.level_id.load(Ordering::Relaxed); - if level_id != 0 { - let account_id = self.account_id.load(Ordering::Relaxed); - - let mut pm = self.game_server.state.player_manager.lock(); - pm.remove_from_level(level_id, account_id); - } - - Ok(()) - }); - - // if you are seeing this. i am so sorry. - gs_handler!(self, handle_player_data, PlayerDataPacket, packet, { - gs_needauth!(self); - - let level_id = self.level_id.load(Ordering::Relaxed); - if level_id == 0 { - return Err(PacketHandlingError::UnexpectedPlayerData); - } - - let account_id = self.account_id.load(Ordering::Relaxed); - - let retval: Result<Option<Vec<u8>>> = { - let mut pm = self.game_server.state.player_manager.lock(); - pm.set_player_data(account_id, &packet.data); - - // this unwrap should be safe - let written_players = pm.get_player_count_on_level(level_id).unwrap() - 1; - drop(pm); - - let calc_size = PACKET_HEADER_LEN + 4 + (written_players * AssociatedPlayerData::ENCODED_SIZE); - - alloca::with_alloca(calc_size, move |data| { - // safety: 'data' will have garbage data but that is considered safe for all our intents and purposes - // as `FastByteBuffer::as_bytes()` will only return what was already written. - let data = unsafe { - let ptr = data.as_mut_ptr().cast::<u8>(); - let len = std::mem::size_of_val(data); - std::slice::from_raw_parts_mut(ptr, len) - }; - - let mut buf = FastByteBuffer::new(data); - - // dont actually do this anywhere else please - buf.write_u16(LevelDataPacket::PACKET_ID); - buf.write_bool(false); - buf.write_u32(written_players as u32); - - // this is scary - self.game_server.state.player_manager.lock().for_each_player_on_level( - level_id, - |player, buf| { - // we do additional length check because player count may have changed since 1st lock - // NOTE: this assumes encoded size of AssociatedPlayerData is a constant - // change this to something else if that won't be true in the future - if buf.len() != buf.capacity() && player.account_id != account_id { - buf.write_value(player); - } - }, - &mut buf, - ); - - let data = buf.as_bytes(); - match self.send_buffer_immediate(data) { - // if we cant send without blocking, accept our defeat and clone the data to a vec - Err(PacketHandlingError::SocketWouldBlock) => Ok(Some(data.to_vec())), - // if another error occured, propagate it up - Err(e) => Err(e), - // if all good, do nothing - Ok(()) => Ok(None), - } - }) - }; - - if let Some(data) = retval? { - debug!("fast PlayerData response failed, issuing a blocking call"); - self.send_buffer(&data).await?; - } - - Ok(()) - }); - - gs_handler!(self, handle_request_player_list, RequestPlayerListPacket, _packet, { - gs_needauth!(self); - - self.send_packet(&PlayerListPacket { - profiles: self.game_server.gather_all_profiles(), - }) - .await - }); - - gs_handler!(self, handle_voice, VoicePacket, packet, { - gs_needauth!(self); - - let accid = self.account_id.load(Ordering::Relaxed); - - // check the throughput - { - let mut last_voice_packet = self.last_voice_packet.lock(); - let now = SystemTime::now(); - let passed_time = now.duration_since(*last_voice_packet)?.as_millis(); - *last_voice_packet = now; - - let total_size = packet.data.opus_frames.iter().map(Vec::len).sum::<usize>(); - - let throughput = total_size / passed_time as usize; // in kb/s - - debug!("voice packet throughput: {}kb/s", throughput); - if throughput > 8 { - warn!("rejecting a voice packet, throughput above the limit: {}kb/s", throughput); - return Ok(()); - } - } - - let vpkt = Arc::new(VoiceBroadcastPacket { - player_id: accid, - data: packet.data.clone(), - }); - - self.game_server.broadcast_voice_packet(&vpkt)?; - - Ok(()) - }); -} - -impl From<anyhow::Error> for PacketHandlingError { - fn from(value: anyhow::Error) -> Self { - PacketHandlingError::Other(value.to_string()) - } -} - -impl From<reqwest::Error> for PacketHandlingError { - fn from(value: reqwest::Error) -> Self { - PacketHandlingError::WebRequestError(value) - } -} - -impl From<SystemTimeError> for PacketHandlingError { - fn from(value: SystemTimeError) -> Self { - PacketHandlingError::SystemTimeError(value) - } -} - -impl From<std::io::Error> for PacketHandlingError { - fn from(value: std::io::Error) -> Self { - PacketHandlingError::IOError(value) - } -} - -impl Display for PacketHandlingError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Other(msg) => f.write_str(msg), - Self::IOError(msg) => f.write_fmt(format_args!("IO Error: {msg}")), - Self::WrongCryptoBoxState => f.write_str("wrong crypto box state for the given operation"), - Self::EncryptionError(msg) => f.write_fmt(format_args!("Encryption failed: {msg}")), - Self::DecryptionError(msg) => f.write_fmt(format_args!("Decryption failed: {msg}")), - Self::MalformedCiphertext => f.write_str("malformed ciphertext in an encrypted packet"), - Self::MalformedMessage => f.write_str("malformed message structure"), - Self::MalformedLoginAttempt => f.write_str("malformed login attempt"), - Self::NoHandler(id) => f.write_fmt(format_args!("no packet handler for packet ID {id}")), - Self::WebRequestError(msg) => f.write_fmt(format_args!("web request error: {msg}")), - Self::UnexpectedPlayerData => f.write_str("received PlayerDataPacket when not on a level"), - Self::SystemTimeError(msg) => f.write_fmt(format_args!("system time error: {msg}")), - Self::SocketSendFailed(err) => f.write_fmt(format_args!("socket send failed: {err}")), - Self::SocketWouldBlock => f.write_str("could not do a non-blocking operation on the socket as it would block"), - Self::UnexpectedCentralResponse => f.write_str("got unexpected response from the central server"), - } - } -} diff --git a/server/game/src/server_thread/error.rs b/server/game/src/server_thread/error.rs new file mode 100644 index 00000000..5ce29628 --- /dev/null +++ b/server/game/src/server_thread/error.rs @@ -0,0 +1,67 @@ +use std::{fmt::Display, time::SystemTimeError}; + +pub enum PacketHandlingError { + Other(String), // unknown generic error + WrongCryptoBoxState, // cryptobox was either Some or None when should've been the other one + EncryptionError(String), // failed to encrypt data + DecryptionError(String), // failed to decrypt data + IOError(std::io::Error), // generic IO error + MalformedMessage, // packet is missing a header + MalformedLoginAttempt, // LoginPacket with cleartext credentials + MalformedCiphertext, // missing nonce/mac in the encrypted ciphertext + NoHandler(u16), // no handler found for this packet ID + WebRequestError(reqwest::Error), // error making a web request to the central server + UnexpectedPlayerData, // client sent PlayerDataPacket outside of a level + SystemTimeError(SystemTimeError), // clock went backwards..? + SocketSendFailed(std::io::Error), // failed to send data on a socket due to an IO error + SocketWouldBlock, // failed to send data on a socket because operation would block + UnexpectedCentralResponse, // malformed response from the central server +} + +pub type Result<T> = core::result::Result<T, PacketHandlingError>; + +impl From<anyhow::Error> for PacketHandlingError { + fn from(value: anyhow::Error) -> Self { + PacketHandlingError::Other(value.to_string()) + } +} + +impl From<reqwest::Error> for PacketHandlingError { + fn from(value: reqwest::Error) -> Self { + PacketHandlingError::WebRequestError(value) + } +} + +impl From<SystemTimeError> for PacketHandlingError { + fn from(value: SystemTimeError) -> Self { + PacketHandlingError::SystemTimeError(value) + } +} + +impl From<std::io::Error> for PacketHandlingError { + fn from(value: std::io::Error) -> Self { + PacketHandlingError::IOError(value) + } +} + +impl Display for PacketHandlingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Other(msg) => f.write_str(msg), + Self::IOError(msg) => f.write_fmt(format_args!("IO Error: {msg}")), + Self::WrongCryptoBoxState => f.write_str("wrong crypto box state for the given operation"), + Self::EncryptionError(msg) => f.write_fmt(format_args!("Encryption failed: {msg}")), + Self::DecryptionError(msg) => f.write_fmt(format_args!("Decryption failed: {msg}")), + Self::MalformedCiphertext => f.write_str("malformed ciphertext in an encrypted packet"), + Self::MalformedMessage => f.write_str("malformed message structure"), + Self::MalformedLoginAttempt => f.write_str("malformed login attempt"), + Self::NoHandler(id) => f.write_fmt(format_args!("no packet handler for packet ID {id}")), + Self::WebRequestError(msg) => f.write_fmt(format_args!("web request error: {msg}")), + Self::UnexpectedPlayerData => f.write_str("received PlayerDataPacket when not on a level"), + Self::SystemTimeError(msg) => f.write_fmt(format_args!("system time error: {msg}")), + Self::SocketSendFailed(err) => f.write_fmt(format_args!("socket send failed: {err}")), + Self::SocketWouldBlock => f.write_str("could not do a non-blocking operation on the socket as it would block"), + Self::UnexpectedCentralResponse => f.write_str("got unexpected response from the central server"), + } + } +} diff --git a/server/game/src/server_thread/handlers/connection.rs b/server/game/src/server_thread/handlers/connection.rs new file mode 100644 index 00000000..5548f755 --- /dev/null +++ b/server/game/src/server_thread/handlers/connection.rs @@ -0,0 +1,158 @@ +use std::sync::atomic::Ordering; + +use crypto_box::ChaChaBox; +use globed_shared::PROTOCOL_VERSION; + +use crate::server_thread::{GameServerThread, PacketHandlingError}; +use log::debug; + +use super::*; +use crate::data::*; + +impl GameServerThread { + gs_handler!(self, handle_ping, PingPacket, packet, { + self.send_packet_fast(&PingResponsePacket { + id: packet.id, + player_count: self.game_server.state.player_count.load(Ordering::Relaxed), + }) + .await + }); + + gs_handler!(self, handle_crypto_handshake, CryptoHandshakeStartPacket, packet, { + match packet.protocol { + p if p > PROTOCOL_VERSION => { + gs_disconnect!( + self, + format!( + "Outdated server! You are running protocol v{p} while the server is still on v{PROTOCOL_VERSION}.", + ) + ); + } + p if p < PROTOCOL_VERSION => { + gs_disconnect!( + self, + format!( + "Outdated client! Please update the mod in order to connect to the server. Client protocol version: v{p}, server: v{PROTOCOL_VERSION}", + ) + ); + } + _ => {} + } + + { + let mut cbox = self.crypto_box.lock(); + + // as ServerThread is now tied to the SocketAddrV4 and not account id like in globed v0 + // erroring here is not a concern, even if the user's game crashes without a disconnect packet, + // they would have a new randomized port when they restart and this would never fail. + if cbox.is_some() { + return Err(PacketHandlingError::WrongCryptoBoxState); + } + + let new_box = ChaChaBox::new(&packet.key.pubkey, &self.game_server.secret_key); + *cbox = Some(new_box); + } + + self.send_packet_fast(&CryptoHandshakeResponsePacket { + key: CryptoPublicKey { + pubkey: self.game_server.secret_key.public_key().clone(), + }, + }) + .await + }); + + gs_handler!(self, handle_keepalive, KeepalivePacket, _packet, { + gs_needauth!(self); + + self.send_packet_fast(&KeepaliveResponsePacket { + player_count: self.game_server.state.player_count.load(Ordering::Relaxed), + }) + .await + }); + + gs_handler!(self, handle_login, LoginPacket, packet, { + if self.game_server.standalone { + debug!("Bypassing login for {}", packet.account_id); + self.game_server.check_already_logged_in(packet.account_id)?; + self.authenticated.store(true, Ordering::Relaxed); + self.account_id.store(packet.account_id, Ordering::Relaxed); + self.game_server.state.player_count.fetch_add(1u32, Ordering::Relaxed); + { + let mut account_data = self.account_data.lock(); + account_data.account_id = packet.account_id; + account_data.name = format!("Player{}", packet.account_id); + } + self.send_packet_fast(&LoggedInPacket {}).await?; + return Ok(()); + } + + // lets verify the given token + + let url = format!("{}gs/verify", self.game_server.config.central_url); + + let response = self + .game_server + .config + .http_client + .post(url) + .query(&[ + ("account_id", packet.account_id.to_string()), + ("token", packet.token.clone()), + ("pw", self.game_server.config.central_pw.clone()), + ]) + .send() + .await? + .error_for_status()? + .text() + .await?; + + if !response.starts_with("status_ok:") { + self.terminate(); + self.send_packet(&LoginFailedPacket { + message: format!("authentication failed: {response}"), + }) + .await?; + + return Ok(()); + } + + let player_name = response + .split_once(':') + .ok_or(PacketHandlingError::UnexpectedCentralResponse)? + .1; + + self.game_server.check_already_logged_in(packet.account_id)?; + self.authenticated.store(true, Ordering::Relaxed); + self.account_id.store(packet.account_id, Ordering::Relaxed); + self.game_server.state.player_count.fetch_add(1u32, Ordering::Relaxed); // increment player count + + { + let mut account_data = self.account_data.lock(); + account_data.account_id = packet.account_id; + account_data.name = player_name.to_string(); + + let special_user_data = self + .game_server + .central_conf + .lock() + .special_users + .get(&packet.account_id) + .cloned(); + + if let Some(sud) = special_user_data { + account_data.special_user_data = Some(sud.try_into()?); + } + } + + debug!("Login successful from {player_name} ({})", packet.account_id); + + self.send_packet_fast(&LoggedInPacket {}).await?; + + Ok(()) + }); + + gs_handler_sync!(self, handle_disconnect, DisconnectPacket, _packet, { + self.terminate(); + Ok(()) + }); +} diff --git a/server/game/src/server_thread/handlers/game.rs b/server/game/src/server_thread/handlers/game.rs new file mode 100644 index 00000000..ad3e2bc7 --- /dev/null +++ b/server/game/src/server_thread/handlers/game.rs @@ -0,0 +1,181 @@ +use std::{ + sync::{atomic::Ordering, Arc}, + time::SystemTime, +}; + +use crate::{ + data::packets::PacketHeader, + server_thread::{GameServerThread, PacketHandlingError, Result}, +}; +use log::{debug, warn}; + +use super::{gs_disconnect, gs_handler, gs_needauth}; +use crate::data::*; + +impl GameServerThread { + gs_handler!(self, handle_sync_icons, SyncIconsPacket, packet, { + gs_needauth!(self); + + let mut account_data = self.account_data.lock(); + account_data.icons.clone_from(&packet.icons); + Ok(()) + }); + + gs_handler!(self, handle_request_profiles, RequestProfilesPacket, packet, { + gs_needauth!(self); + + self.send_packet(&PlayerProfilesPacket { + profiles: self.game_server.gather_profiles(&packet.ids), + }) + .await + }); + + gs_handler!(self, handle_level_join, LevelJoinPacket, packet, { + gs_needauth!(self); + + let account_id = self.account_id.load(Ordering::Relaxed); + let old_level = self.level_id.swap(packet.level_id, Ordering::Relaxed); + + let mut pm = self.game_server.state.player_manager.lock(); + + if old_level != 0 { + pm.remove_from_level(old_level, account_id); + } + + pm.add_to_level(packet.level_id, account_id); + + Ok(()) + }); + + gs_handler!(self, handle_level_leave, LevelLeavePacket, _packet, { + gs_needauth!(self); + + let level_id = self.level_id.load(Ordering::Relaxed); + if level_id != 0 { + let account_id = self.account_id.load(Ordering::Relaxed); + + let mut pm = self.game_server.state.player_manager.lock(); + pm.remove_from_level(level_id, account_id); + } + + Ok(()) + }); + + // if you are seeing this. i am so sorry. + gs_handler!(self, handle_player_data, PlayerDataPacket, packet, { + gs_needauth!(self); + + let level_id = self.level_id.load(Ordering::Relaxed); + if level_id == 0 { + return Err(PacketHandlingError::UnexpectedPlayerData); + } + + let account_id = self.account_id.load(Ordering::Relaxed); + + let retval: Result<Option<Vec<u8>>> = { + let mut pm = self.game_server.state.player_manager.lock(); + pm.set_player_data(account_id, &packet.data); + + // this unwrap should be safe and > 0 given that self.level_id != 0 + let written_players = pm.get_player_count_on_level(level_id).unwrap() - 1; + + // no one else on the level, no need to send a response packet + if written_players == 0 { + return Ok(()); + } + + drop(pm); + + let calc_size = PacketHeader::SIZE + 4 + (written_players * AssociatedPlayerData::ENCODED_SIZE); + + alloca::with_alloca(calc_size, move |data| { + // safety: 'data' will have garbage data but that is considered safe for all our intents and purposes + // as `FastByteBuffer::as_bytes()` will only return what was already written. + let data = unsafe { + let ptr = data.as_mut_ptr().cast::<u8>(); + let len = std::mem::size_of_val(data); + std::slice::from_raw_parts_mut(ptr, len) + }; + + let mut buf = FastByteBuffer::new(data); + + // dont actually do this anywhere else please + buf.write_packet_header::<LevelDataPacket>(); + buf.write_u32(written_players as u32); + + // this is scary + self.game_server.state.player_manager.lock().for_each_player_on_level( + level_id, + |player, buf| { + // we do additional length check because player count may have changed since 1st lock + // NOTE: this assumes encoded size of AssociatedPlayerData is a constant + // change this to something else if that won't be true in the future + if buf.len() != buf.capacity() && player.account_id != account_id { + buf.write_value(player); + } + }, + &mut buf, + ); + + let data = buf.as_bytes(); + match self.send_buffer_immediate(data) { + // if we cant send without blocking, accept our defeat and clone the data to a vec + Err(PacketHandlingError::SocketWouldBlock) => Ok(Some(data.to_vec())), + // if another error occured, propagate it up + Err(e) => Err(e), + // if all good, do nothing + Ok(()) => Ok(None), + } + }) + }; + + if let Some(data) = retval? { + debug!("fast PlayerData response failed, issuing a blocking call"); + self.send_buffer(&data).await?; + } + + Ok(()) + }); + + gs_handler!(self, handle_request_player_list, RequestPlayerListPacket, _packet, { + gs_needauth!(self); + + self.send_packet(&PlayerListPacket { + profiles: self.game_server.gather_all_profiles(), + }) + .await + }); + + gs_handler!(self, handle_voice, VoicePacket, packet, { + gs_needauth!(self); + + let accid = self.account_id.load(Ordering::Relaxed); + + // check the throughput + { + let mut last_voice_packet = self.last_voice_packet.lock(); + let now = SystemTime::now(); + let passed_time = now.duration_since(*last_voice_packet)?.as_millis(); + *last_voice_packet = now; + + let total_size = packet.data.opus_frames.iter().map(Vec::len).sum::<usize>(); + + let throughput = total_size / passed_time as usize; // in kb/s + + debug!("voice packet throughput: {}kb/s", throughput); + if throughput > 8 { + warn!("rejecting a voice packet, throughput above the limit: {}kb/s", throughput); + return Ok(()); + } + } + + let vpkt = Arc::new(VoiceBroadcastPacket { + player_id: accid, + data: packet.data.clone(), + }); + + self.game_server.broadcast_voice_packet(&vpkt)?; + + Ok(()) + }); +} diff --git a/server/game/src/server_thread/handlers/mod.rs b/server/game/src/server_thread/handlers/mod.rs new file mode 100644 index 00000000..58a6730a --- /dev/null +++ b/server/game/src/server_thread/handlers/mod.rs @@ -0,0 +1,57 @@ +mod connection; +mod game; + +macro_rules! gs_handler { + ($self:ident,$name:ident,$pktty:ty,$pkt:ident,$code:expr) => { + pub(crate) async fn $name(&$self, buf: &mut bytebuffer::ByteReader<'_>) -> crate::server_thread::Result<()> { + let $pkt = <$pktty>::decode_from_reader(buf)?; + #[cfg(debug_assertions)] + log::debug!("Handling packet {}", <$pktty>::NAME); + $code + } + }; +} + +macro_rules! gs_handler_sync { + ($self:ident,$name:ident,$pktty:ty,$pkt:ident,$code:expr) => { + pub(crate) fn $name(&$self, buf: &mut bytebuffer::ByteReader<'_>) -> crate::server_thread::Result<()> { + let $pkt = <$pktty>::decode_from_reader(buf)?; + #[cfg(debug_assertions)] + log::debug!("Handling packet {}", <$pktty>::NAME); + $code + } + }; +} + +macro_rules! gs_disconnect { + ($self:ident, $msg:expr) => { + $self.terminate(); + $self + .send_packet_fast(&ServerDisconnectPacket { message: $msg }) + .await?; + return Ok(()); + }; +} + +#[allow(unused_macros)] +macro_rules! gs_notice { + ($self:expr, $msg:expr) => { + $self.send_packet(&ServerNoticePacket { message: $msg }).await?; + }; +} + +macro_rules! gs_needauth { + ($self:ident) => { + if !$self.authenticated.load(Ordering::Relaxed) { + gs_disconnect!($self, "unauthorized, please try connecting again".to_string()); + } + }; +} + +pub(crate) use gs_disconnect; +pub(crate) use gs_handler; +pub(crate) use gs_handler_sync; +pub(crate) use gs_needauth; + +#[allow(unused_imports)] +pub(crate) use gs_notice; diff --git a/server/game/src/server_thread/mod.rs b/server/game/src/server_thread/mod.rs new file mode 100644 index 00000000..4e225c89 --- /dev/null +++ b/server/game/src/server_thread/mod.rs @@ -0,0 +1,296 @@ +use std::{ + net::SocketAddrV4, + sync::{ + atomic::{AtomicBool, AtomicI32, Ordering}, + Arc, + }, + time::{Duration, SystemTime}, +}; + +use parking_lot::Mutex as SyncMutex; + +use bytebuffer::{ByteBuffer, ByteReader}; +use crypto_box::{ + aead::{Aead, AeadCore, OsRng}, + ChaChaBox, +}; +use log::{debug, warn}; +use tokio::sync::{mpsc, Mutex}; + +use crate::{data::*, server::GameServer}; + +mod error; +mod handlers; + +pub use error::{PacketHandlingError, Result}; + +// TODO adjust this to PlayerData size in the future plus some headroom +pub const SMALL_PACKET_LIMIT: usize = 128; +const CHANNEL_BUFFER_SIZE: usize = 8; + +pub enum ServerThreadMessage { + Packet(Vec<u8>), + SmallPacket([u8; SMALL_PACKET_LIMIT]), + BroadcastVoice(Arc<VoiceBroadcastPacket>), + TerminationNotice(String), +} + +pub struct GameServerThread { + game_server: &'static GameServer, + + rx: Mutex<mpsc::Receiver<ServerThreadMessage>>, + tx: mpsc::Sender<ServerThreadMessage>, + awaiting_termination: AtomicBool, + pub authenticated: AtomicBool, + crypto_box: SyncMutex<Option<ChaChaBox>>, + + peer: SocketAddrV4, + pub account_id: AtomicI32, + pub level_id: AtomicI32, + pub account_data: SyncMutex<PlayerAccountData>, + + last_voice_packet: SyncMutex<SystemTime>, +} + +impl GameServerThread { + /* public api for the main server */ + + pub fn new(peer: SocketAddrV4, game_server: &'static GameServer) -> Self { + let (tx, rx) = mpsc::channel::<ServerThreadMessage>(CHANNEL_BUFFER_SIZE); + Self { + tx, + rx: Mutex::new(rx), + peer, + crypto_box: SyncMutex::new(None), + account_id: AtomicI32::new(0), + level_id: AtomicI32::new(0), + authenticated: AtomicBool::new(false), + game_server, + awaiting_termination: AtomicBool::new(false), + account_data: SyncMutex::new(PlayerAccountData::default()), + last_voice_packet: SyncMutex::new(SystemTime::now()), + } + } + + pub async fn run(&self) { + let mut rx = self.rx.lock().await; + + loop { + if self.awaiting_termination.load(Ordering::Relaxed) { + break; + } + + match tokio::time::timeout(Duration::from_secs(60), rx.recv()).await { + Ok(Some(message)) => match self.handle_message(message).await { + Ok(()) => {} + Err(err) => warn!("[@{}]: {}", self.peer, err.to_string()), + }, + Ok(None) | Err(_) => break, // sender closed | timeout + }; + } + } + + pub fn push_new_message(&self, data: ServerThreadMessage) -> anyhow::Result<()> { + self.tx.try_send(data)?; + Ok(()) + } + + pub fn terminate(&self) { + self.awaiting_termination.store(true, Ordering::Relaxed); + } + + /* private utilities */ + + async fn send_packet<P: Packet>(&self, packet: &P) -> Result<()> { + #[cfg(debug_assertions)] + log::debug!("Sending {}", P::NAME); + + let serialized = self.serialize_packet(packet)?; + self.send_buffer(serialized.as_bytes()).await + } + + // fast packet sending with best-case zero heap allocation + // packet must implement EncodableWithKnownSize to be fast sendable + // it also must **NOT** be encrypted, for dynamically sized or encrypted packets, use send_packet. + // on average 2-3x faster than send_packet, even worst case should be faster by a bit + // TODO encryption + async fn send_packet_fast<P: Packet + EncodableWithKnownSize>(&self, packet: &P) -> Result<()> { + assert!(!P::ENCRYPTED, "Attempting to fast encode an encrypted packet ({})", P::NAME); + + let to_send: Result<Option<Vec<u8>>> = alloca::with_alloca(PacketHeader::SIZE + P::ENCODED_SIZE, move |data| { + // safety: 'data' will have garbage data but that is considered safe for all our intents and purposes + // as `FastByteBuffer::as_bytes()` will only return what was already written. + let data = unsafe { + let ptr = data.as_mut_ptr().cast::<u8>(); + let len = std::mem::size_of_val(data); + std::slice::from_raw_parts_mut(ptr, len) + }; + + let mut buf = FastByteBuffer::new(data); + + buf.write_packet_header::<P>(); + buf.write_value(packet); + + let send_data = buf.as_bytes(); + match self.send_buffer_immediate(send_data) { + Err(PacketHandlingError::SocketWouldBlock) => Ok(Some(send_data.to_vec())), + Err(e) => Err(e), + Ok(()) => Ok(None), + } + }); + + if let Some(to_send) = to_send? { + self.send_buffer(&to_send).await?; + } + + Ok(()) + } + + async fn send_buffer(&self, buffer: &[u8]) -> Result<()> { + self.game_server + .socket + .send_to(buffer, self.peer) + .await + .map(|_size| ()) + .map_err(PacketHandlingError::SocketSendFailed) + } + + // attempt to send a buffer immediately to the socket, but if it requires blocking then nuh uh + fn send_buffer_immediate(&self, buffer: &[u8]) -> Result<()> { + self.game_server + .socket + .try_send_to(buffer, std::net::SocketAddr::V4(self.peer)) + .map(|_| ()) + .map_err(|e| { + if e.kind() == std::io::ErrorKind::WouldBlock { + PacketHandlingError::SocketWouldBlock + } else { + PacketHandlingError::SocketSendFailed(e) + } + }) + } + + fn serialize_packet<P: Packet>(&self, packet: &P) -> Result<ByteBuffer> { + let mut buf = ByteBuffer::new(); + buf.write_packet_header::<P>(); + + if !P::ENCRYPTED { + buf.write_value(packet); + return Ok(buf); + } + + let cbox = self.crypto_box.lock(); + + // should never happen + #[cfg(debug_assertions)] + if !cbox.is_some() { + return Err(PacketHandlingError::WrongCryptoBoxState); + } + + // encode the packet into a temp buf, encrypt the data and write to the initial buf + + let mut cltxtbuf = ByteBuffer::new(); + cltxtbuf.write_value(packet); + + let cbox = cbox.as_ref().unwrap(); + let nonce = ChaChaBox::generate_nonce(&mut OsRng); + + let encrypted = cbox + .encrypt(&nonce, cltxtbuf.as_bytes()) + .map_err(|e| PacketHandlingError::EncryptionError(e.to_string()))?; + + #[cfg(not(rust_analyzer))] // i am so sorry + buf.write_bytes(&nonce); + buf.write_bytes(&encrypted); + + Ok(buf) + } + + async fn handle_message(&self, message: ServerThreadMessage) -> Result<()> { + match message { + ServerThreadMessage::Packet(data) => self.handle_packet(&data).await?, + ServerThreadMessage::SmallPacket(data) => self.handle_packet(&data).await?, + ServerThreadMessage::BroadcastVoice(voice_packet) => self.send_packet(&*voice_packet).await?, + ServerThreadMessage::TerminationNotice(message) => { + self.terminate(); + self.send_packet(&ServerDisconnectPacket { message }).await?; + } + } + + Ok(()) + } + + /* packet handlers */ + + async fn handle_packet(&self, message: &[u8]) -> Result<()> { + #[cfg(debug_assertions)] + if message.len() < PacketHeader::SIZE { + return Err(PacketHandlingError::MalformedMessage); + } + + let mut data = ByteReader::from_bytes(message); + + let header = data.read_packet_header()?; + + // minor optimization + if header.packet_id == PlayerDataPacket::PACKET_ID { + return self.handle_player_data(&mut data).await; + } + + // also for optimization, reject the voice packet immediately if the player is blocked from vc + if header.packet_id == VoicePacket::PACKET_ID { + let accid = self.account_id.load(Ordering::Relaxed); + if self.game_server.chat_blocked(accid) { + debug!("blocking voice packet from {accid}"); + return Ok(()); + } + } + + // reject cleartext credentials + if header.packet_id == LoginPacket::PACKET_ID && !header.encrypted { + return Err(PacketHandlingError::MalformedLoginAttempt); + } + + let cleartext_vec: Vec<u8>; + if header.encrypted { + if message.len() < 24 + PacketHeader::SIZE { + return Err(PacketHandlingError::MalformedCiphertext); + } + + let cbox = self.crypto_box.lock(); + if cbox.is_none() { + return Err(PacketHandlingError::WrongCryptoBoxState); + } + + let cbox = cbox.as_ref().unwrap(); + + let nonce = &message[PacketHeader::SIZE..PacketHeader::SIZE + 24]; + let ciphertext = &message[PacketHeader::SIZE + 24..]; + + cleartext_vec = cbox + .decrypt(nonce.into(), ciphertext) + .map_err(|e| PacketHandlingError::DecryptionError(e.to_string()))?; + data = ByteReader::from_bytes(&cleartext_vec); + } + + match header.packet_id { + /* connection related */ + PingPacket::PACKET_ID => self.handle_ping(&mut data).await, + CryptoHandshakeStartPacket::PACKET_ID => self.handle_crypto_handshake(&mut data).await, + KeepalivePacket::PACKET_ID => self.handle_keepalive(&mut data).await, + LoginPacket::PACKET_ID => self.handle_login(&mut data).await, + DisconnectPacket::PACKET_ID => self.handle_disconnect(&mut data), + + /* game related */ + SyncIconsPacket::PACKET_ID => self.handle_sync_icons(&mut data).await, + RequestProfilesPacket::PACKET_ID => self.handle_request_profiles(&mut data).await, + LevelJoinPacket::PACKET_ID => self.handle_level_join(&mut data).await, + LevelLeavePacket::PACKET_ID => self.handle_level_leave(&mut data).await, + PlayerDataPacket::PACKET_ID => self.handle_player_data(&mut data).await, + RequestPlayerListPacket::PACKET_ID => self.handle_request_player_list(&mut data).await, + + VoicePacket::PACKET_ID => self.handle_voice(&mut data).await, + x => Err(PacketHandlingError::NoHandler(x)), + } + } +} diff --git a/server/readme.md b/server/readme.md index ec86e181..68deaa0e 100644 --- a/server/readme.md +++ b/server/readme.md @@ -1,12 +1,12 @@ # Globed Server -Prebuilt server binaries are available for Linux (x64 and ARM64) and Windows (x64) in every GitHub release. +Prebuilt server binaries are available for Linux (x64 and ARM64) and Windows (x64) in every GitHub release. The server is *extremely* optimized so it is very lightweight and will run well even on weak hardware. If you want to build it yourself, do note that the central server must be compiled with a nightly Rust toolchain, until `async fn` in traits becomes stable (which is set to be in Rust 1.75). ## Central server -The central server doesn't uses a single JSON file for configuration. By default, the file is put in `central-conf.json` in the current working directory when you run the server, but it can be overriden with the environment variable `GLOBED_CONFIG_PATH`. The path can be a folder or a full file path. +The central server uses a single JSON file for configuration. By default, the file is created with the name `central-conf.json` in the current working directory when you run the server, but it can be overriden with the environment variable `GLOBED_CONFIG_PATH`. The path can be a folder or a full file path. ### Central server configuration @@ -40,25 +40,28 @@ Note that the server is written with security in mind, so many of those options <sup>**</sup> - it may take a few minutes for you to see any changes -The minimum log level is `Warn` for other crates and `Trace` for the server itself in debug builds. In release builds, the default is `Info` and you can change it to `Warn` by defining the environment variable `GLOBED_LESS_LOG=1` - -## Game +## Game server To bridge the servers together, you must use the password from the `game_server_password` option in the central server configuration. Then, you have 2 options whenever you start the server: ```sh -./globed-game-server 0.0.0.0:41001 http://127.0.0.1:41000 password +./globed-game-server.exe 0.0.0.0:41001 http://127.0.0.1:41000 password -# or like this: (replace 'export' with 'set' on windows) +# or like this: -export GLOBED_GS_ADDRESS=0.0.0.0:41001 -export GLOBED_GS_CENTRAL_URL=http://127.0.0.1:41000 -export GLOBED_GS_CENTRAL_PASSWORD=password -./globed-game-server +# on linux remember to replace `set` with `export` and all that +set GLOBED_GS_ADDRESS=0.0.0.0:41001 +set GLOBED_GS_CENTRAL_URL=http://127.0.0.1:41000 +set GLOBED_GS_CENTRAL_PASSWORD=password +./globed-game-server.exe ``` Replace `0.0.0.0:41001` with the address you want the game server to listen on, `http://127.0.0.1:41000` with the URL of your central server, and `password` with the password. -If you want to start the game server in a standalone manner, so that it doesn't need a central server to work, replace the URL with the string "none". Do keep in mind this disables player authentication and some other features, and is only recommended for testing. +**You can start the game server in a standalone mode (without needing a central server)**. For that, simply replace the central URL with the string `none`. Do keep in mind this disables player authentication completely and is recommended only for testing purposes. + +## Extra + +In debug builds, all logging is enabled. In release builds, the `Debug` and `Trace` levels are disabled, so you will only see logs with levels `Info`, `Warn` and `Error`. -The minimum log levels are the same as in the central server, except the environment variable to change them is `GLOBED_GS_LESS_LOG`. +This can be changed by setting the environment variable `GLOBED_LESS_LOG=1` for the central server, or `GLOBED_GS_LESS_LOG=1` for the game server. With this option, only logs with levels `Warn` and `Error` will be printed (in both debug and release builds) diff --git a/src/data/bytebuffer.hpp b/src/data/bytebuffer.hpp index 4ef27b18..18748db3 100644 --- a/src/data/bytebuffer.hpp +++ b/src/data/bytebuffer.hpp @@ -23,8 +23,8 @@ template <typename T> concept Serializable = Encodable<T> && Decodable<T>; // helper macros so you can do GLOBED_ENCODE {...} in serializable structs or packets -#define GLOBED_ENCODE void encode(ByteBuffer& buf) const -#define GLOBED_DECODE void decode(ByteBuffer& buf) +#define GLOBED_ENCODE inline void encode(ByteBuffer& buf) const +#define GLOBED_DECODE inline void decode(ByteBuffer& buf) class ByteBuffer { public: diff --git a/src/data/packets/packet.hpp b/src/data/packets/packet.hpp index dc2fc73e..edf03af4 100644 --- a/src/data/packets/packet.hpp +++ b/src/data/packets/packet.hpp @@ -32,6 +32,22 @@ class Packet { virtual packetid_t getPacketId() const = 0; virtual bool getEncrypted() const = 0; +}; - static constexpr size_t HEADER_LEN = sizeof(packetid_t) + 1; +class PacketHeader { +public: + static constexpr size_t SIZE = sizeof(packetid_t) + sizeof(bool); + + GLOBED_ENCODE { + buf.writeU16(id); + buf.writeBool(encrypted); + } + + GLOBED_DECODE { + id = buf.readU16(); + encrypted = buf.readBool(); + } + + packetid_t id; + bool encrypted; }; \ No newline at end of file diff --git a/src/net/game_socket.cpp b/src/net/game_socket.cpp index 3f573d43..0a84bed6 100644 --- a/src/net/game_socket.cpp +++ b/src/net/game_socket.cpp @@ -20,36 +20,34 @@ std::shared_ptr<Packet> GameSocket::recvPacket() { auto received = receive(reinterpret_cast<char*>(buffer), BUF_SIZE); GLOBED_REQUIRE(received > 0, "failed to receive data from a socket") - // read the header, 2 bytes for packet ID, 1 byte for encrypted - GLOBED_REQUIRE(received >= Packet::HEADER_LEN, "packet is missing a header") + GLOBED_REQUIRE(received >= PacketHeader::SIZE, "packet is missing a header") ByteBuffer buf(reinterpret_cast<byte*>(buffer), received); // read header - packetid_t packetId = buf.readU16(); - bool encrypted = buf.readU8() != 0; + auto header = buf.readValue<PacketHeader>(); // packet size without the header - size_t messageLength = received - Packet::HEADER_LEN; + size_t messageLength = received - PacketHeader::SIZE; #ifdef GLOBED_DEBUG_PACKETS - PacketLogger::get().record(packetId, encrypted, false, received); + PacketLogger::get().record(header.id, header.encrypted, false, received); #endif - auto packet = matchPacket(packetId); + auto packet = matchPacket(header.id); - GLOBED_REQUIRE(packet.get() != nullptr, std::string("invalid server-side packet: ") + std::to_string(packetId)) + GLOBED_REQUIRE(packet.get() != nullptr, std::string("invalid server-side packet: ") + std::to_string(header.id)) - if (packet->getEncrypted() && !encrypted) { + if (packet->getEncrypted() && !header.encrypted) { GLOBED_REQUIRE(false, "server sent a cleartext packet when expected an encrypted one") } - if (encrypted) { + if (header.encrypted) { GLOBED_REQUIRE(box.get() != nullptr, "attempted to decrypt a packet when no cryptobox is initialized") bytevector& bufvec = buf.getDataRef(); - messageLength = box->decryptInPlace(bufvec.data() + Packet::HEADER_LEN, messageLength); - buf.resize(messageLength + Packet::HEADER_LEN); + messageLength = box->decryptInPlace(bufvec.data() + PacketHeader::SIZE, messageLength); + buf.resize(messageLength + PacketHeader::SIZE); } packet->decode(buf); @@ -59,18 +57,23 @@ std::shared_ptr<Packet> GameSocket::recvPacket() { void GameSocket::sendPacket(std::shared_ptr<Packet> packet) { ByteBuffer buf; - buf.writeU16(packet->getPacketId()); - buf.writeU8(static_cast<uint8_t>(packet->getEncrypted())); + PacketHeader header = { + .id = packet->getPacketId(), + .encrypted = packet->getEncrypted() + }; + + buf.writeValue(header); packet->encode(buf); - size_t packetSize = buf.size() - Packet::HEADER_LEN; + + size_t packetSize = buf.size() - PacketHeader::SIZE; bytevector& dataref = buf.getDataRef(); if (packet->getEncrypted()) { GLOBED_REQUIRE(box.get() != nullptr, "attempted to encrypt a packet when no cryptobox is initialized") // grow the vector by CryptoBox::PREFIX_LEN extra bytes to do in-place encryption buf.grow(CryptoBox::PREFIX_LEN); - box->encryptInPlace(dataref.data() + Packet::HEADER_LEN, packetSize); + box->encryptInPlace(dataref.data() + PacketHeader::SIZE, packetSize); } #ifdef GLOBED_DEBUG_PACKETS