From 2390205a10e60d9a27ea0e7ce1388e5e5d24e66d Mon Sep 17 00:00:00 2001 From: dank_meme01 Date: Wed, 22 Nov 2023 18:13:22 +0100 Subject: [PATCH] some changes --- README.md | 9 +- server/central/src/config.rs | 2 +- server/central/src/ip_blocker.rs | 6 +- server/central/src/logger.rs | 8 +- server/central/src/main.rs | 11 +-- server/central/src/state.rs | 4 +- server/central/src/web/routes/auth.rs | 15 ++- server/central/src/web/routes/game_server.rs | 2 +- server/game/src/bytebufferext.rs | 12 +-- server/game/src/data/packets/client/game.rs | 14 +-- server/game/src/data/packets/server/game.rs | 12 +++ server/game/src/data/types/gd.rs | 72 +++++++-------- server/game/src/logger.rs | 8 +- server/game/src/main.rs | 97 +++++++++++--------- server/game/src/server.rs | 70 ++++++++------ server/game/src/server_thread.rs | 91 +++++++++++------- server/protocol.md | 45 +++++++++ server/readme.md | 33 +++++-- src/data/packets/all.cpp | 2 + src/data/packets/all.hpp | 2 +- src/data/packets/client/game.hpp | 12 +++ src/data/packets/server/game.hpp | 15 +++ src/data/types/gd.hpp | 52 +++++------ 23 files changed, 374 insertions(+), 220 deletions(-) create mode 100644 server/protocol.md diff --git a/README.md b/README.md index cdc7e75d..2fe3cb62 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,14 @@ Known issues: ## Hosting a server -todo! +See the [server readme](./server/readme.md) for more information about the server and how you can host it. ## Building -Building should be the same as any other mod! - -todo macaroni os +```sh +cmake -DCMAKE_BUILD_TYPE=Release -B build +cmake --build build --config Release +``` ## Credit diff --git a/server/central/src/config.rs b/server/central/src/config.rs index 13b627d9..893ef5b6 100644 --- a/server/central/src/config.rs +++ b/server/central/src/config.rs @@ -58,7 +58,7 @@ fn default_secret_key() -> String { .map(char::from) .collect(); - format!("Insecure-{}", rand_string) + format!("Insecure-{rand_string}") } fn default_challenge_expiry() -> u32 { diff --git a/server/central/src/ip_blocker.rs b/server/central/src/ip_blocker.rs index e7283acb..7d5c5f93 100644 --- a/server/central/src/ip_blocker.rs +++ b/server/central/src/ip_blocker.rs @@ -10,7 +10,7 @@ pub struct IpBlocker { } impl IpBlocker { - pub fn new(v4: Vec, v6: Vec) -> Self { + pub fn new(v4: &[String], v6: &[String]) -> Self { let range_v4 = v4.iter().map(|s| s.parse().unwrap()).collect(); let range_v6 = v6.iter().map(|s| s.parse().unwrap()).collect(); @@ -44,10 +44,10 @@ lazy_static! { } else if proto == "v6" { v6.push(range.to_string()); } else { - eprintln!("ignoring invalid IP address entry: {}", line); + eprintln!("ignoring invalid IP address entry: {line}"); } } - IpBlocker::new(v4, v6) + IpBlocker::new(&v4, &v6) }; } diff --git a/server/central/src/logger.rs b/server/central/src/logger.rs index 6ec88944..ed08e2ac 100644 --- a/server/central/src/logger.rs +++ b/server/central/src/logger.rs @@ -10,10 +10,10 @@ const TIME_FORMAT: &str = "[year]-[month]-[day] [hour]:[minute]:[second].[subsec impl log::Log for Logger { fn enabled(&self, metadata: &log::Metadata) -> bool { - if !metadata.target().starts_with("globed_central_server") { - metadata.level() <= Level::Warn - } else { + if metadata.target().starts_with("globed_central_server") { true + } else { + metadata.level() <= Level::Warn } } @@ -37,7 +37,7 @@ impl log::Log for Logger { Level::Trace => (record.level().to_string().black(), record.args().to_string().black()), }; - println!("[{}] [{}] - {}", formatted_time, level, args,) + println!("[{formatted_time}] [{level}] - {args}"); } } diff --git a/server/central/src/main.rs b/server/central/src/main.rs index 1a320a06..3e147c38 100644 --- a/server/central/src/main.rs +++ b/server/central/src/main.rs @@ -32,9 +32,8 @@ async fn main() -> Result<(), Box> { // config file - let mut config_path = std::env::var("GLOBED_CONFIG_PATH") - .map(PathBuf::from) - .unwrap_or_else(|_| std::env::current_dir().unwrap()); + let mut config_path = + std::env::var("GLOBED_CONFIG_PATH").map_or_else(|_| std::env::current_dir().unwrap(), PathBuf::from); if config_path.is_dir() { config_path = config_path.join("central-conf.json"); @@ -80,7 +79,7 @@ async fn main() -> Result<(), Box> { let mut state = watcher_state.state_write().await; let cpath = state.config_path.clone(); match state.config.reload_in_place(&cpath) { - Ok(_) => { + Ok(()) => { info!("Successfully reloaded the configuration"); } Err(err) => { @@ -93,8 +92,8 @@ async fn main() -> Result<(), Box> { // roa your favorite web app let router = web::routes::build_router(); - let routes = router.routes(Box::leak(Box::new(mnt_point)))?; - let app = App::state(state).end(routes); + let route_list = router.routes(Box::leak(Box::new(mnt_point)))?; + let app = App::state(state).end(route_list); app.listen(mnt_addr, |addr| { info!("Globed central server launched on {addr}"); diff --git a/server/central/src/state.rs b/server/central/src/state.rs index ba28420d..2e7a9a60 100644 --- a/server/central/src/state.rs +++ b/server/central/src/state.rs @@ -61,7 +61,7 @@ impl ServerStateData { // uses hmac-sha256 to derive an auth key from user's account ID and name pub fn generate_authkey(&self, account_id: i32, account_name: &str) -> Vec { - let val = format!("{}:{}", account_id, account_name); + let val = format!("{account_id}:{account_name}"); let mut hmac: Hmac = self.hmac.clone(); hmac.update(val.as_bytes()); @@ -92,7 +92,7 @@ impl ServerStateData { .expect("whoops our clock went backwards") .as_secs(); - let data = format!("{}.{}.{}", account_id, account_name, timestamp); + let data = format!("{account_id}.{account_name}.{timestamp}"); let mut hmac = self.hmac.clone(); hmac.update(data.as_bytes()); let res = hmac.finalize(); diff --git a/server/central/src/web/routes/auth.rs b/server/central/src/web/routes/auth.rs index a8e8f2ab..3d3c2f52 100644 --- a/server/central/src/web/routes/auth.rs +++ b/server/central/src/web/routes/auth.rs @@ -59,18 +59,19 @@ macro_rules! get_user_ip { pub async fn totp_login(context: &mut Context) -> roa::Result { check_user_agent!(context, _ua); + let state = context.state_read().await; + get_user_ip!(state, context, _ip); + let account_id = context.must_query("aid")?.parse::()?; let account_name = &*context.must_query("aname")?; let code = &*context.must_query("code")?; // if account_name.to_lowercase().contains("sevenworks") - // && rand::thread_rng().gen_ratio(1, 25) { - + // || account_name.to_lowercase() == "7works" && rand::thread_rng().gen_ratio(1, 25) + // { // throw!(StatusCode::IM_A_TEAPOT); // } - let state = context.state_read().await; - if state.should_block(account_id) { throw!( StatusCode::FORBIDDEN, @@ -138,7 +139,7 @@ pub async fn challenge_start(context: &mut Context) -> roa::Result } else { let passed_time = current_time - challenge.started; // if it hasn't expired yet, throw an error - if passed_time.as_secs() < (state.config.challenge_expiry as u64) { + if passed_time.as_secs() < u64::from(state.config.challenge_expiry) { throw!( StatusCode::FORBIDDEN, "challenge already requested for this account ID, please wait a minute and try again" @@ -264,7 +265,7 @@ pub async fn challenge_finish(context: &mut Context) -> roa::Result // no ratelimiting in debug mode if !cfg!(debug_assertions) { match state.record_login_attempt(&user_ip) { - Ok(_) => {} + Ok(()) => {} Err(err) => { warn!("peer is sending too many verification requests: {}", user_ip); throw!(StatusCode::TOO_MANY_REQUESTS, err.to_string()) @@ -370,8 +371,6 @@ pub async fn challenge_finish(context: &mut Context) -> roa::Result context.write(format!("{}:{}", comment_id, b64e::STANDARD.encode(authkey))); return Ok(()); - } else { - break; } } diff --git a/server/central/src/web/routes/game_server.rs b/server/central/src/web/routes/game_server.rs index 6aae837b..37080905 100644 --- a/server/central/src/web/routes/game_server.rs +++ b/server/central/src/web/routes/game_server.rs @@ -30,7 +30,7 @@ macro_rules! check_user_agent { let $ua = useragent.unwrap().to_str()?; if !cfg!(debug_assertions) && !$ua.starts_with("globed-game-server") { - throw!(StatusCode::UNAUTHORIZED, "mismatched user agent"); + throw!(StatusCode::UNAUTHORIZED, "bad request"); } }; } diff --git a/server/game/src/bytebufferext.rs b/server/game/src/bytebufferext.rs index 1b776bf5..013be6fb 100644 --- a/server/game/src/bytebufferext.rs +++ b/server/game/src/bytebufferext.rs @@ -127,7 +127,7 @@ impl ByteBufferExt for ByteBuffer { impl ByteBufferExtWrite for ByteBuffer { fn write_bool(&mut self, val: bool) { - self.write_u8(if val { 1u8 } else { 0u8 }); + self.write_u8(u8::from(val)); } fn write_byte_array(&mut self, val: &[u8]) { @@ -152,25 +152,25 @@ impl ByteBufferExtWrite for ByteBuffer { fn write_value_vec(&mut self, val: &[T]) { self.write_u32(val.len() as u32); - for elem in val.iter() { + for elem in val { elem.encode(self); } } fn write_enum, B: Encodable>(&mut self, val: E) { - self.write_value(&val.into()) + self.write_value(&val.into()); } fn write_color3(&mut self, val: cocos::Color3B) { - self.write_value(&val) + self.write_value(&val); } fn write_color4(&mut self, val: cocos::Color4B) { - self.write_value(&val) + self.write_value(&val); } fn write_point(&mut self, val: cocos::Point) { - self.write_value(&val) + self.write_value(&val); } } diff --git a/server/game/src/data/packets/client/game.rs b/server/game/src/data/packets/client/game.rs index f885103c..dfd581a9 100644 --- a/server/game/src/data/packets/client/game.rs +++ b/server/game/src/data/packets/client/game.rs @@ -29,13 +29,9 @@ packet!(RequestProfilesPacket, 11001, false, { encode_unimpl!(RequestProfilesPacket); decode_impl!(RequestProfilesPacket, buf, { - let len = buf.read_u32()?; - let mut ids = Vec::new(); - for _ in 0..len { - ids.push(buf.read_i32()?); - } - - Ok(Self { ids }) + Ok(Self { + ids: buf.read_value_vec()?, + }) }); /* LevelJoinPacket - 11002 */ @@ -66,6 +62,10 @@ encode_unimpl!(PlayerDataPacket); decode_impl!(PlayerDataPacket, buf, Ok(Self { data: buf.read_value()? })); +/* RequestPlayerListPacket - 11005 */ + +empty_client_packet!(RequestPlayerListPacket, 11005); + /* VoicePacket - 11010 */ packet!(VoicePacket, 11010, true, { diff --git a/server/game/src/data/packets/server/game.rs b/server/game/src/data/packets/server/game.rs index bc9f4986..e37f915b 100644 --- a/server/game/src/data/packets/server/game.rs +++ b/server/game/src/data/packets/server/game.rs @@ -22,6 +22,18 @@ decode_unimpl!(PlayerProfilesPacket); empty_server_packet!(LevelDataPacket, 21001); +/* PlayerListPacket - 21002 */ + +packet!(PlayerListPacket, 21002, false, { + profiles: Vec, +}); + +encode_impl!(PlayerListPacket, buf, self, { + buf.write_value_vec(&self.profiles); +}); + +decode_unimpl!(PlayerListPacket); + /* VoiceBroadcastPacket - 21010 */ packet!(VoiceBroadcastPacket, 21010, true, { diff --git a/server/game/src/data/types/gd.rs b/server/game/src/data/types/gd.rs index 7a2aabbc..6bac35a7 100644 --- a/server/game/src/data/types/gd.rs +++ b/server/game/src/data/types/gd.rs @@ -6,18 +6,18 @@ use super::Color3B; #[derive(Clone)] pub struct PlayerIconData { - pub cube: i32, - pub ship: i32, - pub ball: i32, - pub ufo: i32, - pub wave: i32, - pub robot: i32, - pub spider: i32, - pub swing: i32, - pub jetpack: i32, - pub death_effect: i32, - pub color1: i32, - pub color2: i32, + pub cube: i16, + pub ship: i16, + pub ball: i16, + pub ufo: i16, + pub wave: i16, + pub robot: i16, + pub spider: i16, + pub swing: i16, + pub jetpack: i16, + pub death_effect: i16, + pub color1: i16, + pub color2: i16, } impl Default for PlayerIconData { @@ -40,33 +40,33 @@ impl Default for PlayerIconData { } encode_impl!(PlayerIconData, buf, self, { - buf.write_i32(self.cube); - buf.write_i32(self.ship); - buf.write_i32(self.ball); - buf.write_i32(self.ufo); - buf.write_i32(self.wave); - buf.write_i32(self.robot); - buf.write_i32(self.spider); - buf.write_i32(self.swing); - buf.write_i32(self.jetpack); - buf.write_i32(self.death_effect); - buf.write_i32(self.color1); - buf.write_i32(self.color2); + buf.write_i16(self.cube); + buf.write_i16(self.ship); + buf.write_i16(self.ball); + buf.write_i16(self.ufo); + buf.write_i16(self.wave); + buf.write_i16(self.robot); + buf.write_i16(self.spider); + buf.write_i16(self.swing); + buf.write_i16(self.jetpack); + buf.write_i16(self.death_effect); + buf.write_i16(self.color1); + buf.write_i16(self.color2); }); decode_impl!(PlayerIconData, buf, { - let cube = buf.read_i32()?; - let ship = buf.read_i32()?; - let ball = buf.read_i32()?; - let ufo = buf.read_i32()?; - let wave = buf.read_i32()?; - let robot = buf.read_i32()?; - let spider = buf.read_i32()?; - let swing = buf.read_i32()?; - let jetpack = buf.read_i32()?; - let death_effect = buf.read_i32()?; - let color1 = buf.read_i32()?; - let color2 = buf.read_i32()?; + let cube = buf.read_i16()?; + let ship = buf.read_i16()?; + let ball = buf.read_i16()?; + let ufo = buf.read_i16()?; + let wave = buf.read_i16()?; + let robot = buf.read_i16()?; + let spider = buf.read_i16()?; + let swing = buf.read_i16()?; + let jetpack = buf.read_i16()?; + let death_effect = buf.read_i16()?; + let color1 = buf.read_i16()?; + let color2 = buf.read_i16()?; Ok(Self { cube, ship, diff --git a/server/game/src/logger.rs b/server/game/src/logger.rs index 06e7579e..17aa0286 100644 --- a/server/game/src/logger.rs +++ b/server/game/src/logger.rs @@ -10,10 +10,10 @@ const TIME_FORMAT: &str = "[year]-[month]-[day] [hour]:[minute]:[second].[subsec impl log::Log for Logger { fn enabled(&self, metadata: &log::Metadata) -> bool { - if !metadata.target().starts_with("globed_game_server") { - metadata.level() <= Level::Warn - } else { + if metadata.target().starts_with("globed_game_server") { true + } else { + metadata.level() <= Level::Warn } } @@ -37,7 +37,7 @@ impl log::Log for Logger { Level::Trace => (record.level().to_string().black(), record.args().to_string().black()), }; - println!("[{}] [{}] - {}", formatted_time, level, args,) + println!("[{formatted_time}] [{level}] - {args}"); } } diff --git a/server/game/src/main.rs b/server/game/src/main.rs index 2a149326..a5de1c80 100644 --- a/server/game/src/main.rs +++ b/server/game/src/main.rs @@ -1,8 +1,8 @@ -use std::error::Error; +use std::{collections::HashMap, error::Error}; use anyhow::anyhow; use globed_shared::{GameServerBootData, PROTOCOL_VERSION}; -use log::{error, info, LevelFilter}; +use log::{error, info, warn, LevelFilter}; use logger::Logger; use server::GameServerConfiguration; use state::ServerState; @@ -33,36 +33,36 @@ async fn main() -> Result<(), Box> { }); } - let mut host_address: String = "".to_string(); - let mut central_url: String = "".to_string(); - let mut central_pw: String = "".to_string(); + let mut host_address = String::new(); + let mut central_url = String::new(); + let mut central_pw = String::new(); let mut args = std::env::args(); let exe_name = args.next().unwrap(); // skip executable - let arg1 = args.next(); - let arg2 = args.next(); - let arg3 = args.next(); - - if arg1.is_some() { - host_address = arg1.unwrap(); - if arg2.is_some() { - central_url = arg2.unwrap(); - if arg3.is_some() { - central_pw = arg3.unwrap(); + let arg_hostaddr = args.next(); + let arg_central = args.next(); + let arg_password = args.next(); + + if arg_hostaddr.is_some() { + host_address = arg_hostaddr.unwrap(); + if arg_central.is_some() { + central_url = arg_central.unwrap(); + if arg_password.is_some() { + central_pw = arg_password.unwrap(); } } } if host_address.is_empty() { - host_address = std::env::var("GLOBED_GS_ADDRESS").unwrap_or("".to_string()); + host_address = std::env::var("GLOBED_GS_ADDRESS").unwrap_or_default(); } if central_url.is_empty() { - central_url = std::env::var("GLOBED_GS_CENTRAL_URL").unwrap_or("".to_string()); + central_url = std::env::var("GLOBED_GS_CENTRAL_URL").unwrap_or_default(); } if central_pw.is_empty() { - central_pw = std::env::var("GLOBED_GS_CENTRAL_PASSWORD").unwrap_or("".to_string()); + central_pw = std::env::var("GLOBED_GS_CENTRAL_PASSWORD").unwrap_or_default(); } if central_url.is_empty() || central_pw.is_empty() || host_address.is_empty() { @@ -75,7 +75,7 @@ async fn main() -> Result<(), Box> { panic!("aborting due to misconfiguration"); } - if !central_url.ends_with('/') { + if central_url != "none" && !central_url.ends_with('/') { central_url += "/"; } @@ -92,30 +92,45 @@ async fn main() -> Result<(), Box> { let state = ServerState::new(); - info!("Retreiving config from the central server.."); - - let response = config - .http_client - .post(format!("{}{}", config.central_url, "gs/boot")) - .query(&[("pw", config.central_pw.clone())]) - .send() - .await? - .error_for_status() - .map_err(|e| anyhow!("central server returned an error: {e}"))?; - - let configuration = response.text().await?; - let boot_data: GameServerBootData = serde_json::from_str(&configuration)?; + let (gsbd, standalone) = if config.central_url == "none" { + warn!("Starting in standalone mode, authentication is disabled"); + + ( + GameServerBootData { + protocol: PROTOCOL_VERSION, + no_chat: Vec::new(), + special_users: HashMap::new(), + }, + true, + ) + } else { + info!("Retreiving config from the central server.."); + + let response = config + .http_client + .post(format!("{}{}", config.central_url, "gs/boot")) + .query(&[("pw", config.central_pw.clone())]) + .send() + .await? + .error_for_status() + .map_err(|e| anyhow!("central server returned an error: {e}"))?; + + let configuration = response.text().await?; + let boot_data: GameServerBootData = serde_json::from_str(&configuration)?; + + if boot_data.protocol != PROTOCOL_VERSION { + error!("Incompatible protocol versions!"); + error!( + "This game server is on {}, while the central server uses {}", + PROTOCOL_VERSION, boot_data.protocol + ); + panic!("aborting due to incompatible protocol versions"); + } - if boot_data.protocol != PROTOCOL_VERSION { - error!("Incompatible protocol versions!"); - error!( - "This game server is on {}, while the central server uses {}", - PROTOCOL_VERSION, boot_data.protocol - ); - panic!("aborting due to incompatible protocol versions"); - } + (boot_data, false) + }; - let server = Box::leak(Box::new(GameServer::new(host_address, state, boot_data, config).await)); + let server = Box::leak(Box::new(GameServer::new(host_address, state, gsbd, config, standalone).await)); server.run().await?; diff --git a/server/game/src/server.rs b/server/game/src/server.rs index 8b272f68..a50e7474 100644 --- a/server/game/src/server.rs +++ b/server/game/src/server.rs @@ -39,6 +39,7 @@ pub struct GameServer { pub secret_key: SecretKey, pub central_conf: SyncMutex, pub config: GameServerConfiguration, + pub standalone: bool, } impl GameServer { @@ -47,6 +48,7 @@ impl GameServer { state: ServerState, central_conf: GameServerBootData, config: GameServerConfiguration, + standalone: bool, ) -> Self { let secret_key = SecretKey::generate(&mut OsRng); @@ -58,28 +60,31 @@ impl GameServer { secret_key, central_conf: SyncMutex::new(central_conf), config, + standalone, } } pub async fn run(&'static self) -> anyhow::Result<()> { info!("Server launched on {}", self.address); - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(300)); - interval.tick().await; - - loop { + if !self.standalone { + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(300)); interval.tick().await; - match self.refresh_bootdata().await { - Ok(_) => debug!("refreshed central server configuration"), - Err(e) => error!("failed to refresh configuration from the central server: {e}"), + + loop { + interval.tick().await; + match self.refresh_bootdata().await { + Ok(()) => debug!("refreshed central server configuration"), + Err(e) => error!("failed to refresh configuration from the central server: {e}"), + } } - } - }); + }); + } loop { match self.recv_and_handle().await { - Ok(_) => {} + Ok(()) => {} Err(err) => { warn!("Failed to handle a packet: {err}"); } @@ -93,13 +98,13 @@ impl GameServer { // TODO dont send it to every single thread in existence let threads: Vec<_> = self.threads.read().values().cloned().collect(); for thread in threads { - thread.send_message(ServerThreadMessage::BroadcastVoice(vpkt.clone())).await?; + thread.push_new_message(ServerThreadMessage::BroadcastVoice(vpkt.clone()))?; } Ok(()) } - pub async fn gather_profiles(&'static self, ids: &[i32]) -> Vec { + pub fn gather_profiles(&'static self, ids: &[i32]) -> Vec { let threads = self.threads.read(); ids.iter() @@ -112,6 +117,15 @@ impl GameServer { .collect() } + pub fn gather_all_profiles(&'static self) -> Vec { + let threads = self.threads.read(); + threads + .values() + .filter(|thr| thr.authenticated.load(Ordering::Relaxed)) + .map(|thread| thread.account_data.lock().clone()) + .collect() + } + pub fn chat_blocked(&'static self, user_id: i32) -> bool { self.central_conf.lock().no_chat.contains(&user_id) } @@ -137,17 +151,17 @@ impl GameServer { tokio::spawn(async move { match thread.run().await { - Ok(_) => { + Ok(()) => { // remove the thread from the list of threads in order to cleanup log::trace!("removing client: {}", peer); - self.remove_client(&peer).await; + self.remove_client(peer); } Err(err) => { warn!("Client thread died ({peer}): {err}"); } }; - self.post_disconnect_cleanup(thread).await; + self.post_disconnect_cleanup(&thread); }); self.threads.write().insert(peer, thread_cl.clone()); @@ -155,25 +169,23 @@ impl GameServer { }; // don't heap allocate for small packets - thread - .send_message(if len <= SMALL_PACKET_LIMIT { - let mut smallbuf = [0u8; SMALL_PACKET_LIMIT]; - smallbuf[..len].copy_from_slice(&buf[..len]); - - ServerThreadMessage::SmallPacket(smallbuf) - } else { - ServerThreadMessage::Packet(buf[..len].to_vec()) - }) - .await?; + thread.push_new_message(if len <= SMALL_PACKET_LIMIT { + let mut smallbuf = [0u8; SMALL_PACKET_LIMIT]; + smallbuf[..len].copy_from_slice(&buf[..len]); + + ServerThreadMessage::SmallPacket(smallbuf) + } else { + ServerThreadMessage::Packet(buf[..len].to_vec()) + })?; Ok(()) } - async fn remove_client(&'static self, key: &SocketAddrV4) { - self.threads.write().remove(key); + fn remove_client(&'static self, key: SocketAddrV4) { + self.threads.write().remove(&key); } - async fn post_disconnect_cleanup(&'static self, thread: Arc) { + fn post_disconnect_cleanup(&'static self, thread: &GameServerThread) { if !thread.authenticated.load(Ordering::Relaxed) { return; } diff --git a/server/game/src/server_thread.rs b/server/game/src/server_thread.rs index 826cbf78..92f78573 100644 --- a/server/game/src/server_thread.rs +++ b/server/game/src/server_thread.rs @@ -23,14 +23,15 @@ use tokio::sync::{ }; use crate::{ - bytebufferext::{ByteBufferExt, ByteBufferExtRead, ByteBufferExtWrite, Decodable}, + bytebufferext::*, data::{ packets::{client::*, server::*, Packet, PacketWithId, PACKET_HEADER_LEN}, - types::{AssociatedPlayerData, CryptoPublicKey, PlayerAccountData}, + types::*, }, server::GameServer, }; +// TODO adjust this to PlayerData size in the future pub const SMALL_PACKET_LIMIT: usize = 128; const CHANNEL_BUFFER: usize = 4; @@ -74,6 +75,15 @@ macro_rules! gs_handler { }; } +macro_rules! gs_handler_sync { + ($self:ident,$name:ident,$pktty:ty,$pkt:ident,$code:expr) => { + fn $name(&$self, buf: &mut ByteReader<'_>) -> anyhow::Result<()> { + let $pkt = <$pktty>::decode_from_reader(buf)?; + $code + } + }; +} + macro_rules! gs_disconnect { ($self:ident, $msg:expr) => { $self.terminate(); @@ -127,19 +137,18 @@ impl GameServerThread { match tokio::time::timeout(Duration::from_secs(60), rx.recv()).await { Ok(Some(message)) => match self.handle_message(message).await { - Ok(_) => {} + Ok(()) => {} Err(err) => warn!("[@{}]: {}", self.peer, err.to_string()), }, - Ok(None) => break, // sender closed - Err(_) => break, // timeout + Ok(None) | Err(_) => break, // sender closed | timeout }; } Ok(()) } - pub async fn send_message(&self, data: ServerThreadMessage) -> anyhow::Result<()> { - self.tx.send(data).await?; + pub fn push_new_message(&self, data: ServerThreadMessage) -> anyhow::Result<()> { + self.tx.try_send(data)?; Ok(()) } @@ -195,17 +204,17 @@ impl GameServerThread { async fn handle_message(&self, message: ServerThreadMessage) -> anyhow::Result<()> { match message { ServerThreadMessage::Packet(data) => match self.handle_packet(&data).await { - Ok(_) => {} + Ok(()) => {} Err(err) => bail!("failed to handle packet: {err}"), }, ServerThreadMessage::SmallPacket(data) => match self.handle_packet(&data).await { - Ok(_) => {} + Ok(()) => {} Err(err) => bail!("failed to handle packet: {err}"), }, ServerThreadMessage::BroadcastVoice(voice_packet) => match self.send_packet(&*voice_packet).await { - Ok(_) => {} + Ok(()) => {} Err(err) => bail!("failed to broadcast voice packet: {err}"), }, } @@ -267,7 +276,7 @@ impl GameServerThread { 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).await, + DisconnectPacket::PACKET_ID => self.handle_disconnect(&mut data), /* game related */ SyncIconsPacket::PACKET_ID => self.handle_sync_icons(&mut data).await, @@ -275,6 +284,7 @@ impl GameServerThread { 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(anyhow!("No handler for packet id {x}")), @@ -286,9 +296,7 @@ impl GameServerThread { id: packet.id, player_count: self.game_server.state.player_count.load(Ordering::Relaxed), }) - .await?; - - Ok(()) + .await }); gs_handler!(self, handle_crypto_handshake, CryptoHandshakeStartPacket, packet, { @@ -320,8 +328,8 @@ impl GameServerThread { // they would have a new randomized port when they restart and this would never fail. gs_require!(cbox.is_none(), "attempting to initialize a cryptobox twice"); - let sbox = ChaChaBox::new(&packet.key.pubkey, &self.game_server.secret_key); - *cbox = Some(sbox); + let new_box = ChaChaBox::new(&packet.key.pubkey, &self.game_server.secret_key); + *cbox = Some(new_box); } self.send_packet(&CryptoHandshakeResponsePacket { @@ -329,9 +337,7 @@ impl GameServerThread { pubkey: self.game_server.secret_key.public_key().clone(), }, }) - .await?; - - Ok(()) + .await }); gs_handler!(self, handle_keepalive, KeepalivePacket, _packet, { @@ -340,13 +346,26 @@ impl GameServerThread { self.send_packet(&KeepaliveResponsePacket { player_count: self.game_server.state.player_count.load(Ordering::Relaxed), }) - .await?; - - Ok(()) + .await }); gs_handler!(self, handle_login, LoginPacket, packet, { + if self.game_server.standalone { + debug!("Bypassing login for {}", 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(&LoggedInPacket {}).await?; + return Ok(()); + } + // lets verify the given token + let url = format!("{}gs/verify", self.game_server.config.central_url); let response = self @@ -368,7 +387,7 @@ impl GameServerThread { if !response.starts_with("status_ok:") { self.terminate(); self.send_packet(&LoginFailedPacket { - message: format!("authentication failed: {}", response), + message: format!("authentication failed: {response}"), }) .await?; @@ -406,12 +425,13 @@ impl GameServerThread { Ok(()) }); - gs_handler!(self, handle_disconnect, DisconnectPacket, _packet, { + gs_handler_sync!(self, handle_disconnect, DisconnectPacket, _packet, { self.terminate(); Ok(()) }); /* game related */ + gs_handler!(self, handle_sync_icons, SyncIconsPacket, packet, { gs_needauth!(self); @@ -424,11 +444,9 @@ impl GameServerThread { gs_needauth!(self); self.send_packet(&PlayerProfilesPacket { - profiles: self.game_server.gather_profiles(&packet.ids).await, + profiles: self.game_server.gather_profiles(&packet.ids), }) - .await?; - - Ok(()) + .await }); gs_handler!(self, handle_level_join, LevelJoinPacket, packet, { @@ -440,7 +458,7 @@ impl GameServerThread { let mut pm = self.game_server.state.player_manager.lock(); if old_level != 0 { - pm.remove_from_level(old_level, account_id) + pm.remove_from_level(old_level, account_id); } pm.add_to_level(packet.level_id, account_id); @@ -491,7 +509,7 @@ impl GameServerThread { buf.write_bool(false); buf.write_u32(players.len() as u32 - 1); // minus ourselves - for player in players.iter() { + for player in &players { if player.account_id == account_id { continue; } @@ -501,9 +519,16 @@ impl GameServerThread { debug!("size at the end: {}", buf.len()); } - self.send_buffer(buf.as_bytes()).await?; + self.send_buffer(buf.as_bytes()).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, { @@ -518,7 +543,7 @@ impl GameServerThread { let passed_time = now.duration_since(*last_voice_packet)?.as_millis(); *last_voice_packet = now; - let total_size = packet.data.opus_frames.iter().map(|frame| frame.len()).sum::(); + let total_size = packet.data.opus_frames.iter().map(Vec::len).sum::(); let throughput = (total_size as f32) / (passed_time as f32); // in kb/s diff --git a/server/protocol.md b/server/protocol.md new file mode 100644 index 00000000..c8bd869e --- /dev/null +++ b/server/protocol.md @@ -0,0 +1,45 @@ +## Protocol + +this is a brief protocol description so that I don't forget what everything does :p + +plus sign means encrypted packet + +### Client + +Connection related + +* 10000 - PingPacket - ping +* 10001 - CryptoHandshakeStartPacket - handshake +* 10002 - KeepalivePacket - keepalive +* 10003+ - LoginPacket - authentication +* 10004 - DisconnectPacket - client disconnection + +Game related + +* 11000 - SyncIconsPacket - store client's icons +* 11001 - RequestProfilesPacket - request icons of other players (response 21000) +* 11002 - LevelJoinPacket - join a level +* 11003 - LevelJoinPacket - leave a level +* 11004 - PlayerDataPacket - player data +* 11005 - RequestPlayerListPacket - request list of all people on the game server (response 21002) +* 11010+ - VoicePacket - voice frame + + +### Server + +Connection related + +* 20000 - PingResponsePacket - ping response +* 20001 - CryptoHandshakeResponsePacket - handshake response +* 20002 - KeepaliveResponsePacket - keepalive response +* 20003 - ServerDisconnectPacket - server kicked you out +* 20004 - LoggedInPacket - successful auth +* 20005 - LoginFailedPacket - bad auth (has error message) +* 20006+ - ServerNoticePacket - message popup for the user + +Game related + +* 21000 - PlayerProfilesPacket - list of players' names and icons +* 21001 - LevelDataPacket - level data +* 21002 - PlayerListPacket - list of all people on the game server +* 21010+ - VoiceBroadcastPacket - voice frame from another user \ No newline at end of file diff --git a/server/readme.md b/server/readme.md index a54dee84..ec86e181 100644 --- a/server/readme.md +++ b/server/readme.md @@ -1,10 +1,12 @@ # Globed Server -**Note**: 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). +Prebuilt server binaries are available for Linux (x64 and ARM64) and Windows (x64) in every GitHub release. -## Central +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). -The central server doesn't use any databases and uses a JSON file for configuration. By default, the configuration 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. +## 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. ### Central server configuration @@ -17,11 +19,11 @@ Note that the server is written with security in mind, so many of those options |---------|---------|----------------|-------------| | `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 | +| `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` | | `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 access | -| `no_chat_list` | `[]` | ✅ | List of account IDs of users who are able to connect and play, but have cannot send text/voice messages | +| `no_chat_list` | `[]` | ✅** | List of account IDs of users who are able to connect and play, but have cannot send text/voice messages | | `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 | @@ -36,12 +38,27 @@ 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 + 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 -todo! +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 + +# or like this: (replace 'export' with 'set' on windows) + +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 +``` + +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. -To bridge the servers together, you must set a password in the central server configuration file. After that you simply pass it as the environment variable `GLOBED_GS_CENTRAL_PASSWORD`. Once you boot up the game server it will try to make a request to the central server and tell you if the authentication succeeded or not. +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. -The minimum log levels are the same as in the central server, except the environment variable to change them is `GLOBED_GS_LESS_LOG` +The minimum log levels are the same as in the central server, except the environment variable to change them is `GLOBED_GS_LESS_LOG`. diff --git a/src/data/packets/all.cpp b/src/data/packets/all.cpp index e773402d..ffd4aa93 100644 --- a/src/data/packets/all.cpp +++ b/src/data/packets/all.cpp @@ -16,6 +16,8 @@ std::shared_ptr matchPacket(packetid_t packetId) { // game related PACKET(PlayerProfilesPacket); + PACKET(LevelDataPacket); + PACKET(PlayerListPacket); #if GLOBED_VOICE_SUPPORT PACKET(VoiceBroadcastPacket); #endif diff --git a/src/data/packets/all.hpp b/src/data/packets/all.hpp index 343ebd02..1b80f3ec 100644 --- a/src/data/packets/all.hpp +++ b/src/data/packets/all.hpp @@ -7,7 +7,7 @@ * misc.hpp - x2xxx - misc * * 2. in your class, inherit Packet and add GLOBED_PACKET(id, encrypt), encrypt should be true for packets that are important. -* 3. add GLOBED_ENCODE and GLOBED_DECODE (you can replcae either with the _UNIMPL version if not meant to be used) +* 3. add GLOBED_ENCODE and GLOBED_DECODE (you can replace either with the _UNIMPL version if not meant to be used) * 4. For client packets, you may also choose to add a ::create(...) function and/or a constructor * 5. For server packets, in `all.cpp` add the packet to the switch as PACKET(id, cls). */ diff --git a/src/data/packets/client/game.hpp b/src/data/packets/client/game.hpp index 5a8671ae..92dd8592 100644 --- a/src/data/packets/client/game.hpp +++ b/src/data/packets/client/game.hpp @@ -91,6 +91,18 @@ class PlayerDataPacket : public Packet { PlayerData data; }; +class RequestPlayerListPacket : public Packet { + GLOBED_PACKET(11005, false) + + GLOBED_PACKET_ENCODE {} + GLOBED_PACKET_DECODE_UNIMPL + + RequestPlayerListPacket() {} + static std::shared_ptr create() { + return std::make_shared(); + } +}; + #if GLOBED_VOICE_SUPPORT #include