From 70c76f14dd1f97bf3fe7c6970f5b2077ab06493d Mon Sep 17 00:00:00 2001 From: dank_meme01 <42031238+dankmeme01@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:15:32 +0100 Subject: [PATCH] uhhh stuff i guess --- CMakeLists.txt | 3 + LICENSE | 21 ++++ README.md | 18 ++-- server/game-derives/src/lib.rs | 71 +++++++----- server/game/src/data/bytebufferext.rs | 4 +- server/game/src/data/packets/mod.rs | 25 +---- server/game/src/data/types/common.rs | 2 +- server/game/src/server.rs | 101 +++++++++++++++--- .../src/server_thread/handlers/connection.rs | 2 +- server/game/src/server_thread/mod.rs | 21 ++-- server/game/src/util/channel.rs | 18 ++-- server/game/src/util/lockfreemutcell.rs | 32 ++++++ server/game/src/util/mod.rs | 6 +- server/game/src/util/rate_limiter.rs | 29 +---- server/readme.md | 70 +++++++++--- src/crypto/box.cpp | 2 - src/crypto/secret_box.cpp | 2 - src/main.cpp | 13 ++- src/managers/server_manager.cpp | 4 + src/managers/server_manager.hpp | 3 + src/managers/settings.cpp | 7 +- src/managers/settings.hpp | 45 +++++--- src/net/network_manager.cpp | 40 +++++-- src/net/network_manager.hpp | 17 +-- src/ui/hooks/menu_layer.hpp | 2 +- src/ui/hooks/play_layer.hpp | 26 +++-- src/ui/menu/main/globed_menu_layer.cpp | 75 +++++++++++-- src/ui/menu/main/globed_menu_layer.hpp | 3 +- src/ui/menu/player_list/player_list_popup.cpp | 2 +- src/util/crypto.cpp | 11 +- src/util/crypto.hpp | 11 +- src/util/formatting.cpp | 14 --- src/util/formatting.hpp | 20 +++- src/util/time.hpp | 5 + 34 files changed, 493 insertions(+), 232 deletions(-) create mode 100644 LICENSE create mode 100644 server/game/src/util/lockfreemutcell.rs diff --git a/CMakeLists.txt b/CMakeLists.txt index 66fd41b6..5510d9bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_OSX_ARCHITECTURES "x86_64") set(CMAKE_CXX_VISIBILITY_PRESET hidden) +# Enable LTO (2.5x less binary size, no noticable compile time hit) +set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) + project(globed2 VERSION 1.0.0) option(ENABLE_DEBUG "Debug mode" OFF) diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1627d546 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 dank_meme + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 8696c384..39cf957b 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,9 @@ This repository contains the complete rewrite of Globed, for Geometry Dash 2.2 a ## Features * Real-time multiplayer -* Voice chat and text messages with **full encryption and zero logging** -* blazing fast server written in pure Rust 🚀 +* Voice chat and text messages with **full encryption and zero logging/storage** +* Available on three of the major platforms (Windows, Mac and Android) +* Fully open-source, including the server code ## Installation @@ -23,7 +24,6 @@ Planned features: 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,9 +38,11 @@ camila314 - thank you for [UIBuilder](https://github.com/camila314/uibuilder) RobTop - thank you for releasing this awesome game :) -### Used libraries +### License -* [Geode](https://geode-sdk.org/) -* [UIBuilder](https://github.com/camila314/uibuilder) -* [Opus](https://github.com/xiph/opus) -* [libsodium](https://github.com/jedisct1/libsodium) \ No newline at end of file +Globed is licensed under the MIT license. All of the following libraries used in Globed have their own licenses you may want to read: + +* [Geode](https://geode-sdk.org/) - [BSL 1.0](https://github.com/geode-sdk/geode/blob/main/LICENSE.txt) +* [UIBuilder](https://github.com/camila314/uibuilder) - [MIT](https://github.com/camila314/uibuilder/blob/main/LICENSE) +* [Opus](https://github.com/xiph/opus) - [BSD 3-Clause (?)](https://github.com/xiph/opus/blob/master/COPYING) +* [libsodium](https://github.com/jedisct1/libsodium) - [ISC](https://github.com/jedisct1/libsodium/blob/master/LICENSE) \ No newline at end of file diff --git a/server/game-derives/src/lib.rs b/server/game-derives/src/lib.rs index d5de94ac..3d8dc484 100644 --- a/server/game-derives/src/lib.rs +++ b/server/game-derives/src/lib.rs @@ -1,14 +1,16 @@ +#![feature(proc_macro_diagnostic)] #![allow(clippy::missing_panics_doc)] use darling::FromDeriveInput; -use proc_macro::{self, TokenStream}; +use proc_macro::{self, Span, TokenStream}; use quote::{quote, ToTokens}; use syn::{parse_macro_input, punctuated::Punctuated, Data, DeriveInput, Meta, Token}; -/// Implements `Encodable` for the given type. For `Encodable` to be successfully derived, -/// for structs, all of the members of the struct must also implement `Encodable`. +/// Implements `Encodable` for the given type, allowing you to serialize it into a regular `ByteBuffer`. +/// For `Encodable` to be successfully derived, for structs, all of the members of the struct must also implement `Encodable`. +/// The members are serialized in the same order they are laid out in the struct. /// -/// For enums, the enum must have no associated data fields (only variants), and may have a +/// For enums, the enum must derive `Copy`, must be plain (no associated data fields), and may have a /// `#[repr(u*)]` or `#[repr(i*)]` attribute to indicate the encoded type. By default it will be `i32` if omitted. #[proc_macro_derive(Encodable)] pub fn derive_encodable(input: TokenStream) -> TokenStream { @@ -67,8 +69,8 @@ pub fn derive_encodable(input: TokenStream) -> TokenStream { gen.into() } -/// Implements `KnownSize` for the given type. For `KnownSize` to be successfully derived, -/// for structs, all of the members of the struct must also implement `KnownSize`. +/// Implements `KnownSize` for the given type, allowing you to serialize it into a `FastByteBuffer`. +/// For `KnownSize` to be successfully derived, for structs, all of the members of the struct must also implement `KnownSize`. /// /// For enums, all the same limitations apply as in `Encodable`. #[proc_macro_derive(KnownSize)] @@ -114,8 +116,9 @@ pub fn derive_known_size(input: TokenStream) -> TokenStream { gen.into() } -/// Implements `Decodable` for the given type. For `Decodable` to be successfully derived, -/// for structs, all of the members of the struct must also implement `Decodable`. +/// Implements `Decodable` for the given type, allowing you to deserialize it from a `ByteReader`/`ByteBuffer`. +/// For `Decodable` to be successfully derived, for structs, all of the members of the struct must also implement `Decodable`. +/// The members are deserialized in the same order they are laid out in the struct. /// /// For enums, all the same limitations apply as in `Encodable` plus the enum must have explicitly specified values for all variants. #[proc_macro_derive(Decodable)] @@ -246,8 +249,13 @@ fn get_enum_repr_type(input: &DeriveInput) -> proc_macro2::TokenStream { } } - // assume i32 by default - repr_type.unwrap_or(quote! { i32 }) + if repr_type.is_none() { + // if not specified, assume i32 and give a warning. + repr_type = Some(quote! { i32 }); + Span::call_site().warning("enum repr type not specified - assuming i32. it is recommended to add #[repr(type)] before the enum as that makes it more explicit.").emit(); + } + + repr_type.unwrap() } #[derive(FromDeriveInput)] @@ -263,37 +271,44 @@ struct PacketAttributes { /// ```rust /// #[derive(Packet, Encodable, Decodable)] /// #[packet(id = 10000, encrypted = false)] -/// pub struct MyPacket { /* ... */ } +/// pub struct MyPacket { /* fields */ } /// ``` #[proc_macro_derive(Packet, attributes(packet))] pub fn packet(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); - let opts = PacketAttributes::from_derive_input(&input).expect("wrong value passed into #[packet] derive macro"); + let Ok(opts) = PacketAttributes::from_derive_input(&input) else { + return quote! { + compile_error!("invalid or missing signature for #[packet] attribute, please see documentation for `Packet` proc macro"); + } + .into(); + }; + let DeriveInput { ident, .. } = input; let id = opts.id; let enc = opts.encrypted; - let output = quote! { - impl PacketMetadata for #ident { - const PACKET_ID: crate::data::packets::PacketId = #id; - const ENCRYPTED: bool = #enc; - const NAME: &'static str = stringify!(#ident); - } + let output = match &input.data { + Data::Struct(_) => { + quote! { + impl PacketMetadata for #ident { + const PACKET_ID: crate::data::packets::PacketId = #id; + const ENCRYPTED: bool = #enc; + const NAME: &'static str = stringify!(#ident); + } - impl Packet for #ident { - fn get_packet_id(&self) -> crate::data::packets::PacketId { - #id - } + impl Packet for #ident {} - fn get_encrypted(&self) -> bool { - #enc + impl #ident { + pub const fn header() -> crate::data::packets::PacketHeader { + crate::data::packets::PacketHeader::from_packet::() + } + } } } - - impl #ident { - pub const fn header() -> crate::data::packets::PacketHeader { - crate::data::packets::PacketHeader::from_packet::() + Data::Enum(_) | Data::Union(_) => { + quote! { + compile_error!("Packet cannot be derived for enums or unions"); } } }; diff --git a/server/game/src/data/bytebufferext.rs b/server/game/src/data/bytebufferext.rs index 06e63d6b..ec4ab06f 100644 --- a/server/game/src/data/bytebufferext.rs +++ b/server/game/src/data/bytebufferext.rs @@ -57,8 +57,8 @@ pub trait KnownSize { pub const MAX_NAME_SIZE: usize = 32; /// maximum characters in a `ServerNoticePacket` or `ServerDisconnectPacket` (164) pub const MAX_NOTICE_SIZE: usize = 164; -/// maximum characters in a user message (256) -pub const MAX_MESSAGE_SIZE: usize = 256; +/// maximum characters in a user message (156) +pub const MAX_MESSAGE_SIZE: usize = 156; /// max profiles that can be requested in `RequestProfilesPacket` (128) pub const MAX_PROFILES_REQUESTED: usize = 128; diff --git a/server/game/src/data/packets/mod.rs b/server/game/src/data/packets/mod.rs index cbefde62..54c231f8 100644 --- a/server/game/src/data/packets/mod.rs +++ b/server/game/src/data/packets/mod.rs @@ -4,14 +4,11 @@ pub mod server; pub use client::*; pub use server::*; -use crate::data::bytebufferext::*; +use crate::data::*; -type PacketId = u16; +pub type PacketId = u16; -pub trait Packet: Send + Sync + PacketMetadata { - fn get_packet_id(&self) -> PacketId; - fn get_encrypted(&self) -> bool; -} +pub trait Packet: PacketMetadata {} // god i hate this pub trait PacketMetadata { @@ -20,6 +17,7 @@ pub trait PacketMetadata { const NAME: &'static str; } +#[derive(Encodable, Decodable, KnownSize)] pub struct PacketHeader { pub packet_id: PacketId, pub encrypted: bool, @@ -34,18 +32,5 @@ impl PacketHeader { } } - pub const SIZE: usize = size_of_types!(PacketId, bool); + pub const SIZE: usize = Self::ENCODED_SIZE; } - -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 }) -}); - -size_calc_impl!(PacketHeader, PacketHeader::SIZE); diff --git a/server/game/src/data/types/common.rs b/server/game/src/data/types/common.rs index 66e680c3..b1bb8e08 100644 --- a/server/game/src/data/types/common.rs +++ b/server/game/src/data/types/common.rs @@ -34,7 +34,7 @@ impl_primitive!(f64, read_f64, write_f64); encode_impl!(String, buf, self, buf.write_string(self)); decode_impl!(String, buf, Ok(buf.read_string()?)); -encode_impl!(&str, buf, self, buf.write_string(self)); +encode_impl!(str, buf, self, buf.write_string(self)); /* Option */ diff --git a/server/game/src/server.rs b/server/game/src/server.rs index ba3b8618..0f92256f 100644 --- a/server/game/src/server.rs +++ b/server/game/src/server.rs @@ -1,9 +1,10 @@ use std::{ - net::{SocketAddr, SocketAddrV4}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, sync::{atomic::Ordering, Arc}, time::Duration, }; +use bytebuffer::ByteReader; use parking_lot::Mutex as SyncMutex; use anyhow::{anyhow, bail}; @@ -20,6 +21,7 @@ use crate::{ data::*, server_thread::{GameServerThread, ServerThreadMessage, SMALL_PACKET_LIMIT}, state::ServerState, + util::SimpleRateLimiter, }; const MAX_PACKET_SIZE: usize = 8192; @@ -34,6 +36,7 @@ pub struct GameServer { pub state: ServerState, pub socket: UdpSocket, pub threads: SyncMutex>>, + rate_limiters: SyncMutex>, pub secret_key: SecretKey, pub central_conf: SyncMutex, pub config: GameServerConfiguration, @@ -52,6 +55,7 @@ impl GameServer { state, socket, threads: SyncMutex::new(FxHashMap::default()), + rate_limiters: SyncMutex::new(FxHashMap::default()), secret_key: SecretKey::generate(&mut OsRng), central_conf: SyncMutex::new(central_conf), config, @@ -62,6 +66,7 @@ impl GameServer { pub async fn run(&'static self) -> ! { info!("Server launched on {}", self.socket.local_addr().unwrap()); + // spawn central conf refresher (runs every 5 minutes) if !self.standalone { tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(300)); @@ -77,6 +82,17 @@ impl GameServer { }); } + // spawn stale rate limiter remover (runs once an hour) + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(3600)); + interval.tick().await; + + loop { + interval.tick().await; + self.remove_stale_rate_limiters(); + } + }); + // preallocate a buffer let mut buf = [0u8; MAX_PACKET_SIZE]; @@ -194,6 +210,23 @@ impl GameServer { SocketAddr::V6(_) => bail!("rejecting request from ipv6 host"), }; + let ip_addr = peer.ip(); + + // block packets if the client is sending too many of them + if self.is_rate_limited(*ip_addr) { + if cfg!(debug_assertions) { + bail!("{ip_addr} is ratelimited"); + } + + // silently drop the packet in release mode + return Ok(()); + } + + // if it's a small packet like ping, we don't need to send it via the channel and can handle it right here + if self.try_fast_handle(&buf[..len], peer).await? { + return Ok(()); + } + let thread = self.threads.lock().get(&peer).cloned(); let thread = if let Some(thread) = thread { @@ -216,21 +249,10 @@ impl GameServer { }); self.threads.lock().insert(peer, thread_cl.clone()); + thread_cl }; - // check if the client is sending too many packets - // safety: `thread.rate_limiter` is only used here and never accessed anywhere else. - let allowed = unsafe { thread.rate_limiter.try_tick() }; - if !allowed { - if cfg!(debug_assertions) { - bail!("{peer} is ratelimited"); - } - - // silently reject the packet in release mode - return Ok(()); - } - // don't heap allocate for small packets let message = if len <= SMALL_PACKET_LIMIT { let mut smallbuf = [0u8; SMALL_PACKET_LIMIT]; @@ -246,6 +268,53 @@ impl GameServer { Ok(()) } + /// Try to fast handle a packet if the packet does not require spawning a new "thread". + /// Returns true if packet was successfully handled, in which case the data should be discarded. + async fn try_fast_handle(&'static self, data: &[u8], peer: SocketAddrV4) -> anyhow::Result { + let mut byte_reader = ByteReader::from_bytes(data); + let header = byte_reader.read_packet_header().map_err(|e| anyhow!("{e}"))?; + + match header.packet_id { + PingPacket::PACKET_ID => { + let pkt = PingPacket::decode_from_reader(&mut byte_reader).map_err(|e| anyhow!("{e}"))?; + let response = PingResponsePacket { + id: pkt.id, + player_count: self.state.player_count.load(Ordering::Relaxed), + }; + + let mut buf_array = [0u8; PacketHeader::SIZE + PingResponsePacket::ENCODED_SIZE]; + let mut buf = FastByteBuffer::new(&mut buf_array); + buf.write_packet_header::(); + buf.write_value(&response); + + let send_bytes = buf.as_bytes(); + + match self.socket.try_send_to(send_bytes, peer.into()) { + Ok(_) => Ok(true), + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { + self.socket.send_to(send_bytes, peer).await?; + Ok(true) + } + Err(e) => Err(e.into()), + } + } + + _ => Ok(false), + } + } + + fn is_rate_limited(&'static self, addr: Ipv4Addr) -> bool { + let mut limiters = self.rate_limiters.lock(); + if let Some(limiter) = limiters.get_mut(&addr) { + !limiter.try_tick() + } else { + let rl_request_limit = (self.central_conf.lock().tps + 5) as usize; + let rate_limiter = SimpleRateLimiter::new(rl_request_limit, Duration::from_millis(950)); + limiters.insert(addr, rate_limiter); + false + } + } + fn post_disconnect_cleanup(&'static self, thread: &GameServerThread, peer: SocketAddrV4) { self.threads.lock().remove(&peer); @@ -268,6 +337,12 @@ impl GameServer { } } + /// Removes rate limiters of IP addresses that haven't sent a packet in a long time (10 minutes) + fn remove_stale_rate_limiters(&'static self) { + let mut limiters = self.rate_limiters.lock(); + limiters.retain(|_, limiter| limiter.since_last_refill() < Duration::from_secs(600)); + } + async fn refresh_bootdata(&'static self) -> anyhow::Result<()> { let response = self .config diff --git a/server/game/src/server_thread/handlers/connection.rs b/server/game/src/server_thread/handlers/connection.rs index 84acc411..68f25a67 100644 --- a/server/game/src/server_thread/handlers/connection.rs +++ b/server/game/src/server_thread/handlers/connection.rs @@ -72,7 +72,7 @@ impl GameServerThread { gs_handler!(self, handle_login, LoginPacket, packet, { if self.game_server.standalone { - debug!("Bypassing login for {} ({})", packet.name, packet.account_id); + debug!("Login successful from {} ({})", packet.name, 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); diff --git a/server/game/src/server_thread/mod.rs b/server/game/src/server_thread/mod.rs index 8259f53a..9d2a5b41 100644 --- a/server/game/src/server_thread/mod.rs +++ b/server/game/src/server_thread/mod.rs @@ -16,12 +16,7 @@ use crypto_box::{ }; use globed_shared::logger::*; -use crate::{ - data::*, - server::GameServer, - server_thread::handlers::*, - util::{SimpleRateLimiter, UnsafeChannel, UnsafeRateLimiter}, -}; +use crate::{data::*, server::GameServer, server_thread::handlers::*, util::TokioChannel}; mod error; mod handlers; @@ -33,6 +28,8 @@ use self::handlers::MAX_VOICE_PACKET_SIZE; // TODO adjust this to PlayerData size in the future plus some headroom pub const SMALL_PACKET_LIMIT: usize = 164; const CHANNEL_BUFFER_SIZE: usize = 8; + +// do not touch those, encryption related const NONCE_SIZE: usize = 24; const MAC_SIZE: usize = 16; @@ -48,7 +45,7 @@ pub enum ServerThreadMessage { pub struct GameServerThread { game_server: &'static GameServer, - channel: UnsafeChannel, + channel: TokioChannel, awaiting_termination: AtomicBool, pub authenticated: AtomicBool, crypto_box: OnceLock, @@ -59,18 +56,14 @@ pub struct GameServerThread { pub account_data: SyncMutex, last_voice_packet: AtomicU64, - pub rate_limiter: UnsafeRateLimiter, // do NOT interact with this field oustide of GameServer. } impl GameServerThread { /* public api for the main server */ pub fn new(peer: SocketAddrV4, game_server: &'static GameServer) -> Self { - let rl_request_limit = (game_server.central_conf.lock().tps + 5) as usize; - let rate_limiter = SimpleRateLimiter::new(rl_request_limit, Duration::from_millis(950)); - let rate_limiter = UnsafeRateLimiter::new(rate_limiter); Self { - channel: UnsafeChannel::new(CHANNEL_BUFFER_SIZE), + channel: TokioChannel::new(CHANNEL_BUFFER_SIZE), peer, crypto_box: OnceLock::new(), account_id: AtomicI32::new(0), @@ -80,7 +73,6 @@ impl GameServerThread { awaiting_termination: AtomicBool::new(false), account_data: SyncMutex::new(PlayerAccountData::default()), last_voice_packet: AtomicU64::new(0), - rate_limiter, } } @@ -90,7 +82,8 @@ impl GameServerThread { break; } - match tokio::time::timeout(Duration::from_secs(60), self.channel.recv()).await { + // safety: we are the only receiver for this channel. + match tokio::time::timeout(Duration::from_secs(60), unsafe { self.channel.recv() }).await { Ok(Ok(message)) => match self.handle_message(message).await { Ok(()) => {} Err(err) => self.print_error(&err), diff --git a/server/game/src/util/channel.rs b/server/game/src/util/channel.rs index 3d902176..ebe2124a 100644 --- a/server/game/src/util/channel.rs +++ b/server/game/src/util/channel.rs @@ -1,16 +1,17 @@ -use std::cell::SyncUnsafeCell; use tokio::sync::mpsc; +use super::LockfreeMutCell; + /// Simple wrapper around `tokio::mpsc` channels except receiver does not need to be mutable. -/// Obviously not safe to call `recv` from multiple threads. -pub struct UnsafeChannel { +/// Obviously not safe to call `recv` from multiple threads, it's a single consumer channel duh +pub struct TokioChannel { pub tx: mpsc::Sender, - pub rx: SyncUnsafeCell>, + pub rx: LockfreeMutCell>, } pub struct SenderDropped; -impl UnsafeChannel { +impl TokioChannel { pub fn new(size: usize) -> Self { Self::from_tx_rx(mpsc::channel(size)) } @@ -18,7 +19,7 @@ impl UnsafeChannel { fn from_tx_rx((tx, rx): (mpsc::Sender, mpsc::Receiver)) -> Self { Self { tx, - rx: SyncUnsafeCell::new(rx), + rx: LockfreeMutCell::new(rx), } } @@ -30,8 +31,9 @@ impl UnsafeChannel { self.tx.send(msg).await } - pub async fn recv(&self) -> Result { - let chan = unsafe { self.rx.get().as_mut().unwrap() }; + /// Safety: is guaranteed to be safe as long as you don't call it from multiple threads at once. + pub async unsafe fn recv(&self) -> Result { + let chan = self.rx.get_mut(); chan.recv().await.ok_or(SenderDropped) } } diff --git a/server/game/src/util/lockfreemutcell.rs b/server/game/src/util/lockfreemutcell.rs new file mode 100644 index 00000000..7fac9ab6 --- /dev/null +++ b/server/game/src/util/lockfreemutcell.rs @@ -0,0 +1,32 @@ +use std::cell::SyncUnsafeCell; + +/// Very simple wrapper around `SyncUnsafeCell` with nicer API. +#[repr(transparent)] +pub struct LockfreeMutCell { + cell: SyncUnsafeCell, +} + +impl LockfreeMutCell { + pub fn new(data: T) -> Self { + Self { + cell: SyncUnsafeCell::new(data), + } + } + + pub unsafe fn get(&self) -> &T { + &*self.cell.get() + } + + /// We trust you have received the usual lecture from the local System + /// Administrator. It usually boils down to these three things: + /// + /// #1) Respect the privacy of others. + /// + /// #2) Think before you type. + /// + /// #3) With great power comes great responsibility. + #[allow(clippy::mut_from_ref)] + pub unsafe fn get_mut(&self) -> &mut T { + &mut *self.cell.get() + } +} diff --git a/server/game/src/util/mod.rs b/server/game/src/util/mod.rs index 911da44c..f5e7eb19 100644 --- a/server/game/src/util/mod.rs +++ b/server/game/src/util/mod.rs @@ -1,5 +1,7 @@ pub mod channel; +pub mod lockfreemutcell; pub mod rate_limiter; -pub use channel::{SenderDropped, UnsafeChannel}; -pub use rate_limiter::{SimpleRateLimiter, UnsafeRateLimiter}; +pub use channel::{SenderDropped, TokioChannel}; +pub use lockfreemutcell::LockfreeMutCell; +pub use rate_limiter::SimpleRateLimiter; diff --git a/server/game/src/util/rate_limiter.rs b/server/game/src/util/rate_limiter.rs index 734c24bb..113baec2 100644 --- a/server/game/src/util/rate_limiter.rs +++ b/server/game/src/util/rate_limiter.rs @@ -1,7 +1,4 @@ -use std::{ - cell::SyncUnsafeCell, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; /// Naive rate limiter implementation, cannot sleep and is not thread safe on its own. pub struct SimpleRateLimiter { @@ -37,27 +34,9 @@ impl SimpleRateLimiter { } } } -} - -/// A ratelimiter wrapped in a `SyncUnsafeCell`, does not need to be mutable to use. -/// If two different threads attempt to call `try_tick()` on the same instance at the same time, -/// the behavior is undefined. -#[repr(transparent)] -pub struct UnsafeRateLimiter { - limiter: SyncUnsafeCell, -} - -impl UnsafeRateLimiter { - pub fn new(limiter: SimpleRateLimiter) -> Self { - Self { - limiter: SyncUnsafeCell::new(limiter), - } - } - /// Returns `true` if we are not ratelimited, `false` if we are. - /// Calling this from multiple threads at the same time is **undefined behavior**. - pub unsafe fn try_tick(&self) -> bool { - // UnsafeCell can never be nullptr, so the unwrap is safe. - self.limiter.get().as_mut().unwrap_unchecked().try_tick() + /// Returns the amount of time since last refill + pub fn since_last_refill(&self) -> Duration { + self.last_refill.elapsed() } } diff --git a/server/readme.md b/server/readme.md index 55ec0cfd..be10fc67 100644 --- a/server/readme.md +++ b/server/readme.md @@ -1,18 +1,24 @@ # Globed Server -If you want to host a server yourself, the process is as easy as downloading the server binaries from the latest GitHub release, named `globed-central-server` and `globed-game-server`. Depending on your OS and architecture, you want the one ending in `.exe` on Windows, the `-x64` one on Linux x64, and the `-arm64` one on Linux ARM64. +If you want to host a server yourself, first you have to download the server binaries from the latest GitHub release, named `globed-central-server` and `globed-game-server`. Depending on your OS and architecture, you want the one ending in `.exe` on Windows, the `-x64` one on Linux x64, and the `-arm64` one on Linux ARM64. -If you want to build the server yourself, you need a nightly Rust toolchain. After that, it's as simple as: -```sh -cd server/ -rustup override set nightly # has to be done only once -cargo build --release -``` +After that is done, you have 2 paths: + +* If you want to setup a small, simple server you can jump to the [standalone section](#standalone) +* If you want to setup a bigger or more configurable server, keep reading. + +First, you want to launch the central server binary. It should create a file called `central-conf.json` in the folder you ran it from. This is where you can configure everything about the server. For documentation about all the options, jump to the [configuration section](#central-server-configuration), however for now we only need the option `game_server_password`. + +With your central server properly setup and started, jump to the [bridged](#bridged) section of the game server configuration and launch the game server, with the password that you configured earlier. + +If you did everything right, you should see no errors or warnings in the console and instead you should see "Server launched on x.x.x.x". This means everything worked! Congrats :) ## Game server configuration note: if you are not on Windows, in the following examples replace `set` with `export` and replace `globed-game-server.exe` with the appropriate server binary (such as `globed-game-server-x64`) +### Standalone + If you want to spin up a quick, standalone game server, without needing to start a central server, then it is as simple as running the `globed-game-server.exe` executable directly. If you want to change the address then you'll have to run the executable with an additional argument like so: @@ -21,10 +27,14 @@ If you want to change the address then you'll have to run the executable with an globed-game-server.exe 0.0.0.0:41001 ``` -Keep in mind that this makes the configuration very limited (for example you can't blacklist/whitelist users anymore) and disables any kind of player authentication. +To connect to your server, you want to use the Direct Connection option inside the server switcher in-game. + +Keep in mind that a standalone server makes the configuration very limited (for example you can't blacklist/whitelist users anymore) and disables any kind of player authentication. + + +### Bridged -To start the game server and bridge it together with an active central server -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: +To start the game server and bridge it together with an active central server 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.exe 0.0.0.0:41001 http://127.0.0.1:41000 password @@ -49,15 +59,16 @@ By default, the file is created with the name `central-conf.json` in the current Note that the server is written with security in mind, so many of those options may not be exactly interesting for you. If you are hosting a small server for your friends then the defaults should be good enough, however if you are hosting a big public server, it is recommended that you adjust the settings accordingly. -| JSON ID | Default | Hot-reloadable | Description | +| JSON key | Default | Hot-reloadable | Description | |---------|---------|----------------|-------------| | `web_mountpoint` | `"/"` | ❌ | HTTP mountpoint (the prefix before every endpoint) | | `web_address` | `"0.0.0.0:41000"` | ❌ | HTTP address | -| `special_users` | `{}` | ✅** | Each entry has the account ID as the key and an object with properties `name` and `color` as the value. The `color` property is used for changing the color of the name for this user | -| `game_servers` | `[]` | ✅ | Each object has 4 keys: `id` (must be a unique string), `name`, `address` (in format `ip:port`), `region` | +| `special_users` | `{}` | ✅** | List of users that have special properties, for example a unique name color (see below for the format) | +| `game_servers` | `[]` | ✅ | List of game servers that will be sent to the clients (see below for the format) | | `userlist_mode` | `"none"` | ✅ | Can be `blacklist`, `whitelist`, `none`. See `userlist` property for more information | | `userlist` | `[]` | ✅ | If `userlist_mode` is set to `blacklist`, block account IDs in this list. If set to `whitelist`, only the users in the list will be allowed to connect | | `no_chat_list` | `[]` | ✅** | List of account IDs of users who are able to connect and play, but have cannot send text/voice messages | +| `tps` | `30` | ✅** | Dictates how many packets per second clients can (and will) send when in a level. Higher = smoother experience but more processing power and bandwidth. | | `use_gd_api`* | `false` | ✅ | Use robtop's API to verify account ownership. Note that you must set `challenge_level` accordingly if you enable this setting | | `gd_api`* | `(...)` | ✅ | Link to robtop's API that will be used if `use_gd_api` is enabled. This setting is useful for GDPS owners | | `gd_api_ratelimit`* | `5` | ❌ | If `use_gd_api` is enabled, sets the maximum request number per `gd_api_period` that can be made to robtop's API. Used to avoid ratelimits | @@ -72,7 +83,38 @@ Note that the server is written with security in mind, so many of those options * - security setting, be careful with changing it if making a public server -** - it may take a few minutes for you to see any changes +** - this setting is synced to game servers, so it may take a few minutes for you to see any changes. additionally, you may need to reconnect if you were already connected to a server. + +Formatting for special users: + +```json +{ + "123123": { + "name": "myname", + "color": "#ff0000", + } +} +``` + +Formatting for game servers: + +```json +{ + "id": "my-server-id", + "name": "Server name", + "address": "127.0.0.1:41001", + "region": "my home i guess?" +} +``` + +## Building + +If you want to build the server yourself, you need a nightly Rust toolchain. After that, it's as simple as: +```sh +cd server/ +rustup override set nightly # has to be done only once +cargo build --release +``` ## Extra diff --git a/src/crypto/box.cpp b/src/crypto/box.cpp index 903c0511..4e3c20cb 100644 --- a/src/crypto/box.cpp +++ b/src/crypto/box.cpp @@ -7,8 +7,6 @@ using namespace util::data; CryptoBox::CryptoBox(byte* key) { - CRYPTO_SODIUM_INIT - memBasePtr = reinterpret_cast(sodium_malloc( KEY_LEN * 2 + // publicKey, peerPublicKey SECRET_KEY_LEN + // secretKey diff --git a/src/crypto/secret_box.cpp b/src/crypto/secret_box.cpp index e365177a..bd164c34 100644 --- a/src/crypto/secret_box.cpp +++ b/src/crypto/secret_box.cpp @@ -6,8 +6,6 @@ using namespace util::data; SecretBox::SecretBox(bytevector key) { - CRYPTO_SODIUM_INIT; - CRYPTO_REQUIRE(key.size() == crypto_secretbox_KEYBYTES, "provided key is too long or too short for SecretBox"); this->key = reinterpret_cast(sodium_malloc( diff --git a/src/main.cpp b/src/main.cpp index c6e035d6..6d4c9fe4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,7 +27,7 @@ using namespace geode::prelude; -void setupSodiumMisuse(); +void setupLibsodium(); void setupErrorCheckNode(); void setupCustomKeybinds(); void printDebugInfo(); @@ -45,7 +45,7 @@ void printDebugInfo(); // } $on_mod(Loaded) { - setupSodiumMisuse(); + setupLibsodium(); setupErrorCheckNode(); setupCustomKeybinds(); @@ -62,7 +62,7 @@ void printDebugInfo(); class $modify(MyMenuLayer, MenuLayer) { void onMoreGames(CCObject*) { - if (NetworkManager::get().established()) { + if (NetworkManager::get().handshaken()) { util::debugging::PacketLogger::get().getSummary().print(); } @@ -74,8 +74,11 @@ class $modify(MyMenuLayer, MenuLayer) { } }; -// if there is a logic error in the crypto code, this lambda will be called -void setupSodiumMisuse() { +void setupLibsodium() { + // sodium_init returns 0 on success, 1 if already initialized, -1 on fail + GLOBED_REQUIRE(sodium_init() != -1, "sodium_init failed") + + // if there is a logic error in the crypto code, this lambda will be called sodium_set_misuse_handler([](){ log::error("sodium_misuse called. we are officially screwed."); util::debugging::suicide(); diff --git a/src/managers/server_manager.cpp b/src/managers/server_manager.cpp index beaf7ca3..60f4a49b 100644 --- a/src/managers/server_manager.cpp +++ b/src/managers/server_manager.cpp @@ -137,6 +137,10 @@ std::unordered_map GlobedServerManager::extractGame auto data = _data.lock(); for (const auto& [serverId, gsi] : data->servers) { + if (serverId == STANDALONE_SERVER_ID) { + continue; + } + out[serverId] = GameServerView { .id = serverId, .name = gsi.name, diff --git a/src/managers/server_manager.hpp b/src/managers/server_manager.hpp index 15d8507f..46a7528b 100644 --- a/src/managers/server_manager.hpp +++ b/src/managers/server_manager.hpp @@ -38,9 +38,12 @@ struct GameServerView { // This class is fully thread safe to use. class GlobedServerManager { +public: GLOBED_SINGLETON(GlobedServerManager); GlobedServerManager(); + static constexpr const char* STANDALONE_SERVER_ID = "__standalone_gs__"; + /* central server control */ void setCentral(std::string address); diff --git a/src/managers/settings.cpp b/src/managers/settings.cpp index 15f53106..b33cf4ab 100644 --- a/src/managers/settings.cpp +++ b/src/managers/settings.cpp @@ -10,15 +10,10 @@ void GlobedSettings::reset(const std::string& key) { this->refreshCache(); } -CachedSettings GlobedSettings::getCached() { +GlobedSettings::CachedSettings GlobedSettings::getCached() { return *_cache.lock(); } -void GlobedSettings::refreshCache() { - auto cache = _cache.lock(); - cache->serverTps = this->get("server-tps"); -} - bool GlobedSettings::getFlag(const std::string& key) { return geode::Mod::get()->getSavedValue("gflag-" + key) == 2; } diff --git a/src/managers/settings.hpp b/src/managers/settings.hpp index e4567d01..ae62627a 100644 --- a/src/managers/settings.hpp +++ b/src/managers/settings.hpp @@ -2,10 +2,6 @@ #include #include -struct CachedSettings { - uint32_t serverTps; -}; - #define MAKE_DEFAULT(_key, value) if (key == (_key)) return (value); // Besides `getCached()`, this class is not thread safe (reason: Mod::getSavedValue/Mod::setSavedValue) @@ -13,6 +9,8 @@ class GlobedSettings { GLOBED_SINGLETON(GlobedSettings); GlobedSettings(); + struct CachedSettings; + /* * ok i know this is a mess but here's a rundown of how the settings work: * say we have a setting that is an int called 'hello'. @@ -35,28 +33,49 @@ class GlobedSettings { } } + // reset a setting to its default value + void reset(const std::string& key); + + // cached settings for performance & thread safety + CachedSettings getCached(); + + bool getFlag(const std::string& key); + void setFlag(const std::string& key, bool state = true); + + /* for ease of modification, all settings and their defaults are defined in the following struct and 3 functions. */ + + struct CachedSettings { + uint32_t tpsCap; + }; + // directly get the setting as json template inline T get(const std::string& key) { if (this->getFlag("_gset_-" + key)) { return geode::Mod::get()->getSavedValue("gsetting-" + key); } else { - MAKE_DEFAULT("server-tps", (uint32_t)30) + MAKE_DEFAULT("tps-cap", (uint32_t)0) return T{}; } } - // reset a setting to its default value - void reset(const std::string& key); - - // get cached settings for performance - CachedSettings getCached(); + // reset all settings to their default values + inline void resetAll() { + static const char* settings[] = { + "tps-cap", + }; + for (auto* setting : settings) { + this->reset(setting); + } - void refreshCache(); + this->refreshCache(); + } - bool getFlag(const std::string& key); - void setFlag(const std::string& key, bool state = true); + inline void refreshCache() { + auto cache = _cache.lock(); + cache->tpsCap = this->get("tps-cap"); + } private: util::sync::WrappingMutex _cache; diff --git a/src/net/network_manager.cpp b/src/net/network_manager.cpp index 517c4ac7..ba8dd60e 100644 --- a/src/net/network_manager.cpp +++ b/src/net/network_manager.cpp @@ -20,10 +20,15 @@ NetworkManager::NetworkManager() { addBuiltinListener([this](auto packet) { gameSocket.box->setPeerKey(packet->data.key.data()); - _established = true; + _handshaken = true; // and lets try to login! auto& am = GlobedAccountManager::get(); - auto authtoken = *am.authToken.lock(); + std::string authtoken; + + if (!_connectingStandalone) { + authtoken = *am.authToken.lock(); + } + auto gddata = am.gdData.lock(); this->send(LoginPacket::create(gddata->accountId, gddata->accountName, authtoken)); }); @@ -87,14 +92,18 @@ NetworkManager::~NetworkManager() { log::debug("Goodbye!"); } -void NetworkManager::connect(const std::string& addr, unsigned short port) { +void NetworkManager::connect(const std::string& addr, unsigned short port, bool standalone) { if (this->connected()) { this->disconnect(false); } + _connectingStandalone = standalone; + lastReceivedPacket = util::time::now(); - GLOBED_REQUIRE(!GlobedAccountManager::get().authToken.lock()->empty(), "attempting to connect with no authtoken set in account manager") + if (!standalone) { + GLOBED_REQUIRE(!GlobedAccountManager::get().authToken.lock()->empty(), "attempting to connect with no authtoken set in account manager") + } GLOBED_REQUIRE(gameSocket.connect(addr, port), "failed to connect to the server") gameSocket.createBox(); @@ -113,6 +122,16 @@ void NetworkManager::connectWithView(const GameServerView& gsview) { } } +void NetworkManager::connectStandalone(const std::string& addr, unsigned short port) { + try { + this->connect(addr, port); + GlobedServerManager::get().setActiveGameServer(GlobedServerManager::STANDALONE_SERVER_ID); + } catch (const std::exception& e) { + this->disconnect(true); + ErrorQueues::get().error(std::string("Connection failed: ") + e.what()); + } +} + void NetworkManager::disconnect(bool quiet) { if (!this->connected()) { return; @@ -123,8 +142,9 @@ void NetworkManager::disconnect(bool quiet) { gameSocket.sendPacket(DisconnectPacket::create()); } - _established = false; + _handshaken = false; _loggedin = false; + _connectingStandalone = false; gameSocket.disconnect(); gameSocket.cleanupBox(); @@ -279,10 +299,14 @@ bool NetworkManager::connected() { return gameSocket.connected; } +bool NetworkManager::handshaken() { + return _handshaken; +} + bool NetworkManager::established() { - return this->connected() && _established; + return _loggedin; } -bool NetworkManager::authenticated() { - return this->established() && _loggedin; +bool NetworkManager::standalone() { + return _connectingStandalone; } \ No newline at end of file diff --git a/src/net/network_manager.hpp b/src/net/network_manager.hpp index 47fbb195..abe0b390 100644 --- a/src/net/network_manager.hpp +++ b/src/net/network_manager.hpp @@ -42,9 +42,11 @@ class NetworkManager { ~NetworkManager(); // Connect to a server - void connect(const std::string& addr, unsigned short port); + void connect(const std::string& addr, unsigned short port, bool standalone = false); // Safer version of `connect`, sets the active game server in `GlobedServerManager` doesn't throw an exception on error void connectWithView(const GameServerView& gsview); + // Is similar to `connectWithView` (does not throw exceptions) and is made specifically for standalone servers. + void connectStandalone(const std::string& addr, unsigned short port); // Disconnect from a server. Does nothing if not connected void disconnect(bool quiet = false); @@ -83,14 +85,16 @@ class NetworkManager { // Returns true if ANY connection has been made with a server. The handshake might not have been done at this point. bool connected(); - // Returns true ONLY if we are connected to a server and the crypto handshake has finished. + // Returns true ONLY if we are connected to a server and the crypto handshake has finished. We might not have logged in yet. + bool handshaken(); + + // Returns true if we have fully authenticated and are ready to rock. bool established(); - // Returns true if we have already proved we are the account owner and are ready to rock. - bool authenticated(); + // Returns true if we are connected to a standalone game server, not tied to any central server. + bool standalone(); private: - static constexpr chrono::seconds KEEPALIVE_INTERVAL = chrono::seconds(5); static constexpr chrono::seconds DISCONNECT_AFTER = chrono::seconds(15); @@ -112,8 +116,9 @@ class NetworkManager { // misc AtomicBool _running = true; - AtomicBool _established = false; + AtomicBool _handshaken = false; AtomicBool _loggedin = false; + AtomicBool _connectingStandalone = false; util::time::time_point lastKeepalive; util::time::time_point lastReceivedPacket; diff --git a/src/ui/hooks/menu_layer.hpp b/src/ui/hooks/menu_layer.hpp index 22ac92b2..bcd0c8f5 100644 --- a/src/ui/hooks/menu_layer.hpp +++ b/src/ui/hooks/menu_layer.hpp @@ -51,7 +51,7 @@ class $modify(HookedMenuLayer, MenuLayer) { } void maybeUpdateButton(float _) { - bool authenticated = NetworkManager::get().authenticated(); + bool authenticated = NetworkManager::get().established(); if (authenticated != m_fields->btnActive) { m_fields->btnActive = authenticated; this->updateGlobedButton(); diff --git a/src/ui/hooks/play_layer.hpp b/src/ui/hooks/play_layer.hpp index fe85075f..d332348a 100644 --- a/src/ui/hooks/play_layer.hpp +++ b/src/ui/hooks/play_layer.hpp @@ -22,8 +22,8 @@ using namespace geode::prelude; class $modify(GlobedPlayLayer, PlayLayer) { bool globedReady; bool deafened = false; - CachedSettings settings; - uint32_t configuredTps; + GlobedSettings::CachedSettings settings; + uint32_t configuredTps = 0; /* speedhack detection */ float lastKnownTimeScale = 1.0f; @@ -39,10 +39,16 @@ class $modify(GlobedPlayLayer, PlayLayer) { auto& nm = NetworkManager::get(); // if not authenticated, do nothing - m_fields->globedReady = nm.authenticated(); + m_fields->globedReady = nm.established(); if (!m_fields->globedReady) return true; - m_fields->configuredTps = std::min(nm.connectedTps.load(), m_fields->settings.serverTps); + auto tpsCap = m_fields->settings.tpsCap; + + if (tpsCap != 0) { + m_fields->configuredTps = std::min(nm.connectedTps.load(), tpsCap); + } else { + m_fields->configuredTps = nm.connectedTps; + } // send LevelJoinPacket nm.send(LevelJoinPacket::create(m_level->m_levelID)); @@ -128,15 +134,15 @@ class $modify(GlobedPlayLayer, PlayLayer) { auto& vm = GlobedAudioManager::get(); if (event->isDown()) { - if (!vm.isRecording()) { + if (!this->m_fields->deafened && !vm.isRecording()) { // make sure the recording device is valid vm.validateDevices(); if (!vm.isRecordingDeviceSet()) { ErrorQueues::get().warn("Unable to record audio, no recording device is set"); - return ListenerResult::Propagate; + return ListenerResult::Stop; } - vm.startRecording([this](const auto& frame) { + vm.startRecording([](const auto& frame) { // (!) remember that the callback is ran from another thread (!) // auto& nm = NetworkManager::get(); @@ -152,10 +158,12 @@ class $modify(GlobedPlayLayer, PlayLayer) { }); } } else { - vm.stopRecording(); + if (vm.isRecording()) { + vm.stopRecording(); + } } - return ListenerResult::Propagate; + return ListenerResult::Stop; }, "voice-activate"_spr); this->addEventListener([this](keybinds::InvokeBindEvent* event) { diff --git a/src/ui/menu/main/globed_menu_layer.cpp b/src/ui/menu/main/globed_menu_layer.cpp index 6779b593..e0fbe202 100644 --- a/src/ui/menu/main/globed_menu_layer.cpp +++ b/src/ui/menu/main/globed_menu_layer.cpp @@ -23,11 +23,13 @@ bool GlobedMenuLayer::init() { am.initialize(gjam->m_username, gjam->m_accountID, gjam->getGJP(), sm.getCentral()); + auto winsize = CCDirector::get()->getWinSize(); + + // server list + auto listview = Build::create(createServerList(), ServerListCell::CELL_HEIGHT, LIST_WIDTH, LIST_HEIGHT) .collect(); - auto winsize = CCDirector::get()->getWinSize(); - Build::create(listview, "Servers", ccc4(0, 0, 0, 180), LIST_WIDTH, 220.f) .zOrder(2) .anchorPoint(0.f, 0.f) @@ -37,12 +39,26 @@ bool GlobedMenuLayer::init() { listLayer->setPosition({winsize / 2 - listLayer->getScaledContentSize() / 2}); + // standalone dummy list + + auto saListview = Build::create(createStandaloneList(), ServerListCell::CELL_HEIGHT, LIST_WIDTH, LIST_HEIGHT) + .collect(); + + Build::create(saListview, "Servers", ccc4(0, 0, 0, 180), LIST_WIDTH, 220.f) + .zOrder(2) + .anchorPoint(0.f, 0.f) + .parent(this) + .id("standalone-list"_spr) + .store(standaloneLayer); + + standaloneLayer->setPosition({winsize / 2 - standaloneLayer->getScaledContentSize() / 2}); + Build::create() .zOrder(2) .anchorPoint(0.f, 0.f) .pos(listLayer->getPosition()) .parent(this) - .id("signup-layer") + .id("signup-layer"_spr) .store(signupLayer); // refresh servers btn @@ -58,7 +74,7 @@ bool GlobedMenuLayer::init() { } }) .intoNewParent(CCMenu::create()) - .id("btn-refresh-servers") + .id("btn-refresh-servers"_spr) .parent(this); // TODO prod remove wipe authtoken button @@ -73,6 +89,9 @@ bool GlobedMenuLayer::init() { // .id("btn-clear-authtoken") // .parent(this); + // TODO: menu for connecting to a standalone server directly with an IP and port + // it must call the proper func in GlobedServerManager::addGameServer then try to NM::connectStandalone + util::ui::addBackground(this); auto menu = CCMenu::create(); @@ -102,7 +121,7 @@ CCArray* GlobedMenuLayer::createServerList() { auto& nm = NetworkManager::get(); auto& sm = GlobedServerManager::get(); - bool authenticated = nm.authenticated(); + bool authenticated = nm.established(); auto activeServer = sm.getActiveGameServer(); @@ -115,18 +134,56 @@ CCArray* GlobedMenuLayer::createServerList() { return ret; } +CCArray* GlobedMenuLayer::createStandaloneList() { + auto ret = CCArray::create(); + + GameServerView view = {}; + + auto cell = ServerListCell::create(view, false); + ret->addObject(cell); + + return ret; +} + void GlobedMenuLayer::refreshServerList(float _) { - // if we do not have a session token from the central server, don't show game servers auto& am = GlobedAccountManager::get(); - if (!am.hasAuthKey()) { + auto& nm = NetworkManager::get(); + + bool standalone = nm.established() && nm.standalone(); + + // if we do not have a session token from the central server, and are not in a standalone server, don't show game servers + if (!am.hasAuthKey() && !standalone) { listLayer->setVisible(false); + standaloneLayer->setVisible(false); signupLayer->setVisible(true); return; } - listLayer->setVisible(true); signupLayer->setVisible(false); + if (standalone) { + listLayer->setVisible(false); + standaloneLayer->setVisible(true); + + // update the standalone cell + auto listCells = standaloneLayer->m_listView->m_tableView->m_contentLayer->getChildren(); + if (listCells == nullptr) { + return; + } + + auto& sm = GlobedServerManager::get(); + + auto* ccnodew = static_cast(listCells->objectAtIndex(0)); + auto* slc = static_cast(ccnodew->getChildren()->objectAtIndex(2)); + + auto server = sm.getGameServer(GlobedServerManager::STANDALONE_SERVER_ID); + slc->updateWith(server, true); + return; + } + + listLayer->setVisible(true); + standaloneLayer->setVisible(false); + // if there are pending changes, hard refresh the list and ping all servers auto& sm = GlobedServerManager::get(); if (sm.pendingChanges) { @@ -153,7 +210,7 @@ void GlobedMenuLayer::refreshServerList(float _) { auto active = sm.getActiveGameServer(); - bool authenticated = NetworkManager::get().authenticated(); + bool authenticated = NetworkManager::get().established(); for (auto* obj : CCArrayExt(listCells)) { auto slc = static_cast(obj->getChildren()->objectAtIndex(2)); diff --git a/src/ui/menu/main/globed_menu_layer.hpp b/src/ui/menu/main/globed_menu_layer.hpp index f8b5d0ad..899adc11 100644 --- a/src/ui/menu/main/globed_menu_layer.hpp +++ b/src/ui/menu/main/globed_menu_layer.hpp @@ -11,11 +11,12 @@ class GlobedMenuLayer : public cocos2d::CCLayer { static cocos2d::CCScene* scene(); private: - GJListLayer* listLayer; + GJListLayer *listLayer, *standaloneLayer; GlobedSignupLayer* signupLayer; bool init(); cocos2d::CCArray* createServerList(); + cocos2d::CCArray* createStandaloneList(); void refreshServerList(float _); void requestServerList(); void keyBackClicked(); diff --git a/src/ui/menu/player_list/player_list_popup.cpp b/src/ui/menu/player_list/player_list_popup.cpp index 9be28164..80e9dd54 100644 --- a/src/ui/menu/player_list/player_list_popup.cpp +++ b/src/ui/menu/player_list/player_list_popup.cpp @@ -10,7 +10,7 @@ using namespace geode::prelude; bool PlayerListPopup::setup() { auto& nm = NetworkManager::get(); - if (!nm.established()) { + if (!nm.handshaken()) { return false; } diff --git a/src/util/crypto.cpp b/src/util/crypto.cpp index 8cf2daef..0339edfa 100644 --- a/src/util/crypto.cpp +++ b/src/util/crypto.cpp @@ -16,10 +16,9 @@ bytevector secureRandom(size_t size) { } void secureRandom(byte* dest, size_t size) { - CRYPTO_SODIUM_INIT; randombytes_buf(dest, size); } - + bytevector pwHash(const std::string& input) { return pwHash(reinterpret_cast(input.c_str()), input.size()); } @@ -29,8 +28,6 @@ bytevector pwHash(const bytevector& input) { } bytevector pwHash(const byte* input, size_t len) { - CRYPTO_SODIUM_INIT; - bytevector out(crypto_pwhash_SALTBYTES + crypto_secretbox_KEYBYTES); byte* salt = out.data(); byte* key = out.data() + crypto_pwhash_SALTBYTES; @@ -80,8 +77,6 @@ std::string simpleTOTP(const bytevector& key) { } std::string simpleTOTPForPeriod(const byte *key, size_t keySize, uint64_t period) { - CRYPTO_SODIUM_INIT; - CRYPTO_REQUIRE(keySize == crypto_auth_hmacsha256_KEYBYTES, "invalid key size passed to simpleTOTPForPeriod"); if constexpr (GLOBED_LITTLE_ENDIAN) { @@ -95,7 +90,7 @@ std::string simpleTOTPForPeriod(const byte *key, size_t keySize, uint64_t period size_t offset = hmacResult[crypto_auth_hmacsha256_BYTES - 1] & 0x0F; // Calculate the 4-byte dynamic binary code (what the fuck is this?) - uint32_t binaryCode = + uint32_t binaryCode = (hmacResult[offset] & 0x7f) << 24 | (hmacResult[offset + 1]) << 16 | (hmacResult[offset + 2]) << 8 | @@ -221,7 +216,7 @@ std::string hexEncode(const std::string& source) { bytevector hexDecode(const byte* source, size_t size) { size_t outLen = size / 2; bytevector out(outLen); - + size_t realOutLen; CRYPTO_ERR_CHECK(sodium_hex2bin( diff --git a/src/util/crypto.hpp b/src/util/crypto.hpp index 5b061b21..8798dc25 100644 --- a/src/util/crypto.hpp +++ b/src/util/crypto.hpp @@ -7,9 +7,6 @@ #define CRYPTO_REQUIRE(condition, message) GLOBED_REQUIRE(condition, "crypto error: " message) #define CRYPTO_ERR_CHECK(result, message) CRYPTO_REQUIRE(result == 0, message) -// sodium_init returns 0 on success, 1 if already initialized, -1 on fail -#define CRYPTO_SODIUM_INIT CRYPTO_REQUIRE(sodium_init() != -1, "sodium_init failed") - namespace util::crypto { // generate `size` bytes of cryptographically secure random data data::bytevector secureRandom(size_t size); @@ -29,7 +26,7 @@ namespace util::crypto { data::bytevector simpleHash(const data::bytevector& input); // generate a simple, consistent hash from this buffer data::bytevector simpleHash(const data::byte* input, size_t size); - + // generate a 6-digit TOTP code given the key (rfc 6238 compliant i think) std::string simpleTOTP(const data::byte* key, size_t keySize); // generate a 6-digit TOTP code given the key (rfc 6238 compliant i think) @@ -60,13 +57,13 @@ namespace util::crypto { // encodes the given string into a base64 string std::string base64Encode(const std::string& source, Base64Variant variant = Base64Variant::STANDARD); - // decodes the given base64 buffer into a bytevector + // decodes the given base64 buffer into a bytevector data::bytevector base64Decode(const data::byte* source, size_t size, Base64Variant variant = Base64Variant::STANDARD); // decodes the given base64 string into a bytevector data::bytevector base64Decode(const std::string& source, Base64Variant variant = Base64Variant::STANDARD); - // decodes the given base64 bytevector into a bytevector + // decodes the given base64 bytevector into a bytevector data::bytevector base64Decode(const data::bytevector& source, Base64Variant variant = Base64Variant::STANDARD); - + // encodes the given buffer into a hex string std::string hexEncode(const data::byte* source, size_t size); // encodes the given bytevector into a hex string diff --git a/src/util/formatting.cpp b/src/util/formatting.cpp index ccf1bfea..34f787e0 100644 --- a/src/util/formatting.cpp +++ b/src/util/formatting.cpp @@ -3,20 +3,6 @@ #include namespace util::formatting { - std::string formatDuration(chrono::microseconds time) { - auto seconds = chrono::duration_cast(time).count(); - auto millis = chrono::duration_cast(time).count(); - auto micros = time.count(); - - if (seconds > 0) { - return std::to_string(seconds) + "." + std::to_string(millis % 1000) + "s"; - } else if (millis > 0) { - return std::to_string(millis) + "." + std::to_string(micros % 1000) + "ms"; - } else { - return std::to_string(micros) + "Ξs"; - } - } - std::string formatDateTime(chrono::system_clock::time_point tp) { auto timet = chrono::system_clock::to_time_t(tp); auto nowms = chrono::duration_cast(tp.time_since_epoch()) % 1000; diff --git a/src/util/formatting.hpp b/src/util/formatting.hpp index 5cf595b8..99bfbe1c 100644 --- a/src/util/formatting.hpp +++ b/src/util/formatting.hpp @@ -1,14 +1,26 @@ #pragma once #include +#include namespace util::formatting { - namespace chrono = std::chrono; - // example: 2.123s, 69.123ms - std::string formatDuration(chrono::microseconds time); + template + std::string formatDuration(time::duration time) { + auto seconds = time::asSecs(time); + auto millis = time::asMillis(time); + auto micros = time::asMicros(time); + + if (seconds > 0) { + return std::to_string(seconds) + "." + std::to_string(millis % 1000) + "s"; + } else if (millis > 0) { + return std::to_string(millis) + "." + std::to_string(micros % 1000) + "ms"; + } else { + return std::to_string(micros) + "Ξs"; + } + } // example: 2023-11-16 19:43:50.200 - std::string formatDateTime(chrono::system_clock::time_point tp); + std::string formatDateTime(time::time_point tp); // example: 123.4KiB std::string formatBytes(uint64_t bytes); diff --git a/src/util/time.hpp b/src/util/time.hpp index a64de4c1..a549dd97 100644 --- a/src/util/time.hpp +++ b/src/util/time.hpp @@ -18,6 +18,11 @@ namespace util::time { return clock::now(); } + template + inline long long asSecs(duration tp) { + return chrono::duration_cast(tp).count(); + } + template inline long long asMillis(duration tp) { return chrono::duration_cast(tp).count();