From 29073767227a738c6732e51d8c3c52f5a14e4d57 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 23 Feb 2025 16:10:21 -0500 Subject: [PATCH] Add social server and implement all op codes up until chat room connection blocks further requests --- Cargo.lock | 54 ++- Cargo.toml | 7 +- assets/robocraft/servenvmulti.config | 6 +- rc_chat/Cargo.toml | 13 + rc_chat/build_arm64.sh | 3 + rc_chat/run_debug.sh | 3 + rc_chat/src/cli.rs | 31 ++ rc_chat/src/main.rs | 356 +++++++++++++++ rc_chat/src/operations/mod.rs | 3 + rc_chat/src/state.rs | 17 + rc_chat_room/Cargo.toml | 13 + rc_chat_room/build_arm64.sh | 3 + rc_chat_room/run_debug.sh | 3 + rc_chat_room/src/cli.rs | 23 + rc_chat_room/src/data/clan_invite.rs | 23 + rc_chat_room/src/data/friend.rs | 19 + rc_chat_room/src/data/mod.rs | 2 + rc_chat_room/src/main.rs | 404 ++++++++++++++++++ rc_chat_room/src/operations/clan_invite.rs | 26 ++ rc_chat_room/src/operations/friend_list.rs | 29 ++ rc_chat_room/src/operations/mod.rs | 17 + rc_chat_room/src/operations/more_auth.rs | 42 ++ rc_chat_room/src/operations/platoon_invite.rs | 20 + rc_chat_room/src/operations/settings.rs | 16 + rc_chat_room/src/state.rs | 43 ++ rc_services_room/src/data/custom_games.rs | 33 ++ rc_services_room/src/data/garage_bay.rs | 93 ++++ rc_services_room/src/data/item_shop_bundle.rs | 124 ++++++ rc_services_room/src/data/mod.rs | 4 + rc_services_room/src/data/tech_tree.rs | 28 ++ rc_services_room/src/main.rs | 3 + .../src/operations/avatar_info.rs | 14 + .../src/operations/balance_info.rs | 14 + .../src/operations/cube_inventory.rs | 17 + .../src/operations/custom_game_session.rs | 13 + .../src/operations/custom_games_maps.rs | 53 +++ .../src/operations/dev_message.rs | 12 + .../src/operations/game_event_params.rs | 46 ++ .../src/operations/garage_bay_uuid.rs | 12 + .../src/operations/garage_slots.rs | 46 ++ .../src/operations/garage_upgrades.rs | 25 ++ .../src/operations/item_shop_bundles.rs | 29 ++ rc_services_room/src/operations/mod.rs | 42 ++ .../src/operations/owned_cosmetics.rs | 26 ++ .../src/operations/player_level.rs | 18 + .../src/operations/premium_duration.rs | 20 + .../src/operations/robopass_season.rs | 12 + .../src/operations/tech_points.rs | 12 + .../src/operations/tech_tree_data.rs | 28 ++ .../src/operations/tutorial_status.rs | 16 + rc_services_room/src/operations/user_perms.rs | 16 + rc_services_room/src/operations/user_xp.rs | 12 + rc_social/Cargo.toml | 13 + rc_social/build_arm64.sh | 3 + rc_social/run_debug.sh | 3 + rc_social/src/cli.rs | 31 ++ rc_social/src/main.rs | 356 +++++++++++++++ rc_social/src/operations/mod.rs | 3 + rc_social/src/state.rs | 17 + rc_social_room/Cargo.toml | 13 + rc_social_room/build_arm64.sh | 3 + rc_social_room/run_debug.sh | 3 + rc_social_room/src/cli.rs | 23 + rc_social_room/src/data/clan_invite.rs | 23 + rc_social_room/src/data/friend.rs | 19 + rc_social_room/src/data/mod.rs | 2 + rc_social_room/src/main.rs | 404 ++++++++++++++++++ rc_social_room/src/operations/clan_invite.rs | 26 ++ rc_social_room/src/operations/friend_list.rs | 29 ++ rc_social_room/src/operations/mod.rs | 17 + rc_social_room/src/operations/more_auth.rs | 42 ++ .../src/operations/platoon_invite.rs | 20 + rc_social_room/src/operations/settings.rs | 16 + rc_social_room/src/state.rs | 43 ++ rc_static_data/Rocket.toml | 2 +- rc_static_data/run_debug.sh | 4 +- 76 files changed, 3052 insertions(+), 7 deletions(-) create mode 100644 rc_chat/Cargo.toml create mode 100755 rc_chat/build_arm64.sh create mode 100755 rc_chat/run_debug.sh create mode 100644 rc_chat/src/cli.rs create mode 100644 rc_chat/src/main.rs create mode 100644 rc_chat/src/operations/mod.rs create mode 100644 rc_chat/src/state.rs create mode 100644 rc_chat_room/Cargo.toml create mode 100755 rc_chat_room/build_arm64.sh create mode 100755 rc_chat_room/run_debug.sh create mode 100644 rc_chat_room/src/cli.rs create mode 100644 rc_chat_room/src/data/clan_invite.rs create mode 100644 rc_chat_room/src/data/friend.rs create mode 100644 rc_chat_room/src/data/mod.rs create mode 100644 rc_chat_room/src/main.rs create mode 100644 rc_chat_room/src/operations/clan_invite.rs create mode 100644 rc_chat_room/src/operations/friend_list.rs create mode 100644 rc_chat_room/src/operations/mod.rs create mode 100644 rc_chat_room/src/operations/more_auth.rs create mode 100644 rc_chat_room/src/operations/platoon_invite.rs create mode 100644 rc_chat_room/src/operations/settings.rs create mode 100644 rc_chat_room/src/state.rs create mode 100644 rc_services_room/src/data/custom_games.rs create mode 100644 rc_services_room/src/data/garage_bay.rs create mode 100644 rc_services_room/src/data/item_shop_bundle.rs create mode 100644 rc_services_room/src/data/tech_tree.rs create mode 100644 rc_services_room/src/operations/avatar_info.rs create mode 100644 rc_services_room/src/operations/balance_info.rs create mode 100644 rc_services_room/src/operations/cube_inventory.rs create mode 100644 rc_services_room/src/operations/custom_game_session.rs create mode 100644 rc_services_room/src/operations/custom_games_maps.rs create mode 100644 rc_services_room/src/operations/dev_message.rs create mode 100644 rc_services_room/src/operations/game_event_params.rs create mode 100644 rc_services_room/src/operations/garage_bay_uuid.rs create mode 100644 rc_services_room/src/operations/garage_slots.rs create mode 100644 rc_services_room/src/operations/garage_upgrades.rs create mode 100644 rc_services_room/src/operations/item_shop_bundles.rs create mode 100644 rc_services_room/src/operations/owned_cosmetics.rs create mode 100644 rc_services_room/src/operations/player_level.rs create mode 100644 rc_services_room/src/operations/premium_duration.rs create mode 100644 rc_services_room/src/operations/robopass_season.rs create mode 100644 rc_services_room/src/operations/tech_points.rs create mode 100644 rc_services_room/src/operations/tech_tree_data.rs create mode 100644 rc_services_room/src/operations/tutorial_status.rs create mode 100644 rc_services_room/src/operations/user_perms.rs create mode 100644 rc_services_room/src/operations/user_xp.rs create mode 100644 rc_social/Cargo.toml create mode 100755 rc_social/build_arm64.sh create mode 100755 rc_social/run_debug.sh create mode 100644 rc_social/src/cli.rs create mode 100644 rc_social/src/main.rs create mode 100644 rc_social/src/operations/mod.rs create mode 100644 rc_social/src/state.rs create mode 100644 rc_social_room/Cargo.toml create mode 100755 rc_social_room/build_arm64.sh create mode 100755 rc_social_room/run_debug.sh create mode 100644 rc_social_room/src/cli.rs create mode 100644 rc_social_room/src/data/clan_invite.rs create mode 100644 rc_social_room/src/data/friend.rs create mode 100644 rc_social_room/src/data/mod.rs create mode 100644 rc_social_room/src/main.rs create mode 100644 rc_social_room/src/operations/clan_invite.rs create mode 100644 rc_social_room/src/operations/friend_list.rs create mode 100644 rc_social_room/src/operations/mod.rs create mode 100644 rc_social_room/src/operations/more_auth.rs create mode 100644 rc_social_room/src/operations/platoon_invite.rs create mode 100644 rc_social_room/src/operations/settings.rs create mode 100644 rc_social_room/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index b0dbc96..a0d83ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1755,6 +1755,32 @@ dependencies = [ "zerocopy 0.8.14", ] +[[package]] +name = "rc_chat" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "log", + "polariton", + "polariton_auth", + "polariton_server", + "tokio", +] + +[[package]] +name = "rc_chat_room" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "log", + "polariton", + "polariton_auth", + "polariton_server", + "tokio", +] + [[package]] name = "rc_services" version = "0.1.0" @@ -1782,6 +1808,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "rc_social" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "log", + "polariton", + "polariton_auth", + "polariton_server", + "tokio", +] + +[[package]] +name = "rc_social_room" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "log", + "polariton", + "polariton_auth", + "polariton_server", + "tokio", +] + [[package]] name = "rc_static_data" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a3644dd..8cefe9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,12 @@ edition = "2021" [workspace] members = [ - "auth", "polariton_auth", "rc_services", "rc_static_data", "rc_services_room" + "auth", + "polariton_auth", + "rc_services", "rc_services_room", + "rc_static_data", + "rc_social", "rc_social_room", + "rc_chat", "rc_chat_room" ] [workspace.dependencies] diff --git a/assets/robocraft/servenvmulti.config b/assets/robocraft/servenvmulti.config index 47eb7c7..008ad64 100644 --- a/assets/robocraft/servenvmulti.config +++ b/assets/robocraft/servenvmulti.config @@ -4,9 +4,11 @@ 127.0.0.1:4532 - not.used.hopefully:4534 - chat.server.not.a.valid.tld:4534 + 127.0.0.1:4533 + 127.0.0.1:4534 + 127.0.0.1:4535 http://127.0.0.1:8001/ + http://127.0.0.1:8010/live/data.json diff --git a/rc_chat/Cargo.toml b/rc_chat/Cargo.toml new file mode 100644 index 0000000..d935953 --- /dev/null +++ b/rc_chat/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rc_chat" +version = "0.1.0" +edition = "2021" + +[dependencies] +log.workspace = true +env_logger.workspace = true +tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] } +clap.workspace = true +polariton.workspace = true +polariton_auth = { version = "*", path = "../polariton_auth" } +polariton_server.workspace = true diff --git a/rc_chat/build_arm64.sh b/rc_chat/build_arm64.sh new file mode 100755 index 0000000..7010ff6 --- /dev/null +++ b/rc_chat/build_arm64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo build --release --target aarch64-unknown-linux-musl diff --git a/rc_chat/run_debug.sh b/rc_chat/run_debug.sh new file mode 100755 index 0000000..eedcd2c --- /dev/null +++ b/rc_chat/run_debug.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +RUST_BACKTRACE=1 RUST_LOG=debug cargo run diff --git a/rc_chat/src/cli.rs b/rc_chat/src/cli.rs new file mode 100644 index 0000000..47cc79d --- /dev/null +++ b/rc_chat/src/cli.rs @@ -0,0 +1,31 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// TCP port on which to accept connections + #[arg(short, long, default_value_t = 4534)] + pub port: u16, + + /// IP Address on which to accept connections + #[arg(long, default_value_t = {"127.0.0.1".to_string()})] + pub ip: String, + + /// Socket read tries before giving up (0 to never give up) + #[arg(long, default_value_t = 5)] + pub retries: usize, + + /// Domain and port of the game server to send new connections + #[arg(long, default_value_t = {"127.0.0.1:4535".to_string()})] + pub redirect: String, + + /// Name of game server to send new connections + #[arg(long, default_value_t = {"ngram_is_ngnius".to_string()})] + pub room_name: String, +} + +impl CliArgs { + pub fn get() -> Self { + Self::parse() + } +} diff --git a/rc_chat/src/main.rs b/rc_chat/src/main.rs new file mode 100644 index 0000000..2d221af --- /dev/null +++ b/rc_chat/src/main.rs @@ -0,0 +1,356 @@ +mod cli; +mod state; + +use std::num::NonZero; +use std::sync::Arc; + +use polariton_auth::Handshake; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net; + +use polariton::packet::{Cryptographer, Data, Message, Packet, Ping, StandardMessage, StandardPacket}; +use polariton::operation::{OperationResponse, Typed}; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let args = cli::CliArgs::get(); + log::debug!("Got cli args {:?}", args); + + let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address"); + + // memory leak, but only once (so not a big deal) + let redirect_static = Box::leak(Box::new(args.redirect.clone())); + let room_name_static = Box::leak(Box::new(args.room_name.clone())); + + let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?; + + #[cfg(not(debug_assertions))] + loop { + let (socket, address) = listener.accept().await?; + tokio::spawn(process_socket(socket, address, NonZero::new(args.retries), op_handler.clone(), redirect_static, room_name_static)); + } + #[cfg(debug_assertions)] + { + let (socket, address) = listener.accept().await?; + process_socket(socket, address, NonZero::new(args.retries), redirect_static, room_name_static).await; + Ok(()) + } +} + +async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, retries: Option>, redirect_url: &str, lobby_name: &str) { + log::debug!("Accepting connection from address {}", address); + + let mut buf = Vec::new(); + let enc = match do_connect_handshake(&mut buf, &mut socket, retries, lobby_name, redirect_url).await { + Some(x) => x, + None => { + log::error!("Failed to do connect handshake with {}", address); + return; + } + }; + let sock_state = state::State::new(enc); + while let Ok(packet) = receive_packet(&mut buf, &mut socket, retries, sock_state.binrw_args()).await { + match packet { + Packet::Ping(ping) => { + handle_ping(ping, &mut buf, &mut socket).await; + }, + Packet::Packet(packet) => log::warn!("Not handling packet {:?}", packet), + } + } + log::debug!("Goodbye connection from address {}", address); +} + +async fn handle_ping(ping: Ping, buf: &mut Vec, socket: &mut net::TcpStream) { + buf.clear(); + let resp = Packet::Ping(polariton_auth::ping_pong(ping)); + resp.to_buf(buf, None).unwrap(); + let write_count = socket.write(buf).await.unwrap(); + log::debug!("(ping) Write {} bytes to socket: {:?}", write_count, buf); + buf.clear(); +} + +fn buf_likely_valid(buf: &[u8]) -> bool { + buf.is_empty() || buf[0] == Packet::PING_MAGIC || buf[0] == Packet::FRAMED_MAGIC +} + +async fn read_more(buf: &mut Vec, socket: &mut net::TcpStream) -> Result { + let read_count = socket.read_buf(buf).await?; + log::debug!("Read {} bytes from socket: {:?}", read_count, buf); + Ok(read_count) +} + +async fn receive_packet(buf: &mut Vec, socket: &mut net::TcpStream, max_retries: Option>, args: Option>>) -> Result { + if buf.is_empty() { + let read_count = read_more(buf, socket).await?; + if read_count == 0 { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "socket did not read any bytes")); } // bad packet + } + + let mut last_err = None; + let mut must_succeed_next = false; + if let Some(max_retries) = max_retries { + for _ in 0..max_retries.get() { + match Packet::from_buf(&buf, args.clone()) { + Ok(packet) => { + log::debug!("Received packet {:?}", packet); + return Ok(packet); + }, + Err(e) => last_err = Some(e), + } + if must_succeed_next { + break; + } + must_succeed_next = read_more(buf, socket).await? == 0; + } + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap())); + } else { + while buf_likely_valid(buf.as_slice()) { + match Packet::from_buf(&buf, args.clone()) { + Ok(packet) => { + log::debug!("Received packet {:?}", packet); + return Ok(packet); + }, + Err(e) => last_err = Some(e), + } + if must_succeed_next { + break; + } + must_succeed_next = read_more(buf, socket).await? == 0; + } + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap())); + } +} + +async fn send_packet(packet: Packet, buf: &mut Vec, socket: &mut net::TcpStream, args: Option>>) -> Result<(), std::io::Error> { + log::debug!("Sending packet {:?}", packet); + buf.clear(); + packet.to_buf(buf, args).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + let write_count = socket.write(buf).await?; + log::debug!("Write {} bytes to socket: {:?}", write_count, buf); + #[cfg(debug_assertions)] + { + // print out unencrypted packet too + if let Packet::Packet(standard_p) = packet { + if let Message::Standard(standard_m) = standard_p.message { + if standard_m.is_encrypted() { + let standard_m = standard_m.encrypt(false); + let packet = Packet::Packet(StandardPacket { header: standard_p.header, message: Message::Standard(standard_m) }); + packet.to_buf(buf, None).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + log::debug!("Unencrypted bytes of packet: {:?} (len: {})", buf, buf.len()); + } + + } + } + } + buf.clear(); + Ok(()) +} + +const APP_ID: &str = "SocialServer"; + +struct AuthImpl; + +const TOKEN_KEY: u8 = 216; // token;refresh_token +//const UNKNOWN_BYTE_KEY: u8 = 217; +const SERVICE_KEY: u8 = 224; +const USERNAME_KEY: u8 = 225; + +//const CCU_KEY: u8 = 245; + +#[derive(Debug)] +enum AuthError { + WrongService { expected: String, actual: String }, + MissingService, + MissingToken, + MissingUsername, +} + +impl AuthError { + fn log_err(&self) { + match self { + Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected), + Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY), + Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY), + Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY), + } + } +} + +impl polariton_auth::AuthProvider for AuthImpl { + fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> { + if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) { + if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) { + if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) { + if service.string == APP_ID { + let params_resp = std::collections::HashMap::::new(); + //params_resp.insert(CCU_KEY, Typed::Byte(0)); + log::debug!("Auth success for {} (token: {})", user.string, token.string); + Ok(params_resp) + } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) } + } else { Err(AuthError::MissingUsername) } + } else { Err(AuthError::MissingService) } + } else { Err(AuthError::MissingToken) } + } +} + +async fn do_connect_handshake( + buf: &mut Vec, + socket: &mut net::TcpStream, + max_retries: Option>, + game_server_name: &str, + game_server_url: &str, +) -> Option>> { + let handshake = Handshake::new(APP_ID); + // connect + log::debug!("(connect) Handling first packet"); + let packet1 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read connect packet: {}", e); + return None; + } + }; + buf.clear(); + let (handshake, to_send) = match handshake.connect(&packet1) { + Ok(x) => (x.handshake, x.extra), + Err(e) => { + log::error!("Failed to handle connect handshake: {:?}", e.extra); + return None; + } + }; + match send_packet(to_send, buf, socket, None).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send connect ack packet: {}", e); + return None; + } + } + // encrypt + log::debug!("(connect) Handling second packet"); + let mut packet2 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + buf.clear(); + while let Packet::Ping(ping) = packet2 { + handle_ping(ping, buf, socket).await; + packet2 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + buf.clear(); + } + let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) { + Ok(x) => (x.handshake, x.extra.0, x.extra.1), + Err(e) => { + log::error!("Failed to handle encryption handshake: {:?}", e.extra); + return None; + } + }; + match send_packet(to_send, buf, socket, None).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send encryption ack packet: {}", e); + return None; + } + } + // pre-auth + let handshake = handshake.with_auth(AuthImpl); + // authenticate + log::debug!("(connect) Handling third packet"); + let mut packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + buf.clear(); + while let Packet::Ping(ping) = packet3 { + handle_ping(ping, buf, socket).await; + packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + buf.clear(); + } + let to_send = match handshake.authenticate(&packet3, crypto.clone()) { + Ok(x) => x, + Err(h) => match h.extra { + polariton_auth::AuthError::Validation(e) => { + e.log_err(); + return None; + }, + e => { + log::error!("Failed to handle auth handshake: {:?}", e); + return None; + }, + }, + }; + match send_packet(to_send, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send auth ack packet: {}", e); + return None; + } + } + + // redirect to lobby + let mut packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet_j { + handle_ping(ping, buf, socket).await; + packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + } + if let Packet::Packet(msg) = &packet_j { + if let Message::Standard(st) = &msg.message { + if let Data::OpReq(req) = &st.data { + if req.code == 225 { // join lobby + log::debug!("Max players from lobby join request: {:?}", req.params.to_owned().to_dict().get(&255)); + let mut params = std::collections::HashMap::::new(); + params.insert(230 /* game server address */, Typed::Str(game_server_url.into())); + params.insert(255 /* room name */, Typed::Str(game_server_name.into())); + let resp = Packet::from_message( + Message::Standard( + StandardMessage { flags: 0, + data: Data::OpResp(OperationResponse { + code: req.code, + return_code: 0, + message: Typed::Null, + params: params.into(), + }), + }.encrypt(true)), 0, true, Some(crypto.clone())).unwrap(); + match send_packet(resp, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send lobby ack packet: {}", e); + return None; + } + } + } + } + } + } + + Some(crypto) +} diff --git a/rc_chat/src/operations/mod.rs b/rc_chat/src/operations/mod.rs new file mode 100644 index 0000000..caae20b --- /dev/null +++ b/rc_chat/src/operations/mod.rs @@ -0,0 +1,3 @@ +pub fn handler() -> polariton_server::operations::OperationsHandler { + polariton_server::operations::OperationsHandler::new() +} diff --git a/rc_chat/src/state.rs b/rc_chat/src/state.rs new file mode 100644 index 0000000..2bd18ab --- /dev/null +++ b/rc_chat/src/state.rs @@ -0,0 +1,17 @@ +use std::sync::Arc; + +pub struct State { + pub crypto: Box>, +} + +impl State { + pub fn new(c: Box>) -> Self { + Self { + crypto: c, + } + } + + pub fn binrw_args(&self) -> polariton::packet::WriteArgs { + Some(self.crypto.clone()) + } +} diff --git a/rc_chat_room/Cargo.toml b/rc_chat_room/Cargo.toml new file mode 100644 index 0000000..a415aaf --- /dev/null +++ b/rc_chat_room/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rc_chat_room" +version = "0.1.0" +edition = "2021" + +[dependencies] +log.workspace = true +env_logger.workspace = true +tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] } +clap.workspace = true +polariton.workspace = true +polariton_auth = { version = "*", path = "../polariton_auth" } +polariton_server.workspace = true diff --git a/rc_chat_room/build_arm64.sh b/rc_chat_room/build_arm64.sh new file mode 100755 index 0000000..7010ff6 --- /dev/null +++ b/rc_chat_room/build_arm64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo build --release --target aarch64-unknown-linux-musl diff --git a/rc_chat_room/run_debug.sh b/rc_chat_room/run_debug.sh new file mode 100755 index 0000000..eedcd2c --- /dev/null +++ b/rc_chat_room/run_debug.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +RUST_BACKTRACE=1 RUST_LOG=debug cargo run diff --git a/rc_chat_room/src/cli.rs b/rc_chat_room/src/cli.rs new file mode 100644 index 0000000..11819ce --- /dev/null +++ b/rc_chat_room/src/cli.rs @@ -0,0 +1,23 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// TCP port on which to accept connections + #[arg(short, long, default_value_t = 4535)] + pub port: u16, + + /// IP Address on which to accept connections + #[arg(long, default_value_t = {"127.0.0.1".to_string()})] + pub ip: String, + + /// Socket read tries before giving up (0 to never give up) + #[arg(long, default_value_t = 5)] + pub retries: usize, +} + +impl CliArgs { + pub fn get() -> Self { + Self::parse() + } +} diff --git a/rc_chat_room/src/data/clan_invite.rs b/rc_chat_room/src/data/clan_invite.rs new file mode 100644 index 0000000..abbed9d --- /dev/null +++ b/rc_chat_room/src/data/clan_invite.rs @@ -0,0 +1,23 @@ +use polariton::operation::Typed; + +pub struct ClanInviteInfo { + pub username: String, + pub display_name: String, + pub clan_name: String, + pub clan_size: i32, + pub use_custom_avatar: bool, + pub avatar_id: i32, +} + +impl ClanInviteInfo { + pub fn as_transmissible(&self) -> Typed { + Typed::HashMap(vec![ + (Typed::Str("userName".into()), Typed::Str(self.username.clone().into())), + (Typed::Str("displayName".into()), Typed::Str(self.display_name.clone().into())), + (Typed::Str("clanName".into()), Typed::Str(self.clan_name.clone().into())), + (Typed::Str("clanSize".into()), Typed::Int(self.clan_size)), + (Typed::Str("useCustomAvatar".into()), Typed::Bool(self.use_custom_avatar.into())), + (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id)), + ].into()) + } +} diff --git a/rc_chat_room/src/data/friend.rs b/rc_chat_room/src/data/friend.rs new file mode 100644 index 0000000..724116e --- /dev/null +++ b/rc_chat_room/src/data/friend.rs @@ -0,0 +1,19 @@ +use polariton::operation::Typed; + +pub struct AvatarInfo { + pub name: String, + pub use_custom_avatar: bool, + pub avatar_id: i32, +} + +impl AvatarInfo { + pub fn as_transmissible(&self) -> Typed { + Typed::HashMap(vec![ + (Typed::Str("name".into()), Typed::Str(self.name.clone().into())), + (Typed::Str("useCustomAvatar".into()), Typed::Bool(self.use_custom_avatar.into())), + (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id)), + ].into()) + } +} + +// TODO pub struct FriendInfo {} diff --git a/rc_chat_room/src/data/mod.rs b/rc_chat_room/src/data/mod.rs new file mode 100644 index 0000000..4de3fde --- /dev/null +++ b/rc_chat_room/src/data/mod.rs @@ -0,0 +1,2 @@ +pub mod friend; +pub mod clan_invite; diff --git a/rc_chat_room/src/main.rs b/rc_chat_room/src/main.rs new file mode 100644 index 0000000..dcddc27 --- /dev/null +++ b/rc_chat_room/src/main.rs @@ -0,0 +1,404 @@ +mod cli; +mod state; + +mod data; +mod operations; + +use std::num::NonZero; +use std::sync::Arc; + +use polariton_auth::Handshake; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net; + +use polariton::packet::{Cryptographer, Data, Message, Packet, Ping, StandardMessage, StandardPacket}; +use polariton::operation::{OperationResponse, Typed}; + +pub type UserTy = std::sync::RwLock; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let args = cli::CliArgs::get(); + log::debug!("Got cli args {:?}", args); + + let op_handler = Arc::new(operations::handler()); + + let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address"); + + let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?; + + #[cfg(not(debug_assertions))] + loop { + let (socket, address) = listener.accept().await?; + tokio::spawn(process_socket(socket, address, NonZero::new(args.retries), op_handler.clone())); + } + #[cfg(debug_assertions)] + { + let (socket, address) = listener.accept().await?; + process_socket(socket, address, NonZero::new(args.retries), op_handler.clone()).await; + Ok(()) + } +} + +async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, retries: Option>, op_handler: Arc>) { + log::debug!("Accepting connection from address {}", address); + + let mut read_buf = Vec::new(); + let mut write_buf = Vec::new(); + let enc = match do_connect_handshake(&mut read_buf, &mut socket, retries).await { + Some(x) => x, + None => { + log::error!("Failed to do connect handshake with {}", address); + return; + } + }; + let sock_state = state::State::new(enc); + let user_state = sock_state.user(); + while let Ok(packet) = receive_packet(&mut read_buf, &mut socket, retries, sock_state.binrw_args()).await { + match packet { + Packet::Ping(ping) => { + handle_ping(ping, &mut write_buf, &mut socket).await; + for _ in 0..5 { + read_buf.remove(0); + } + }, + Packet::Packet(packet) => { + // remove packet's advertised size from the buffer + for _ in 0..packet.header.len { + read_buf.remove(0); + } + match packet.message { + Message::Ping(ping) => { + handle_ping(ping, &mut write_buf, &mut socket).await; + }, + Message::Standard(msg) => { + + let is_encrypted = msg.is_encrypted(); + match msg.data { + Data::OpReq(req) => { + let resp = op_handler.handle_op(&user_state, req); + let result = send_packet( + Packet::from_message( + Message::Standard(StandardMessage { + flags: 0, + data: Data::OpResp(resp), + }.encrypt(is_encrypted)), + packet.header.channel, + packet.header.is_reliable(), + sock_state.binrw_args()).unwrap(), + &mut write_buf, &mut socket, sock_state.binrw_args()).await; + match result { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send operation response packet: {}", e); + } + } + }, + data => log::warn!("Failed to handle packet with message data {:?}", data), + } + } + } + } + //log::warn!("Not handling packet {:?}", packet), + } + } + log::debug!("Goodbye connection from address {}", address); +} + +async fn handle_ping(ping: Ping, buf: &mut Vec, socket: &mut net::TcpStream) { + buf.clear(); + let resp = Packet::Ping(polariton_auth::ping_pong(ping)); + resp.to_buf(buf, None).unwrap(); + let write_count = socket.write(buf).await.unwrap(); + log::debug!("(ping) Write {} bytes to socket: {:?}", write_count, buf); + buf.clear(); +} + +fn buf_likely_valid(buf: &[u8]) -> bool { + buf.is_empty() || buf[0] == Packet::PING_MAGIC || buf[0] == Packet::FRAMED_MAGIC +} + +async fn read_more(buf: &mut Vec, socket: &mut net::TcpStream) -> Result { + let read_count = socket.read_buf(buf).await?; + log::debug!("Read {} bytes from socket: {:?}", read_count, buf); + Ok(read_count) +} + +async fn receive_packet(buf: &mut Vec, socket: &mut net::TcpStream, max_retries: Option>, args: Option>>) -> Result { + if buf.is_empty() { + let read_count = read_more(buf, socket).await?; + if read_count == 0 { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "socket did not read any bytes")); } // bad packet + } + + let mut last_err = None; + let mut must_succeed_next = false; + if let Some(max_retries) = max_retries { + for _ in 0..max_retries.get() { + match Packet::from_buf(&buf, args.clone()) { + Ok(packet) => { + log::debug!("Received packet {:?}", packet); + return Ok(packet); + }, + Err(e) => last_err = Some(e), + } + if must_succeed_next { + break; + } + must_succeed_next = read_more(buf, socket).await? == 0; + } + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap())); + } else { + while buf_likely_valid(buf.as_slice()) { + match Packet::from_buf(&buf, args.clone()) { + Ok(packet) => { + log::debug!("Received packet {:?}", packet); + return Ok(packet); + }, + Err(e) => last_err = Some(e), + } + if must_succeed_next { + break; + } + must_succeed_next = read_more(buf, socket).await? == 0; + } + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap())); + } +} + +async fn send_packet(packet: Packet, buf: &mut Vec, socket: &mut net::TcpStream, args: Option>>) -> Result<(), std::io::Error> { + log::debug!("Sending packet {:?}", packet); + buf.clear(); + packet.to_buf(buf, args).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + let write_count = socket.write(buf).await?; + log::debug!("Write {} bytes to socket: {:?}", write_count, buf); + #[cfg(debug_assertions)] + { + // print out unencrypted packet too + if let Packet::Packet(standard_p) = packet { + if let Message::Standard(standard_m) = standard_p.message { + if standard_m.is_encrypted() { + let standard_m = standard_m.encrypt(false); + let packet = Packet::Packet(StandardPacket { header: standard_p.header, message: Message::Standard(standard_m) }); + packet.to_buf(buf, None).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + log::debug!("Unencrypted bytes of packet: {:?} (len: {})", buf, buf.len()); + } + + } + } + } + buf.clear(); + Ok(()) +} + +const APP_ID: &str = "SocialServer"; + +struct AuthImpl; + +const TOKEN_KEY: u8 = 216; // token;refresh_token +//const UNKNOWN_BYTE_KEY: u8 = 217; +const SERVICE_KEY: u8 = 224; +const USERNAME_KEY: u8 = 225; + +//const CCU_KEY: u8 = 245; + +#[derive(Debug)] +enum AuthError { + WrongService { expected: String, actual: String }, + MissingService, + MissingToken, + MissingUsername, +} + +impl AuthError { + fn log_err(&self) { + match self { + Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected), + Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY), + Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY), + Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY), + } + } +} + +impl polariton_auth::AuthProvider for AuthImpl { + fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> { + if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) { + if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) { + if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) { + if service.string == APP_ID { + let params_resp = std::collections::HashMap::::new(); + //params_resp.insert(CCU_KEY, Typed::Byte(0)); + log::debug!("Auth success for {} (token: {})", user.string, token.string); + Ok(params_resp) + } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) } + } else { Err(AuthError::MissingUsername) } + } else { Err(AuthError::MissingService) } + } else { Err(AuthError::MissingToken) } + } +} + +async fn do_connect_handshake( + buf: &mut Vec, + socket: &mut net::TcpStream, + max_retries: Option>, +) -> Option>> { + let handshake = Handshake::new(APP_ID); + // connect + log::debug!("(connect) Handling first packet"); + let packet1 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read connect packet: {}", e); + return None; + } + }; + buf.clear(); + let (handshake, to_send) = match handshake.connect(&packet1) { + Ok(x) => (x.handshake, x.extra), + Err(e) => { + log::error!("Failed to handle connect handshake: {:?}", e.extra); + return None; + } + }; + match send_packet(to_send, buf, socket, None).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send connect ack packet: {}", e); + return None; + } + } + // encrypt + log::debug!("(connect) Handling second packet"); + let mut packet2 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + buf.clear(); + while let Packet::Ping(ping) = packet2 { + handle_ping(ping, buf, socket).await; + packet2 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + buf.clear(); + } + let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) { + Ok(x) => (x.handshake, x.extra.0, x.extra.1), + Err(e) => { + log::error!("Failed to handle encryption handshake: {:?}", e.extra); + return None; + } + }; + match send_packet(to_send, buf, socket, None).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send encryption ack packet: {}", e); + return None; + } + } + // pre-auth + let handshake = handshake.with_auth(AuthImpl); + // authenticate + log::debug!("(connect) Handling third packet"); + let mut packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + buf.clear(); + while let Packet::Ping(ping) = packet3 { + handle_ping(ping, buf, socket).await; + packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + buf.clear(); + } + let to_send = match handshake.authenticate(&packet3, crypto.clone()) { + Ok(x) => x, + Err(h) => match h.extra { + polariton_auth::AuthError::Validation(e) => { + e.log_err(); + return None; + }, + e => { + log::error!("Failed to handle auth handshake: {:?}", e); + return None; + }, + }, + }; + match send_packet(to_send, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send auth ack packet: {}", e); + return None; + } + } + + // join lobby + log::debug!("(join lobby) Handling fourth packet"); + let mut packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + buf.clear(); + while let Packet::Ping(ping) = packet_j { + handle_ping(ping, buf, socket).await; + packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + buf.clear(); + } + if let Packet::Packet(msg) = &packet_j { + if let Message::Standard(st) = &msg.message { + if let Data::OpReq(req) = &st.data { + if req.code == 226 { // join lobby (but for real this time) + let mut params = std::collections::HashMap::::new(); + //params.insert(252 /* actors in game */, Typed::Str(game_server_url.into())); + params.insert(254 /* game server address */, Typed::Int(42)); + params.insert(249 /* actor properties */, Typed::HashMap(Vec::new().into())); + params.insert(248 /* game properties */, Typed::HashMap(Vec::new().into())); + let resp = Packet::from_message( + Message::Standard( + StandardMessage { flags: 0, + data: Data::OpResp(OperationResponse { + code: req.code, + return_code: 0, + message: Typed::Null, + params: params.into(), + }), + }.encrypt(true)), 0, true, Some(crypto.clone())).unwrap(); + match send_packet(resp, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send lobby ack packet: {}", e); + return None; + } + } + } + } + } + } + buf.clear(); + + Some(crypto) +} diff --git a/rc_chat_room/src/operations/clan_invite.rs b/rc_chat_room/src/operations/clan_invite.rs new file mode 100644 index 0000000..f779b9c --- /dev/null +++ b/rc_chat_room/src/operations/clan_invite.rs @@ -0,0 +1,26 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Arr}; + +use crate::data::clan_invite::*; + +const PARAM_KEY: u8 = 42; + +pub(super) fn clan_invites_provider() -> SimpleFunc<39, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Arr(Arr { + ty: 104, // hashmap + items: vec![ + ClanInviteInfo { + username: "RE_user1".to_owned(), + display_name: "RE_user1".to_owned(), + clan_name: "RE_clan1".to_owned(), + clan_size: 42, + use_custom_avatar: false, + avatar_id: 0, + }.as_transmissible() + ], + })); + Ok(params.into()) + }) +} diff --git a/rc_chat_room/src/operations/friend_list.rs b/rc_chat_room/src/operations/friend_list.rs new file mode 100644 index 0000000..2669c8a --- /dev/null +++ b/rc_chat_room/src/operations/friend_list.rs @@ -0,0 +1,29 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Arr}; + +use crate::data::friend::*; + +const FRIENDS_PARAM_KEY: u8 = 5; +const AVATAR_PARAM_KEY: u8 = 76; + +pub(super) fn friends_provider() -> SimpleFunc<4, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(FRIENDS_PARAM_KEY, Typed::Arr(Arr { + ty: 99, // custom + items: vec![Typed::Custom(vec![ // FIXME don't manually serialize + 0u8, // byte custom type + 0u8, 5u8, // short custom object size + 3u8, 0u8, 0u8, 0u8, 0u8, // content + ].into())] })); + params.insert(AVATAR_PARAM_KEY, Typed::Arr(Arr { + ty: 104, // hashmap + items: vec![AvatarInfo { + name: "".to_string(), + use_custom_avatar: false, + avatar_id: 1, + }.as_transmissible()], + })); + Ok(params.into()) + }) +} diff --git a/rc_chat_room/src/operations/mod.rs b/rc_chat_room/src/operations/mod.rs new file mode 100644 index 0000000..47bd1d3 --- /dev/null +++ b/rc_chat_room/src/operations/mod.rs @@ -0,0 +1,17 @@ +mod more_auth; +mod friend_list; +mod settings; +mod clan_invite; + +use polariton_server::operations::OperationsHandler; + +pub fn handler() -> OperationsHandler { + OperationsHandler::new() + .without_state(more_auth::MoreLobbyAuth) + .without_state(polariton_server::operations::Ack::<33, _>::default()) // get user clan info (this is equivalent to not being in a clan) + .without_state(friend_list::friends_provider()) // TODO friend object parsing Token: 0x0200169C RID: 5788 + .without_state(settings::settings_provider()) // TODO save settings persistently + .without_state(polariton_server::operations::Ack::<43, _>::default()) // get my clan info (this is equivalent to not being in a clan) + .without_state(clan_invite::clan_invites_provider()) + .without_state(polariton_server::operations::Ack::<19, _>::default()) // get pending platoon invite (this is equivalent to having no pending invite) +} diff --git a/rc_chat_room/src/operations/more_auth.rs b/rc_chat_room/src/operations/more_auth.rs new file mode 100644 index 0000000..b154590 --- /dev/null +++ b/rc_chat_room/src/operations/more_auth.rs @@ -0,0 +1,42 @@ +use polariton::operation::Typed; +use polariton_server::operations::{Operation, OperationCode}; + +pub struct MoreLobbyAuth; + +impl MoreLobbyAuth { + const AUTH_PAYLOAD_KEY: u8 = 245; +} + +impl Operation for MoreLobbyAuth { + type State = (); + type User = crate::UserTy; + + fn handle(&self, params: polariton::operation::ParameterTable, _: &mut Self::State, user: &Self::User) -> polariton::operation::OperationResponse { + let params_dict = params.to_dict(); + if let Some(Typed::Str(auth_payload)) = params_dict.get(&Self::AUTH_PAYLOAD_KEY) { + let mut write_lock = user.write().unwrap(); + if write_lock.update_with_auth(&auth_payload.string) { + let mut resp_params = std::collections::HashMap::new(); + resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0)); + return polariton::operation::OperationResponse { + code: 230, + return_code: 0, + message: polariton::operation::Typed::Null, + params: resp_params.into(), + } + } + } + polariton::operation::OperationResponse { + code: 230, + return_code: 120, + message: polariton::operation::Typed::Null, + params: std::collections::HashMap::new().into(), + } + } +} + +impl OperationCode for MoreLobbyAuth { + fn op_code() -> u8 { + 230 + } +} diff --git a/rc_chat_room/src/operations/platoon_invite.rs b/rc_chat_room/src/operations/platoon_invite.rs new file mode 100644 index 0000000..8a6664d --- /dev/null +++ b/rc_chat_room/src/operations/platoon_invite.rs @@ -0,0 +1,20 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Arr}; + +use crate::data::friend::*; + +const INVITER_NAME_PARAM_KEY: u8 = 19; +const INVITER_DISPLAY_NAME_PARAM_KEY: u8 = 75; +const INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY: u8 = 13; +const INVITER_AVATAR_ID_NAME_PARAM_KEY: u8 = 14; + +pub(super) fn platoon_pending_provider() -> SimpleFunc<19, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(INVITER_NAME_PARAM_KEY, Typed::Str("RE_platoon_inviter".into()))); + params.insert(INVITER_DISPLAY_NAME_PARAM_KEY, Typed::Str("RE_platoon_inviter_display".into()))); + params.insert(INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY, Typed::Bool(false.into()))); + params.insert(INVITER_AVATAR_ID_NAME_PARAM_KEY, Typed::Int(1))); + Ok(params.into()) + }) +} diff --git a/rc_chat_room/src/operations/settings.rs b/rc_chat_room/src/operations/settings.rs new file mode 100644 index 0000000..469d81d --- /dev/null +++ b/rc_chat_room/src/operations/settings.rs @@ -0,0 +1,16 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Dict}; + +const PARAM_KEY: u8 = 30; + +pub(super) fn settings_provider() -> SimpleFunc<24, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Dict(Dict { + key_ty: 115, + val_ty: 42, + items: Vec::default(), + })); + Ok(params.into()) + }) +} diff --git a/rc_chat_room/src/state.rs b/rc_chat_room/src/state.rs new file mode 100644 index 0000000..7adeeb0 --- /dev/null +++ b/rc_chat_room/src/state.rs @@ -0,0 +1,43 @@ +use std::sync::{Arc, RwLock}; + +pub struct State { + pub crypto: Box>, +} + +impl State { + pub fn new(c: Box>) -> Self { + Self { + crypto: c, + } + } + + pub fn binrw_args(&self) -> polariton::packet::WriteArgs { + Some(self.crypto.clone()) + } + + pub fn user(&self) -> crate::UserTy { + RwLock::new(UserState::default()) + } +} + +#[derive(Default, Debug)] +pub struct UserState { + pub uuid: String, + pub token: String, + pub refresh_token: String, +} + +impl UserState { + pub fn update_with_auth(&mut self, auth_str: &str) -> bool { + let splits: Vec<&str> = auth_str.split(';').collect(); + if splits.len() != 3 { + log::warn!("Invalid auth payload: {}", auth_str); + false + } else { + self.uuid = splits[0].to_owned(); + self.token = splits[1].to_owned(); + self.refresh_token = splits[2].to_owned(); + true + } + } +} diff --git a/rc_services_room/src/data/custom_games.rs b/rc_services_room/src/data/custom_games.rs new file mode 100644 index 0000000..2a79437 --- /dev/null +++ b/rc_services_room/src/data/custom_games.rs @@ -0,0 +1,33 @@ +#![allow(dead_code)] + +#[repr(u8)] +pub enum GameMode { + BattleArena = 0, + SuddenDeath = 1, + Pit = 2, + TestMode = 3, + SinglePlayer = 4, + TeamDeathmatch = 5, + Campaign = 6, +} + +impl GameMode { + pub fn as_str(&self) -> &'static str { + match self { + GameMode::BattleArena => "BattleArena", + GameMode::SuddenDeath => "SuddenDeath", + GameMode::Pit => "Pit", + GameMode::TestMode => "TestMode", + GameMode::SinglePlayer => "SinglePlayerTDM", + GameMode::TeamDeathmatch => "TeamDeathmatch", + GameMode::Campaign => "Campaign", + } + } +} + +#[repr(u8)] +pub enum MapVisibility { + Good = 0, + Poor = 1, + Bad = 2, // VeryPoor +} diff --git a/rc_services_room/src/data/garage_bay.rs b/rc_services_room/src/data/garage_bay.rs new file mode 100644 index 0000000..3a68723 --- /dev/null +++ b/rc_services_room/src/data/garage_bay.rs @@ -0,0 +1,93 @@ +use polariton::operation::{Typed, Arr}; + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub enum MovementCategory { + NotAFunctionalItem = 0, + Wheel = 1, + Hover = 2, + Wing = 3, + Rudder = 4, + Thruster = 5, + InsectLeg = 6, + MechLeg = 7, + Ski = 8, + TankTrack = 9, + Rotor = 10, + SprinterLeg = 11, + Propeller = 12, + Laser = 100, + Plasma = 200, + Mortar = 250, + Rail = 300, + Nano = 400, + Tesla = 500, + Aeroflak = 600, + Ion = 650, + Seeker = 701, + Chaingun = 750, + ShieldModule = 800, + GhostModule = 801, + BlinkModule = 802, + EmpModule = 803, + WindowmakerModule = 804, + EnergyModule = 900, +} + +pub struct GarageSlotInfo { + pub name: String, + pub cubes: u32, + pub crf_id: u32, // 0 means not uploaded + pub was_rated: bool, // ignored when not on CRF + pub movement_categories: Vec, + pub uuid: (u32, u32), + pub thumbnail_version: u32, + pub total_robot_cpu: u32, + pub total_cosmetic_cpu: u32, + pub total_robot_ranking: u32, + pub bay_cpu: u32, + pub tutorial_robot: bool, // assumed to be false (when omitted) + pub starter_robot_index: i32, // assumed to be -1 (whem omitted) + pub control_type: i32, // enum??? + pub control_options: Vec, + pub mastery_level: i32, + pub bay_skin_id: String, + pub weapon_order: Vec, +} + +impl GarageSlotInfo { + pub fn as_transmissible(&self) -> Typed { + Typed::HashMap(vec![ + (Typed::Str("name".into()), Typed::Str(self.name.clone().into())), + (Typed::Str("numberCubes".into()), Typed::Int(self.cubes as i32)), + (Typed::Str("crfId".into()), Typed::Int(self.crf_id as i32)), + (Typed::Str("wasRated".into()), Typed::Bool(self.was_rated.into())), + (Typed::Str("movementCategories".into()), Typed::Arr(Arr { + ty: 105, // int + items: self.movement_categories.iter().map(|x| Typed::Int((*x as i32) * 100_000)).collect(), + })), + (Typed::Str("uniqueId1".into()), Typed::Int(self.uuid.0 as i32)), + (Typed::Str("uniqueId2".into()), Typed::Int(self.uuid.1 as i32)), + (Typed::Str("thumbnailVersion".into()), Typed::Int(self.thumbnail_version as i32)), + (Typed::Str("totalRobotCPU".into()), Typed::Int(self.total_robot_cpu as i32)), + (Typed::Str("totalCosmeticCPU".into()), Typed::Int(self.total_cosmetic_cpu as i32)), + (Typed::Str("totalRobotRanking".into()), Typed::Int(self.total_robot_ranking as i32)), + (Typed::Str("bayCpu".into()), Typed::Int(self.bay_cpu as i32)), + (Typed::Str("tutorialRobot".into()), Typed::Bool(self.tutorial_robot.into())), + (Typed::Str("starterRobotIndex".into()), Typed::Int(self.starter_robot_index)), + (Typed::Str("controlType".into()), Typed::Int(self.control_type)), + (Typed::Str("controlOptions".into()), Typed::Arr(Arr { + ty: 111, // bool + items: self.control_options.iter().map(|&x| Typed::Bool(x.into())).collect(), + })), + (Typed::Str("masteryLevel".into()), Typed::Int(self.mastery_level)), + (Typed::Str("baySkinId".into()), Typed::Str(self.bay_skin_id.clone().into())), + (Typed::Str("weaponOrder".into()), Typed::Arr(Arr { + ty: 105, // int + items: self.weapon_order.iter().map(|x| Typed::Int(*x)).collect(), + })), + ].into()) + } +} + + diff --git a/rc_services_room/src/data/item_shop_bundle.rs b/rc_services_room/src/data/item_shop_bundle.rs new file mode 100644 index 0000000..c267c34 --- /dev/null +++ b/rc_services_room/src/data/item_shop_bundle.rs @@ -0,0 +1,124 @@ +#![allow(dead_code)] + +use std::io::Write; + +use polariton::operation::Typed; + +pub struct ItemShopBundle { + pub sku: String, + pub bundle_name_key: String, + pub sprite: String, + pub is_sprite_full_size: bool, + pub category: ItemShopCategory, + pub currency: CurrencyType, // str + pub price: i32, + pub discount_time: i64, // seconds since unix epoch + pub discount_price: i32, + pub recurrence: ItemShopRecurrence, + pub owns_required_cube: bool, + //pub is_discounted: bool, + pub is_limited_edition: bool, +} + +impl ItemShopBundle { + pub fn as_transmissible(&self) -> Typed { + let mut buf = Vec::new(); + let mut writer = std::io::Cursor::new(&mut buf); + self.dump(&mut writer).unwrap(); + Typed::Bytes(buf.into()) + } + + fn dump(&self, writer: &mut dyn Write) -> std::io::Result { + let sku_bytes = self.sku.as_bytes(); + let mut total_len = writer.write(&encode_7_bit_i32(sku_bytes.len() as i32))?; + total_len += writer.write(sku_bytes)?; + + let bundle_name_key_bytes = self.bundle_name_key.as_bytes(); + total_len += writer.write(&encode_7_bit_i32(bundle_name_key_bytes.len() as i32))?; + total_len += writer.write(bundle_name_key_bytes)?; + + let sprite_bytes = self.sprite.as_bytes(); + total_len += writer.write(&encode_7_bit_i32(sprite_bytes.len() as i32))?; + total_len += writer.write(sprite_bytes)?; + + total_len += writer.write(&[self.is_sprite_full_size as u8])?; + + let currency_bytes = self.currency.as_str().as_bytes(); + total_len += writer.write(&encode_7_bit_i32(currency_bytes.len() as i32))?; + total_len += writer.write(currency_bytes)?; + + total_len += writer.write(&self.price.to_le_bytes())?; + + total_len += writer.write(&self.discount_time.to_le_bytes())?; + + total_len += writer.write(&self.discount_price.to_le_bytes())?; + + total_len += writer.write(&(self.recurrence as i32).to_le_bytes())?; + + total_len += writer.write(&[self.owns_required_cube as u8])?; + + total_len += writer.write(&(self.category as i32).to_le_bytes())?; + + total_len += writer.write(&[self.is_limited_edition as u8])?; + + Ok(total_len) + } + + pub fn as_transmissible_vec(items: Vec) -> Typed { + let mut buf = Vec::new(); + let mut writer = std::io::Cursor::new(&mut buf); + writer.write(&(items.len() as i32).to_le_bytes()).unwrap(); + for item in items.iter() { + item.dump(&mut writer).unwrap(); + } + Typed::Bytes(buf.into()) + } +} + +#[repr(i32)] +#[derive(Copy, Clone)] +pub enum ItemShopCategory { + Cube = 0, + GarageBaySkin = 1, + Bundle = 2, + DeathEffect = 3, + SpawnEffect = 4, + Emotigram = 5, +} + +#[repr(i32)] +#[derive(Copy, Clone)] +pub enum ItemShopRecurrence { + Daily = 0, + Weekly = 1, +} + +#[repr(i32)] +#[derive(Copy, Clone)] +pub enum CurrencyType { + Robits = 0, + CosmeticCredits = 1, +} + +impl CurrencyType { + pub fn as_str(&self) -> &'static str { + match self { + Self::Robits => "Robits", + Self::CosmeticCredits => "CosmeticCredits", + } + } +} + +fn encode_7_bit_i32(mut src: i32) -> Vec { + let mut out = Vec::with_capacity(5); + while src != 0 { + let last_7 = (src & 0x7F) as u8; + src = src >> 7; + if src != 0 { + out.push(last_7 | 0x80); + } else { + out.push(last_7); + } + } + out +} diff --git a/rc_services_room/src/data/mod.rs b/rc_services_room/src/data/mod.rs index 5d19c43..4bb7a7a 100644 --- a/rc_services_room/src/data/mod.rs +++ b/rc_services_room/src/data/mod.rs @@ -12,3 +12,7 @@ pub mod cpu_limits; pub mod cosmetic_limits; pub mod taunts_config; pub mod customisation_info; +pub mod garage_bay; +pub mod custom_games; +pub mod tech_tree; +pub mod item_shop_bundle; diff --git a/rc_services_room/src/data/tech_tree.rs b/rc_services_room/src/data/tech_tree.rs new file mode 100644 index 0000000..832f6a1 --- /dev/null +++ b/rc_services_room/src/data/tech_tree.rs @@ -0,0 +1,28 @@ +use polariton::operation::{Typed, Arr}; + +pub struct TechTreeNode { + pub main_cube_id: i32, // hex + pub position_x: i32, + pub position_y: i32, + pub is_unlocked: bool, + pub is_unlockable: bool, + pub tech_points: u32, + pub neighbours: Vec, // cube IDs, hex +} + +impl TechTreeNode { + pub fn as_transmissible(&self) -> Typed { + Typed::HashMap(vec![ + (Typed::Str("mainCubeId".into()), Typed::Str(hex::encode(self.main_cube_id.to_le_bytes()).into())), + (Typed::Str("positionX".into()), Typed::Int(self.position_x)), + (Typed::Str("positionY".into()), Typed::Int(self.position_y)), + (Typed::Str("isUnlocked".into()), Typed::Bool(self.is_unlocked.into())), + (Typed::Str("isUnlockable".into()), Typed::Bool(self.is_unlockable.into())), + (Typed::Str("tp".into()), Typed::Int(self.tech_points as i32)), + (Typed::Str("neighbours".into()), Typed::Arr(Arr { + ty: 115, // str + items: self.neighbours.iter().map(|cube_id| Typed::Str(hex::encode(cube_id.to_le_bytes()).into())).collect(), + })), + ].into()) + } +} diff --git a/rc_services_room/src/main.rs b/rc_services_room/src/main.rs index 52b2619..381a9a2 100644 --- a/rc_services_room/src/main.rs +++ b/rc_services_room/src/main.rs @@ -60,6 +60,9 @@ async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAdd match packet { Packet::Ping(ping) => { handle_ping(ping, &mut write_buf, &mut socket).await; + for _ in 0..5 { + read_buf.remove(0); + } }, Packet::Packet(packet) => { // remove packet's advertised size from the buffer diff --git a/rc_services_room/src/operations/avatar_info.rs b/rc_services_room/src/operations/avatar_info.rs new file mode 100644 index 0000000..0d5dd99 --- /dev/null +++ b/rc_services_room/src/operations/avatar_info.rs @@ -0,0 +1,14 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const IS_CUSTOM_PARAM_KEY: u8 = 130; +const AVATAR_ID_PARAM_KEY: u8 = 129; + +pub(super) fn get_avatar_provider() -> SimpleFunc<110, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(IS_CUSTOM_PARAM_KEY, Typed::Bool(false.into())); + params.insert(AVATAR_ID_PARAM_KEY, Typed::Int(1)); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/balance_info.rs b/rc_services_room/src/operations/balance_info.rs new file mode 100644 index 0000000..303134e --- /dev/null +++ b/rc_services_room/src/operations/balance_info.rs @@ -0,0 +1,14 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const FREE_BALANCE_PARAM_KEY: u8 = 74; +const PAID_BALANCE_PARAM_KEY: u8 = 87; + +pub(super) fn balance_wallet_provider() -> SimpleFunc<66, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(FREE_BALANCE_PARAM_KEY, Typed::Long(31337_000)); + params.insert(PAID_BALANCE_PARAM_KEY, Typed::Long(1)); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/cube_inventory.rs b/rc_services_room/src/operations/cube_inventory.rs new file mode 100644 index 0000000..1ba0f38 --- /dev/null +++ b/rc_services_room/src/operations/cube_inventory.rs @@ -0,0 +1,17 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Dict}; + +const PARAM_KEY: u8 = 16; + +pub(super) fn cube_inv_provider() -> SimpleFunc<16, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Dict(Dict { + key_ty: 105, // int + val_ty: 105, // int + items: vec![ + (Typed::Int(0), Typed::Int(99)), + ] })); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/custom_game_session.rs b/rc_services_room/src/operations/custom_game_session.rs new file mode 100644 index 0000000..316b1de --- /dev/null +++ b/rc_services_room/src/operations/custom_game_session.rs @@ -0,0 +1,13 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const RESPONSE_CODE_PARAM_KEY: u8 = 168; +//const CUSTOM_GAME_DATA_PARAM_KEY: u8 = 169; + +pub(super) fn get_custom_session_provider() -> SimpleFunc<144, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(0 /* Not in any session */)); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/custom_games_maps.rs b/rc_services_room/src/operations/custom_games_maps.rs new file mode 100644 index 0000000..b2e30cf --- /dev/null +++ b/rc_services_room/src/operations/custom_games_maps.rs @@ -0,0 +1,53 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Dict, Arr}; + +use crate::data::custom_games::*; + +const MODE_MAP_PARAM_KEY: u8 = 170; +const MAP_NAMES_PARAM_KEY: u8 = 178; + +pub(super) fn allowed_maps_provider() -> SimpleFunc<146, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(MODE_MAP_PARAM_KEY, Typed::Dict(Dict { + key_ty: 115, // str + val_ty: 121, // arr + items: vec![ + (Typed::Str(GameMode::BattleArena.as_str().into()), Typed::Arr(Arr { + ty: 115, // str + items: vec![ + Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_02_BA".into()), + Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_03_BA".into()), + Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_02_BA".into()), + Typed::Str("Assets/Scenes/Planet_Earth/RC_Planet_Earth_02_BA".into()), + Typed::Str("Assets/Scenes/Planet_Earth/RC_Planet_Earth_01_BA".into()), + Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_03_BA".into()), + ] + })), + (Typed::Str(GameMode::TeamDeathmatch.as_str().into()), Typed::Arr(Arr { + ty: 115, // str + items: vec![ + Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_01_CTF".into()), + Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_01_CTF".into()), + ] + })), + ], + })); + params.insert(MAP_NAMES_PARAM_KEY, Typed::Dict(Dict { + key_ty: 115, // str + val_ty: 115, // str + items: vec![ + (Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_02_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Neptune_02_BA".into())), + (Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_01_CTF".into()), Typed::Str("strCustomGameMapNameRC_Planet_Neptune_01_CTF".into())), + (Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_03_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Mars_03_BA".into())), + (Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_02_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Mars_02_BA".into())), + (Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_01_CTF".into()), Typed::Str("strCustomGameMapNameRC_Planet_Mars_01_CTF".into())), + (Typed::Str("Assets/Scenes/Planet_Earth/RC_Planet_Earth_02_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Earth_02_BA".into())), + (Typed::Str("Assets/Scenes/Planet_Earth/RC_Planet_Earth_01_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Earth_01_BA".into())), + (Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_03_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Neptune_03_BA".into())), + (Typed::Str("Assets/Scenes/Planet_Test/TestRobot".into()), Typed::Str("TestRobot".into())), + ], + })); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/dev_message.rs b/rc_services_room/src/operations/dev_message.rs new file mode 100644 index 0000000..c87898b --- /dev/null +++ b/rc_services_room/src/operations/dev_message.rs @@ -0,0 +1,12 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const PARAM_KEY: u8 = 2; + +pub(super) fn dev_message_provider() -> SimpleFunc<8, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Bytes(Vec::from("No jam was harmed in the reverse-engineering of this game".as_bytes()).into())); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/game_event_params.rs b/rc_services_room/src/operations/game_event_params.rs new file mode 100644 index 0000000..89d02cb --- /dev/null +++ b/rc_services_room/src/operations/game_event_params.rs @@ -0,0 +1,46 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Arr}; + +use crate::data::custom_games::*; + +const MAP_NAMES_PARAM_KEY: u8 = 78; +const VISIBILITY_PARAM_KEY: u8 = 66; +const MODE_PARAM_KEY: u8 = 136; +const AUTO_HEAL_PARAM_KEY: u8 = 37; +const REMAINING_TICKS_PARAM_KEY: u8 = 145; + +pub(super) fn event_system_params_provider() -> SimpleFunc<24, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(MAP_NAMES_PARAM_KEY, Typed::Arr(Arr { + ty: 115, // str + items: vec![ + Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_03_BA".into()), + Typed::Str("Assets/Scenes/Planet_Earth/RC_Planet_Earth_01_BA".into()), + ], + })); + params.insert(VISIBILITY_PARAM_KEY, Typed::Arr(Arr { + ty: 105, // int + items: vec![ + Typed::Int(GameMode::BattleArena as _), + Typed::Int(GameMode::BattleArena as _), + ], + })); + params.insert(MODE_PARAM_KEY, Typed::Arr(Arr { + ty: 105, // int + items: vec![ + Typed::Int(MapVisibility::Good as _), + Typed::Int(MapVisibility::Bad as _), + ], + })); + params.insert(AUTO_HEAL_PARAM_KEY, Typed::Arr(Arr { + ty: 111, // bool + items: vec![ + Typed::Bool(true.into()), + Typed::Bool(false.into()), + ], + })); + params.insert(REMAINING_TICKS_PARAM_KEY, Typed::Long(1_000_000)); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/garage_bay_uuid.rs b/rc_services_room/src/operations/garage_bay_uuid.rs new file mode 100644 index 0000000..fbe9495 --- /dev/null +++ b/rc_services_room/src/operations/garage_bay_uuid.rs @@ -0,0 +1,12 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const PARAM_KEY: u8 = 54; + +pub(super) fn garage_id_provider() -> SimpleFunc<177, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Str(format!("{}_{}", 12345, 54321).into())); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/garage_slots.rs b/rc_services_room/src/operations/garage_slots.rs new file mode 100644 index 0000000..9309a8c --- /dev/null +++ b/rc_services_room/src/operations/garage_slots.rs @@ -0,0 +1,46 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Dict, Arr}; + +use crate::data::garage_bay::*; + +const SLOTS_PARAM_KEY: u8 = 44; +const SELECTED_SLOT_PARAM_KEY: u8 = 43; +const SLOT_ORDER_PARAM_KEY: u8 = 58; + +pub(super) fn garage_slot_provider() -> SimpleFunc<40, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(SLOTS_PARAM_KEY, Typed::Dict(Dict { + key_ty: 105, // int + val_ty: 104, // hashmap + items: vec![ + (Typed::Int(0), GarageSlotInfo { + name: "Reverse-engineer great success!".to_owned(), + cubes: 1, + crf_id: 0, + was_rated: false, + movement_categories: vec![MovementCategory::Wheel], + uuid: (2,4), + thumbnail_version: 0, + total_robot_cpu: 1, + total_cosmetic_cpu: 0, + total_robot_ranking: 1, + bay_cpu: 2_000, + tutorial_robot: false, + starter_robot_index: -1, + control_type: 0, + control_options: vec![false, false], + mastery_level: 1, + bay_skin_id: "".to_owned(), // TODO + weapon_order: vec![0], + }.as_transmissible()) + ], + })); + params.insert(SELECTED_SLOT_PARAM_KEY, Typed::Int(0)); + params.insert(SLOT_ORDER_PARAM_KEY, Typed::Arr(Arr { + ty: 105, // int + items: vec![Typed::Int(0)], + })); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/garage_upgrades.rs b/rc_services_room/src/operations/garage_upgrades.rs new file mode 100644 index 0000000..23ecba0 --- /dev/null +++ b/rc_services_room/src/operations/garage_upgrades.rs @@ -0,0 +1,25 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Dict}; + +const PARAM_KEY: u8 = 1; + +pub(super) fn garage_upgrades_provider() -> SimpleFunc<1, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::HashMap(vec![ + (Typed::Str("cpuIncreaseCost".into()), Typed::Dict(Dict { + key_ty: 110, // int + val_ty: 110, // int + items: vec![ + // (CPU limit, upgrade cost) + (Typed::Int(100), Typed::Int(100)), + (Typed::Int(200), Typed::Int(200)), + (Typed::Int(1_000), Typed::Int(1_000)), + (Typed::Int(2_000), Typed::Int(2_000)), // max regular bot CPU + (Typed::Int(10_000), Typed::Int(10_000)), // max mega bot cpu + ], + })) + ].into())); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/item_shop_bundles.rs b/rc_services_room/src/operations/item_shop_bundles.rs new file mode 100644 index 0000000..5147cd8 --- /dev/null +++ b/rc_services_room/src/operations/item_shop_bundles.rs @@ -0,0 +1,29 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::ParameterTable; + +use crate::data::item_shop_bundle::*; + +const PARAM_KEY: u8 = 65; + +pub(super) fn item_bundle_provider() -> SimpleFunc<188, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, ItemShopBundle::as_transmissible_vec(vec![ + ItemShopBundle { + sku: "12345".to_owned(), + bundle_name_key: "RE_todo_item_shop_bundle_name_key".to_owned(), + sprite: "RE_todo_item_shop_sprite_name".to_owned(), + is_sprite_full_size: true, + category: ItemShopCategory::Cube, + currency: CurrencyType::Robits, + price: 10_000, + discount_time: 1, + discount_price: 5_000, + recurrence: ItemShopRecurrence::Daily, + owns_required_cube: true, + is_limited_edition: true, + } + ])); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index 9ac2fc3..b540daf 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -22,6 +22,27 @@ mod cpu_limits_config; mod cosmetic_config; mod taunts_config; mod all_customisations_info; +// some social requests must complete here +mod tech_points; +mod cube_inventory; +mod player_level; +mod balance_info; +mod premium_duration; +mod tutorial_status; +mod user_perms; +mod garage_slots; +mod robopass_season; +mod owned_cosmetics; +mod dev_message; +mod custom_games_maps; +mod avatar_info; +mod custom_game_session; +mod user_xp; +mod garage_upgrades; +mod game_event_params; +mod garage_bay_uuid; +mod tech_tree_data; +mod item_shop_bundles; use polariton_server::operations::OperationsHandler; @@ -53,5 +74,26 @@ pub fn handler() -> OperationsHandler { .without_state(cosmetic_config::cosmetic_limits_config_provider()) .without_state(taunts_config::taunts_config_provider()) .without_state(all_customisations_info::all_customisations_provider()) + .without_state(tech_points::tech_points_provider()) + .without_state(cube_inventory::cube_inv_provider()) + .without_state(player_level::player_level_info_provider()) + .without_state(balance_info::balance_wallet_provider()) + .without_state(premium_duration::premium_remaining_provider()) + .without_state(tutorial_status::tutorial_info_provider()) + .without_state(user_perms::user_rights_provider()) + .without_state(garage_slots::garage_slot_provider()) + .without_state(robopass_season::robopass_season_provider()) + .without_state(owned_cosmetics::owned_cosmetics_provider()) + .without_state(owned_cosmetics::selected_cosmetics_provider()) + .without_state(dev_message::dev_message_provider()) + .without_state(custom_games_maps::allowed_maps_provider()) + .without_state(avatar_info::get_avatar_provider()) + .without_state(custom_game_session::get_custom_session_provider()) + .without_state(user_xp::get_user_xp_provider()) + .without_state(garage_upgrades::garage_upgrades_provider()) + .without_state(game_event_params::event_system_params_provider()) + .without_state(garage_bay_uuid::garage_id_provider()) + .without_state(tech_tree_data::tech_tree_layout_provider()) + .without_state(item_shop_bundles::item_bundle_provider()) //.without_state(polariton_server::operations::Ack::<70, _>::default()) } diff --git a/rc_services_room/src/operations/owned_cosmetics.rs b/rc_services_room/src/operations/owned_cosmetics.rs new file mode 100644 index 0000000..95d82d8 --- /dev/null +++ b/rc_services_room/src/operations/owned_cosmetics.rs @@ -0,0 +1,26 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Arr}; + +const PARAM_KEY: u8 = 50; + +pub(super) fn owned_cosmetics_provider() -> SimpleFunc<23, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Arr(Arr { + ty: 115, // str + items: vec![Typed::Str("1".into())], + })); + Ok(params.into()) + }) +} + +pub(super) fn selected_cosmetics_provider() -> SimpleFunc<21, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Arr(Arr { + ty: 115, // str + items: vec![Typed::Str("1".into())], + })); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/player_level.rs b/rc_services_room/src/operations/player_level.rs new file mode 100644 index 0000000..9e8fd6f --- /dev/null +++ b/rc_services_room/src/operations/player_level.rs @@ -0,0 +1,18 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Dict}; + +const PARAM_KEY: u8 = 1; + +pub(super) fn player_level_info_provider() -> SimpleFunc<3, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Dict(Dict { + key_ty: 105, // int + val_ty: 105, // int + items: vec![ + (Typed::Int(0), Typed::Int(99)), + (Typed::Int(10_000), Typed::Int(99_000)), + ] })); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/premium_duration.rs b/rc_services_room/src/operations/premium_duration.rs new file mode 100644 index 0000000..a2ff27c --- /dev/null +++ b/rc_services_room/src/operations/premium_duration.rs @@ -0,0 +1,20 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const DAYS_PARAM_KEY: u8 = 8; +const HOURS_PARAM_KEY: u8 = 13; +const MINUTES_PARAM_KEY: u8 = 14; +const SECONDS_PARAM_KEY: u8 = 15; +const LIFETIME_PARAM_KEY: u8 = 150; + +pub(super) fn premium_remaining_provider() -> SimpleFunc<15, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(DAYS_PARAM_KEY, Typed::Int(0)); + params.insert(HOURS_PARAM_KEY, Typed::Int(0)); + params.insert(MINUTES_PARAM_KEY, Typed::Int(0)); + params.insert(SECONDS_PARAM_KEY, Typed::Int(0)); + params.insert(LIFETIME_PARAM_KEY, Typed::Bool(false.into())); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/robopass_season.rs b/rc_services_room/src/operations/robopass_season.rs new file mode 100644 index 0000000..cb7729e --- /dev/null +++ b/rc_services_room/src/operations/robopass_season.rs @@ -0,0 +1,12 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const PARAM_KEY: u8 = 1; + +pub(super) fn robopass_season_provider() -> SimpleFunc<108, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Null); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/tech_points.rs b/rc_services_room/src/operations/tech_points.rs new file mode 100644 index 0000000..251a3dc --- /dev/null +++ b/rc_services_room/src/operations/tech_points.rs @@ -0,0 +1,12 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const PARAM_KEY: u8 = 214; + +pub(super) fn tech_points_provider() -> SimpleFunc<187, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Int(1337)); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/tech_tree_data.rs b/rc_services_room/src/operations/tech_tree_data.rs new file mode 100644 index 0000000..ccbb6a6 --- /dev/null +++ b/rc_services_room/src/operations/tech_tree_data.rs @@ -0,0 +1,28 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Dict}; + +use crate::data::tech_tree::*; + +const PARAM_KEY: u8 = 210; + +pub(super) fn tech_tree_layout_provider() -> SimpleFunc<183, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Dict(Dict { + key_ty: 115, // str + val_ty: 104, // hashmap + items: vec![ + (Typed::Str("1".into()), TechTreeNode { + main_cube_id: 1, + position_x: 0, + position_y: 0, + is_unlocked: true, + is_unlockable: true, + tech_points: 1, + neighbours: Vec::default(), + }.as_transmissible()) + ], + })); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/tutorial_status.rs b/rc_services_room/src/operations/tutorial_status.rs new file mode 100644 index 0000000..3ebb65b --- /dev/null +++ b/rc_services_room/src/operations/tutorial_status.rs @@ -0,0 +1,16 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const IN_PROGRESS_PARAM_KEY: u8 = 140; +const COMPLETED_PARAM_KEY: u8 = 141; +const SKIPPED_PARAM_KEY: u8 = 142; + +pub(super) fn tutorial_info_provider() -> SimpleFunc<122, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(IN_PROGRESS_PARAM_KEY, Typed::Bool(false.into())); + params.insert(COMPLETED_PARAM_KEY, Typed::Bool(true.into())); + params.insert(SKIPPED_PARAM_KEY, Typed::Bool(true.into())); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/user_perms.rs b/rc_services_room/src/operations/user_perms.rs new file mode 100644 index 0000000..d4a4ff9 --- /dev/null +++ b/rc_services_room/src/operations/user_perms.rs @@ -0,0 +1,16 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const MOD_PARAM_KEY: u8 = 10; +const DEV_PARAM_KEY: u8 = 11; +const ADM_PARAM_KEY: u8 = 12; + +pub(super) fn user_rights_provider() -> SimpleFunc<14, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(MOD_PARAM_KEY, Typed::Bool(false.into())); + params.insert(DEV_PARAM_KEY, Typed::Bool(false.into())); + params.insert(ADM_PARAM_KEY, Typed::Bool(false.into())); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/user_xp.rs b/rc_services_room/src/operations/user_xp.rs new file mode 100644 index 0000000..3b356fa --- /dev/null +++ b/rc_services_room/src/operations/user_xp.rs @@ -0,0 +1,12 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const PARAM_KEY: u8 = 8; + +pub(super) fn get_user_xp_provider() -> SimpleFunc<83, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Int(31337)); + Ok(params.into()) + }) +} diff --git a/rc_social/Cargo.toml b/rc_social/Cargo.toml new file mode 100644 index 0000000..aed827b --- /dev/null +++ b/rc_social/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rc_social" +version = "0.1.0" +edition = "2021" + +[dependencies] +log.workspace = true +env_logger.workspace = true +tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] } +clap.workspace = true +polariton.workspace = true +polariton_auth = { version = "*", path = "../polariton_auth" } +polariton_server.workspace = true diff --git a/rc_social/build_arm64.sh b/rc_social/build_arm64.sh new file mode 100755 index 0000000..7010ff6 --- /dev/null +++ b/rc_social/build_arm64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo build --release --target aarch64-unknown-linux-musl diff --git a/rc_social/run_debug.sh b/rc_social/run_debug.sh new file mode 100755 index 0000000..eedcd2c --- /dev/null +++ b/rc_social/run_debug.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +RUST_BACKTRACE=1 RUST_LOG=debug cargo run diff --git a/rc_social/src/cli.rs b/rc_social/src/cli.rs new file mode 100644 index 0000000..47cc79d --- /dev/null +++ b/rc_social/src/cli.rs @@ -0,0 +1,31 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// TCP port on which to accept connections + #[arg(short, long, default_value_t = 4534)] + pub port: u16, + + /// IP Address on which to accept connections + #[arg(long, default_value_t = {"127.0.0.1".to_string()})] + pub ip: String, + + /// Socket read tries before giving up (0 to never give up) + #[arg(long, default_value_t = 5)] + pub retries: usize, + + /// Domain and port of the game server to send new connections + #[arg(long, default_value_t = {"127.0.0.1:4535".to_string()})] + pub redirect: String, + + /// Name of game server to send new connections + #[arg(long, default_value_t = {"ngram_is_ngnius".to_string()})] + pub room_name: String, +} + +impl CliArgs { + pub fn get() -> Self { + Self::parse() + } +} diff --git a/rc_social/src/main.rs b/rc_social/src/main.rs new file mode 100644 index 0000000..2d221af --- /dev/null +++ b/rc_social/src/main.rs @@ -0,0 +1,356 @@ +mod cli; +mod state; + +use std::num::NonZero; +use std::sync::Arc; + +use polariton_auth::Handshake; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net; + +use polariton::packet::{Cryptographer, Data, Message, Packet, Ping, StandardMessage, StandardPacket}; +use polariton::operation::{OperationResponse, Typed}; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let args = cli::CliArgs::get(); + log::debug!("Got cli args {:?}", args); + + let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address"); + + // memory leak, but only once (so not a big deal) + let redirect_static = Box::leak(Box::new(args.redirect.clone())); + let room_name_static = Box::leak(Box::new(args.room_name.clone())); + + let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?; + + #[cfg(not(debug_assertions))] + loop { + let (socket, address) = listener.accept().await?; + tokio::spawn(process_socket(socket, address, NonZero::new(args.retries), op_handler.clone(), redirect_static, room_name_static)); + } + #[cfg(debug_assertions)] + { + let (socket, address) = listener.accept().await?; + process_socket(socket, address, NonZero::new(args.retries), redirect_static, room_name_static).await; + Ok(()) + } +} + +async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, retries: Option>, redirect_url: &str, lobby_name: &str) { + log::debug!("Accepting connection from address {}", address); + + let mut buf = Vec::new(); + let enc = match do_connect_handshake(&mut buf, &mut socket, retries, lobby_name, redirect_url).await { + Some(x) => x, + None => { + log::error!("Failed to do connect handshake with {}", address); + return; + } + }; + let sock_state = state::State::new(enc); + while let Ok(packet) = receive_packet(&mut buf, &mut socket, retries, sock_state.binrw_args()).await { + match packet { + Packet::Ping(ping) => { + handle_ping(ping, &mut buf, &mut socket).await; + }, + Packet::Packet(packet) => log::warn!("Not handling packet {:?}", packet), + } + } + log::debug!("Goodbye connection from address {}", address); +} + +async fn handle_ping(ping: Ping, buf: &mut Vec, socket: &mut net::TcpStream) { + buf.clear(); + let resp = Packet::Ping(polariton_auth::ping_pong(ping)); + resp.to_buf(buf, None).unwrap(); + let write_count = socket.write(buf).await.unwrap(); + log::debug!("(ping) Write {} bytes to socket: {:?}", write_count, buf); + buf.clear(); +} + +fn buf_likely_valid(buf: &[u8]) -> bool { + buf.is_empty() || buf[0] == Packet::PING_MAGIC || buf[0] == Packet::FRAMED_MAGIC +} + +async fn read_more(buf: &mut Vec, socket: &mut net::TcpStream) -> Result { + let read_count = socket.read_buf(buf).await?; + log::debug!("Read {} bytes from socket: {:?}", read_count, buf); + Ok(read_count) +} + +async fn receive_packet(buf: &mut Vec, socket: &mut net::TcpStream, max_retries: Option>, args: Option>>) -> Result { + if buf.is_empty() { + let read_count = read_more(buf, socket).await?; + if read_count == 0 { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "socket did not read any bytes")); } // bad packet + } + + let mut last_err = None; + let mut must_succeed_next = false; + if let Some(max_retries) = max_retries { + for _ in 0..max_retries.get() { + match Packet::from_buf(&buf, args.clone()) { + Ok(packet) => { + log::debug!("Received packet {:?}", packet); + return Ok(packet); + }, + Err(e) => last_err = Some(e), + } + if must_succeed_next { + break; + } + must_succeed_next = read_more(buf, socket).await? == 0; + } + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap())); + } else { + while buf_likely_valid(buf.as_slice()) { + match Packet::from_buf(&buf, args.clone()) { + Ok(packet) => { + log::debug!("Received packet {:?}", packet); + return Ok(packet); + }, + Err(e) => last_err = Some(e), + } + if must_succeed_next { + break; + } + must_succeed_next = read_more(buf, socket).await? == 0; + } + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap())); + } +} + +async fn send_packet(packet: Packet, buf: &mut Vec, socket: &mut net::TcpStream, args: Option>>) -> Result<(), std::io::Error> { + log::debug!("Sending packet {:?}", packet); + buf.clear(); + packet.to_buf(buf, args).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + let write_count = socket.write(buf).await?; + log::debug!("Write {} bytes to socket: {:?}", write_count, buf); + #[cfg(debug_assertions)] + { + // print out unencrypted packet too + if let Packet::Packet(standard_p) = packet { + if let Message::Standard(standard_m) = standard_p.message { + if standard_m.is_encrypted() { + let standard_m = standard_m.encrypt(false); + let packet = Packet::Packet(StandardPacket { header: standard_p.header, message: Message::Standard(standard_m) }); + packet.to_buf(buf, None).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + log::debug!("Unencrypted bytes of packet: {:?} (len: {})", buf, buf.len()); + } + + } + } + } + buf.clear(); + Ok(()) +} + +const APP_ID: &str = "SocialServer"; + +struct AuthImpl; + +const TOKEN_KEY: u8 = 216; // token;refresh_token +//const UNKNOWN_BYTE_KEY: u8 = 217; +const SERVICE_KEY: u8 = 224; +const USERNAME_KEY: u8 = 225; + +//const CCU_KEY: u8 = 245; + +#[derive(Debug)] +enum AuthError { + WrongService { expected: String, actual: String }, + MissingService, + MissingToken, + MissingUsername, +} + +impl AuthError { + fn log_err(&self) { + match self { + Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected), + Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY), + Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY), + Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY), + } + } +} + +impl polariton_auth::AuthProvider for AuthImpl { + fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> { + if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) { + if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) { + if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) { + if service.string == APP_ID { + let params_resp = std::collections::HashMap::::new(); + //params_resp.insert(CCU_KEY, Typed::Byte(0)); + log::debug!("Auth success for {} (token: {})", user.string, token.string); + Ok(params_resp) + } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) } + } else { Err(AuthError::MissingUsername) } + } else { Err(AuthError::MissingService) } + } else { Err(AuthError::MissingToken) } + } +} + +async fn do_connect_handshake( + buf: &mut Vec, + socket: &mut net::TcpStream, + max_retries: Option>, + game_server_name: &str, + game_server_url: &str, +) -> Option>> { + let handshake = Handshake::new(APP_ID); + // connect + log::debug!("(connect) Handling first packet"); + let packet1 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read connect packet: {}", e); + return None; + } + }; + buf.clear(); + let (handshake, to_send) = match handshake.connect(&packet1) { + Ok(x) => (x.handshake, x.extra), + Err(e) => { + log::error!("Failed to handle connect handshake: {:?}", e.extra); + return None; + } + }; + match send_packet(to_send, buf, socket, None).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send connect ack packet: {}", e); + return None; + } + } + // encrypt + log::debug!("(connect) Handling second packet"); + let mut packet2 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + buf.clear(); + while let Packet::Ping(ping) = packet2 { + handle_ping(ping, buf, socket).await; + packet2 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + buf.clear(); + } + let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) { + Ok(x) => (x.handshake, x.extra.0, x.extra.1), + Err(e) => { + log::error!("Failed to handle encryption handshake: {:?}", e.extra); + return None; + } + }; + match send_packet(to_send, buf, socket, None).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send encryption ack packet: {}", e); + return None; + } + } + // pre-auth + let handshake = handshake.with_auth(AuthImpl); + // authenticate + log::debug!("(connect) Handling third packet"); + let mut packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + buf.clear(); + while let Packet::Ping(ping) = packet3 { + handle_ping(ping, buf, socket).await; + packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + buf.clear(); + } + let to_send = match handshake.authenticate(&packet3, crypto.clone()) { + Ok(x) => x, + Err(h) => match h.extra { + polariton_auth::AuthError::Validation(e) => { + e.log_err(); + return None; + }, + e => { + log::error!("Failed to handle auth handshake: {:?}", e); + return None; + }, + }, + }; + match send_packet(to_send, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send auth ack packet: {}", e); + return None; + } + } + + // redirect to lobby + let mut packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet_j { + handle_ping(ping, buf, socket).await; + packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + } + if let Packet::Packet(msg) = &packet_j { + if let Message::Standard(st) = &msg.message { + if let Data::OpReq(req) = &st.data { + if req.code == 225 { // join lobby + log::debug!("Max players from lobby join request: {:?}", req.params.to_owned().to_dict().get(&255)); + let mut params = std::collections::HashMap::::new(); + params.insert(230 /* game server address */, Typed::Str(game_server_url.into())); + params.insert(255 /* room name */, Typed::Str(game_server_name.into())); + let resp = Packet::from_message( + Message::Standard( + StandardMessage { flags: 0, + data: Data::OpResp(OperationResponse { + code: req.code, + return_code: 0, + message: Typed::Null, + params: params.into(), + }), + }.encrypt(true)), 0, true, Some(crypto.clone())).unwrap(); + match send_packet(resp, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send lobby ack packet: {}", e); + return None; + } + } + } + } + } + } + + Some(crypto) +} diff --git a/rc_social/src/operations/mod.rs b/rc_social/src/operations/mod.rs new file mode 100644 index 0000000..caae20b --- /dev/null +++ b/rc_social/src/operations/mod.rs @@ -0,0 +1,3 @@ +pub fn handler() -> polariton_server::operations::OperationsHandler { + polariton_server::operations::OperationsHandler::new() +} diff --git a/rc_social/src/state.rs b/rc_social/src/state.rs new file mode 100644 index 0000000..2bd18ab --- /dev/null +++ b/rc_social/src/state.rs @@ -0,0 +1,17 @@ +use std::sync::Arc; + +pub struct State { + pub crypto: Box>, +} + +impl State { + pub fn new(c: Box>) -> Self { + Self { + crypto: c, + } + } + + pub fn binrw_args(&self) -> polariton::packet::WriteArgs { + Some(self.crypto.clone()) + } +} diff --git a/rc_social_room/Cargo.toml b/rc_social_room/Cargo.toml new file mode 100644 index 0000000..009f7fa --- /dev/null +++ b/rc_social_room/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rc_social_room" +version = "0.1.0" +edition = "2021" + +[dependencies] +log.workspace = true +env_logger.workspace = true +tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] } +clap.workspace = true +polariton.workspace = true +polariton_auth = { version = "*", path = "../polariton_auth" } +polariton_server.workspace = true diff --git a/rc_social_room/build_arm64.sh b/rc_social_room/build_arm64.sh new file mode 100755 index 0000000..7010ff6 --- /dev/null +++ b/rc_social_room/build_arm64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo build --release --target aarch64-unknown-linux-musl diff --git a/rc_social_room/run_debug.sh b/rc_social_room/run_debug.sh new file mode 100755 index 0000000..eedcd2c --- /dev/null +++ b/rc_social_room/run_debug.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +RUST_BACKTRACE=1 RUST_LOG=debug cargo run diff --git a/rc_social_room/src/cli.rs b/rc_social_room/src/cli.rs new file mode 100644 index 0000000..11819ce --- /dev/null +++ b/rc_social_room/src/cli.rs @@ -0,0 +1,23 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// TCP port on which to accept connections + #[arg(short, long, default_value_t = 4535)] + pub port: u16, + + /// IP Address on which to accept connections + #[arg(long, default_value_t = {"127.0.0.1".to_string()})] + pub ip: String, + + /// Socket read tries before giving up (0 to never give up) + #[arg(long, default_value_t = 5)] + pub retries: usize, +} + +impl CliArgs { + pub fn get() -> Self { + Self::parse() + } +} diff --git a/rc_social_room/src/data/clan_invite.rs b/rc_social_room/src/data/clan_invite.rs new file mode 100644 index 0000000..abbed9d --- /dev/null +++ b/rc_social_room/src/data/clan_invite.rs @@ -0,0 +1,23 @@ +use polariton::operation::Typed; + +pub struct ClanInviteInfo { + pub username: String, + pub display_name: String, + pub clan_name: String, + pub clan_size: i32, + pub use_custom_avatar: bool, + pub avatar_id: i32, +} + +impl ClanInviteInfo { + pub fn as_transmissible(&self) -> Typed { + Typed::HashMap(vec![ + (Typed::Str("userName".into()), Typed::Str(self.username.clone().into())), + (Typed::Str("displayName".into()), Typed::Str(self.display_name.clone().into())), + (Typed::Str("clanName".into()), Typed::Str(self.clan_name.clone().into())), + (Typed::Str("clanSize".into()), Typed::Int(self.clan_size)), + (Typed::Str("useCustomAvatar".into()), Typed::Bool(self.use_custom_avatar.into())), + (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id)), + ].into()) + } +} diff --git a/rc_social_room/src/data/friend.rs b/rc_social_room/src/data/friend.rs new file mode 100644 index 0000000..724116e --- /dev/null +++ b/rc_social_room/src/data/friend.rs @@ -0,0 +1,19 @@ +use polariton::operation::Typed; + +pub struct AvatarInfo { + pub name: String, + pub use_custom_avatar: bool, + pub avatar_id: i32, +} + +impl AvatarInfo { + pub fn as_transmissible(&self) -> Typed { + Typed::HashMap(vec![ + (Typed::Str("name".into()), Typed::Str(self.name.clone().into())), + (Typed::Str("useCustomAvatar".into()), Typed::Bool(self.use_custom_avatar.into())), + (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id)), + ].into()) + } +} + +// TODO pub struct FriendInfo {} diff --git a/rc_social_room/src/data/mod.rs b/rc_social_room/src/data/mod.rs new file mode 100644 index 0000000..4de3fde --- /dev/null +++ b/rc_social_room/src/data/mod.rs @@ -0,0 +1,2 @@ +pub mod friend; +pub mod clan_invite; diff --git a/rc_social_room/src/main.rs b/rc_social_room/src/main.rs new file mode 100644 index 0000000..dcddc27 --- /dev/null +++ b/rc_social_room/src/main.rs @@ -0,0 +1,404 @@ +mod cli; +mod state; + +mod data; +mod operations; + +use std::num::NonZero; +use std::sync::Arc; + +use polariton_auth::Handshake; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net; + +use polariton::packet::{Cryptographer, Data, Message, Packet, Ping, StandardMessage, StandardPacket}; +use polariton::operation::{OperationResponse, Typed}; + +pub type UserTy = std::sync::RwLock; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let args = cli::CliArgs::get(); + log::debug!("Got cli args {:?}", args); + + let op_handler = Arc::new(operations::handler()); + + let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address"); + + let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?; + + #[cfg(not(debug_assertions))] + loop { + let (socket, address) = listener.accept().await?; + tokio::spawn(process_socket(socket, address, NonZero::new(args.retries), op_handler.clone())); + } + #[cfg(debug_assertions)] + { + let (socket, address) = listener.accept().await?; + process_socket(socket, address, NonZero::new(args.retries), op_handler.clone()).await; + Ok(()) + } +} + +async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, retries: Option>, op_handler: Arc>) { + log::debug!("Accepting connection from address {}", address); + + let mut read_buf = Vec::new(); + let mut write_buf = Vec::new(); + let enc = match do_connect_handshake(&mut read_buf, &mut socket, retries).await { + Some(x) => x, + None => { + log::error!("Failed to do connect handshake with {}", address); + return; + } + }; + let sock_state = state::State::new(enc); + let user_state = sock_state.user(); + while let Ok(packet) = receive_packet(&mut read_buf, &mut socket, retries, sock_state.binrw_args()).await { + match packet { + Packet::Ping(ping) => { + handle_ping(ping, &mut write_buf, &mut socket).await; + for _ in 0..5 { + read_buf.remove(0); + } + }, + Packet::Packet(packet) => { + // remove packet's advertised size from the buffer + for _ in 0..packet.header.len { + read_buf.remove(0); + } + match packet.message { + Message::Ping(ping) => { + handle_ping(ping, &mut write_buf, &mut socket).await; + }, + Message::Standard(msg) => { + + let is_encrypted = msg.is_encrypted(); + match msg.data { + Data::OpReq(req) => { + let resp = op_handler.handle_op(&user_state, req); + let result = send_packet( + Packet::from_message( + Message::Standard(StandardMessage { + flags: 0, + data: Data::OpResp(resp), + }.encrypt(is_encrypted)), + packet.header.channel, + packet.header.is_reliable(), + sock_state.binrw_args()).unwrap(), + &mut write_buf, &mut socket, sock_state.binrw_args()).await; + match result { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send operation response packet: {}", e); + } + } + }, + data => log::warn!("Failed to handle packet with message data {:?}", data), + } + } + } + } + //log::warn!("Not handling packet {:?}", packet), + } + } + log::debug!("Goodbye connection from address {}", address); +} + +async fn handle_ping(ping: Ping, buf: &mut Vec, socket: &mut net::TcpStream) { + buf.clear(); + let resp = Packet::Ping(polariton_auth::ping_pong(ping)); + resp.to_buf(buf, None).unwrap(); + let write_count = socket.write(buf).await.unwrap(); + log::debug!("(ping) Write {} bytes to socket: {:?}", write_count, buf); + buf.clear(); +} + +fn buf_likely_valid(buf: &[u8]) -> bool { + buf.is_empty() || buf[0] == Packet::PING_MAGIC || buf[0] == Packet::FRAMED_MAGIC +} + +async fn read_more(buf: &mut Vec, socket: &mut net::TcpStream) -> Result { + let read_count = socket.read_buf(buf).await?; + log::debug!("Read {} bytes from socket: {:?}", read_count, buf); + Ok(read_count) +} + +async fn receive_packet(buf: &mut Vec, socket: &mut net::TcpStream, max_retries: Option>, args: Option>>) -> Result { + if buf.is_empty() { + let read_count = read_more(buf, socket).await?; + if read_count == 0 { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "socket did not read any bytes")); } // bad packet + } + + let mut last_err = None; + let mut must_succeed_next = false; + if let Some(max_retries) = max_retries { + for _ in 0..max_retries.get() { + match Packet::from_buf(&buf, args.clone()) { + Ok(packet) => { + log::debug!("Received packet {:?}", packet); + return Ok(packet); + }, + Err(e) => last_err = Some(e), + } + if must_succeed_next { + break; + } + must_succeed_next = read_more(buf, socket).await? == 0; + } + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap())); + } else { + while buf_likely_valid(buf.as_slice()) { + match Packet::from_buf(&buf, args.clone()) { + Ok(packet) => { + log::debug!("Received packet {:?}", packet); + return Ok(packet); + }, + Err(e) => last_err = Some(e), + } + if must_succeed_next { + break; + } + must_succeed_next = read_more(buf, socket).await? == 0; + } + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap())); + } +} + +async fn send_packet(packet: Packet, buf: &mut Vec, socket: &mut net::TcpStream, args: Option>>) -> Result<(), std::io::Error> { + log::debug!("Sending packet {:?}", packet); + buf.clear(); + packet.to_buf(buf, args).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + let write_count = socket.write(buf).await?; + log::debug!("Write {} bytes to socket: {:?}", write_count, buf); + #[cfg(debug_assertions)] + { + // print out unencrypted packet too + if let Packet::Packet(standard_p) = packet { + if let Message::Standard(standard_m) = standard_p.message { + if standard_m.is_encrypted() { + let standard_m = standard_m.encrypt(false); + let packet = Packet::Packet(StandardPacket { header: standard_p.header, message: Message::Standard(standard_m) }); + packet.to_buf(buf, None).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + log::debug!("Unencrypted bytes of packet: {:?} (len: {})", buf, buf.len()); + } + + } + } + } + buf.clear(); + Ok(()) +} + +const APP_ID: &str = "SocialServer"; + +struct AuthImpl; + +const TOKEN_KEY: u8 = 216; // token;refresh_token +//const UNKNOWN_BYTE_KEY: u8 = 217; +const SERVICE_KEY: u8 = 224; +const USERNAME_KEY: u8 = 225; + +//const CCU_KEY: u8 = 245; + +#[derive(Debug)] +enum AuthError { + WrongService { expected: String, actual: String }, + MissingService, + MissingToken, + MissingUsername, +} + +impl AuthError { + fn log_err(&self) { + match self { + Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected), + Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY), + Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY), + Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY), + } + } +} + +impl polariton_auth::AuthProvider for AuthImpl { + fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> { + if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) { + if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) { + if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) { + if service.string == APP_ID { + let params_resp = std::collections::HashMap::::new(); + //params_resp.insert(CCU_KEY, Typed::Byte(0)); + log::debug!("Auth success for {} (token: {})", user.string, token.string); + Ok(params_resp) + } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) } + } else { Err(AuthError::MissingUsername) } + } else { Err(AuthError::MissingService) } + } else { Err(AuthError::MissingToken) } + } +} + +async fn do_connect_handshake( + buf: &mut Vec, + socket: &mut net::TcpStream, + max_retries: Option>, +) -> Option>> { + let handshake = Handshake::new(APP_ID); + // connect + log::debug!("(connect) Handling first packet"); + let packet1 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read connect packet: {}", e); + return None; + } + }; + buf.clear(); + let (handshake, to_send) = match handshake.connect(&packet1) { + Ok(x) => (x.handshake, x.extra), + Err(e) => { + log::error!("Failed to handle connect handshake: {:?}", e.extra); + return None; + } + }; + match send_packet(to_send, buf, socket, None).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send connect ack packet: {}", e); + return None; + } + } + // encrypt + log::debug!("(connect) Handling second packet"); + let mut packet2 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + buf.clear(); + while let Packet::Ping(ping) = packet2 { + handle_ping(ping, buf, socket).await; + packet2 = match receive_packet(buf, socket, max_retries, None).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + buf.clear(); + } + let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) { + Ok(x) => (x.handshake, x.extra.0, x.extra.1), + Err(e) => { + log::error!("Failed to handle encryption handshake: {:?}", e.extra); + return None; + } + }; + match send_packet(to_send, buf, socket, None).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send encryption ack packet: {}", e); + return None; + } + } + // pre-auth + let handshake = handshake.with_auth(AuthImpl); + // authenticate + log::debug!("(connect) Handling third packet"); + let mut packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + buf.clear(); + while let Packet::Ping(ping) = packet3 { + handle_ping(ping, buf, socket).await; + packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + buf.clear(); + } + let to_send = match handshake.authenticate(&packet3, crypto.clone()) { + Ok(x) => x, + Err(h) => match h.extra { + polariton_auth::AuthError::Validation(e) => { + e.log_err(); + return None; + }, + e => { + log::error!("Failed to handle auth handshake: {:?}", e); + return None; + }, + }, + }; + match send_packet(to_send, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send auth ack packet: {}", e); + return None; + } + } + + // join lobby + log::debug!("(join lobby) Handling fourth packet"); + let mut packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + buf.clear(); + while let Packet::Ping(ping) = packet_j { + handle_ping(ping, buf, socket).await; + packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + buf.clear(); + } + if let Packet::Packet(msg) = &packet_j { + if let Message::Standard(st) = &msg.message { + if let Data::OpReq(req) = &st.data { + if req.code == 226 { // join lobby (but for real this time) + let mut params = std::collections::HashMap::::new(); + //params.insert(252 /* actors in game */, Typed::Str(game_server_url.into())); + params.insert(254 /* game server address */, Typed::Int(42)); + params.insert(249 /* actor properties */, Typed::HashMap(Vec::new().into())); + params.insert(248 /* game properties */, Typed::HashMap(Vec::new().into())); + let resp = Packet::from_message( + Message::Standard( + StandardMessage { flags: 0, + data: Data::OpResp(OperationResponse { + code: req.code, + return_code: 0, + message: Typed::Null, + params: params.into(), + }), + }.encrypt(true)), 0, true, Some(crypto.clone())).unwrap(); + match send_packet(resp, buf, socket, Some(crypto.clone())).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send lobby ack packet: {}", e); + return None; + } + } + } + } + } + } + buf.clear(); + + Some(crypto) +} diff --git a/rc_social_room/src/operations/clan_invite.rs b/rc_social_room/src/operations/clan_invite.rs new file mode 100644 index 0000000..f779b9c --- /dev/null +++ b/rc_social_room/src/operations/clan_invite.rs @@ -0,0 +1,26 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Arr}; + +use crate::data::clan_invite::*; + +const PARAM_KEY: u8 = 42; + +pub(super) fn clan_invites_provider() -> SimpleFunc<39, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Arr(Arr { + ty: 104, // hashmap + items: vec![ + ClanInviteInfo { + username: "RE_user1".to_owned(), + display_name: "RE_user1".to_owned(), + clan_name: "RE_clan1".to_owned(), + clan_size: 42, + use_custom_avatar: false, + avatar_id: 0, + }.as_transmissible() + ], + })); + Ok(params.into()) + }) +} diff --git a/rc_social_room/src/operations/friend_list.rs b/rc_social_room/src/operations/friend_list.rs new file mode 100644 index 0000000..2669c8a --- /dev/null +++ b/rc_social_room/src/operations/friend_list.rs @@ -0,0 +1,29 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Arr}; + +use crate::data::friend::*; + +const FRIENDS_PARAM_KEY: u8 = 5; +const AVATAR_PARAM_KEY: u8 = 76; + +pub(super) fn friends_provider() -> SimpleFunc<4, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(FRIENDS_PARAM_KEY, Typed::Arr(Arr { + ty: 99, // custom + items: vec![Typed::Custom(vec![ // FIXME don't manually serialize + 0u8, // byte custom type + 0u8, 5u8, // short custom object size + 3u8, 0u8, 0u8, 0u8, 0u8, // content + ].into())] })); + params.insert(AVATAR_PARAM_KEY, Typed::Arr(Arr { + ty: 104, // hashmap + items: vec![AvatarInfo { + name: "".to_string(), + use_custom_avatar: false, + avatar_id: 1, + }.as_transmissible()], + })); + Ok(params.into()) + }) +} diff --git a/rc_social_room/src/operations/mod.rs b/rc_social_room/src/operations/mod.rs new file mode 100644 index 0000000..47bd1d3 --- /dev/null +++ b/rc_social_room/src/operations/mod.rs @@ -0,0 +1,17 @@ +mod more_auth; +mod friend_list; +mod settings; +mod clan_invite; + +use polariton_server::operations::OperationsHandler; + +pub fn handler() -> OperationsHandler { + OperationsHandler::new() + .without_state(more_auth::MoreLobbyAuth) + .without_state(polariton_server::operations::Ack::<33, _>::default()) // get user clan info (this is equivalent to not being in a clan) + .without_state(friend_list::friends_provider()) // TODO friend object parsing Token: 0x0200169C RID: 5788 + .without_state(settings::settings_provider()) // TODO save settings persistently + .without_state(polariton_server::operations::Ack::<43, _>::default()) // get my clan info (this is equivalent to not being in a clan) + .without_state(clan_invite::clan_invites_provider()) + .without_state(polariton_server::operations::Ack::<19, _>::default()) // get pending platoon invite (this is equivalent to having no pending invite) +} diff --git a/rc_social_room/src/operations/more_auth.rs b/rc_social_room/src/operations/more_auth.rs new file mode 100644 index 0000000..b154590 --- /dev/null +++ b/rc_social_room/src/operations/more_auth.rs @@ -0,0 +1,42 @@ +use polariton::operation::Typed; +use polariton_server::operations::{Operation, OperationCode}; + +pub struct MoreLobbyAuth; + +impl MoreLobbyAuth { + const AUTH_PAYLOAD_KEY: u8 = 245; +} + +impl Operation for MoreLobbyAuth { + type State = (); + type User = crate::UserTy; + + fn handle(&self, params: polariton::operation::ParameterTable, _: &mut Self::State, user: &Self::User) -> polariton::operation::OperationResponse { + let params_dict = params.to_dict(); + if let Some(Typed::Str(auth_payload)) = params_dict.get(&Self::AUTH_PAYLOAD_KEY) { + let mut write_lock = user.write().unwrap(); + if write_lock.update_with_auth(&auth_payload.string) { + let mut resp_params = std::collections::HashMap::new(); + resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0)); + return polariton::operation::OperationResponse { + code: 230, + return_code: 0, + message: polariton::operation::Typed::Null, + params: resp_params.into(), + } + } + } + polariton::operation::OperationResponse { + code: 230, + return_code: 120, + message: polariton::operation::Typed::Null, + params: std::collections::HashMap::new().into(), + } + } +} + +impl OperationCode for MoreLobbyAuth { + fn op_code() -> u8 { + 230 + } +} diff --git a/rc_social_room/src/operations/platoon_invite.rs b/rc_social_room/src/operations/platoon_invite.rs new file mode 100644 index 0000000..8a6664d --- /dev/null +++ b/rc_social_room/src/operations/platoon_invite.rs @@ -0,0 +1,20 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Arr}; + +use crate::data::friend::*; + +const INVITER_NAME_PARAM_KEY: u8 = 19; +const INVITER_DISPLAY_NAME_PARAM_KEY: u8 = 75; +const INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY: u8 = 13; +const INVITER_AVATAR_ID_NAME_PARAM_KEY: u8 = 14; + +pub(super) fn platoon_pending_provider() -> SimpleFunc<19, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(INVITER_NAME_PARAM_KEY, Typed::Str("RE_platoon_inviter".into()))); + params.insert(INVITER_DISPLAY_NAME_PARAM_KEY, Typed::Str("RE_platoon_inviter_display".into()))); + params.insert(INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY, Typed::Bool(false.into()))); + params.insert(INVITER_AVATAR_ID_NAME_PARAM_KEY, Typed::Int(1))); + Ok(params.into()) + }) +} diff --git a/rc_social_room/src/operations/settings.rs b/rc_social_room/src/operations/settings.rs new file mode 100644 index 0000000..469d81d --- /dev/null +++ b/rc_social_room/src/operations/settings.rs @@ -0,0 +1,16 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed, Dict}; + +const PARAM_KEY: u8 = 30; + +pub(super) fn settings_provider() -> SimpleFunc<24, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::Dict(Dict { + key_ty: 115, + val_ty: 42, + items: Vec::default(), + })); + Ok(params.into()) + }) +} diff --git a/rc_social_room/src/state.rs b/rc_social_room/src/state.rs new file mode 100644 index 0000000..7adeeb0 --- /dev/null +++ b/rc_social_room/src/state.rs @@ -0,0 +1,43 @@ +use std::sync::{Arc, RwLock}; + +pub struct State { + pub crypto: Box>, +} + +impl State { + pub fn new(c: Box>) -> Self { + Self { + crypto: c, + } + } + + pub fn binrw_args(&self) -> polariton::packet::WriteArgs { + Some(self.crypto.clone()) + } + + pub fn user(&self) -> crate::UserTy { + RwLock::new(UserState::default()) + } +} + +#[derive(Default, Debug)] +pub struct UserState { + pub uuid: String, + pub token: String, + pub refresh_token: String, +} + +impl UserState { + pub fn update_with_auth(&mut self, auth_str: &str) -> bool { + let splits: Vec<&str> = auth_str.split(';').collect(); + if splits.len() != 3 { + log::warn!("Invalid auth payload: {}", auth_str); + false + } else { + self.uuid = splits[0].to_owned(); + self.token = splits[1].to_owned(); + self.refresh_token = splits[2].to_owned(); + true + } + } +} diff --git a/rc_static_data/Rocket.toml b/rc_static_data/Rocket.toml index 5127b13..3841eb4 100644 --- a/rc_static_data/Rocket.toml +++ b/rc_static_data/Rocket.toml @@ -5,7 +5,7 @@ port = 8010 ## set only when compiled in debug mode, i.e, `cargo build` [debug] -port = 80 +port = 8010 ## set only when compiled in release mode, i.e, `cargo build --release` [release] diff --git a/rc_static_data/run_debug.sh b/rc_static_data/run_debug.sh index d307e6c..6351ef2 100755 --- a/rc_static_data/run_debug.sh +++ b/rc_static_data/run_debug.sh @@ -1,3 +1,3 @@ #!/bin/bash -cargo build -RUST_LOG=debug sudo -HE ../target/debug/rc_static_data +#cargo build +RUST_LOG=debug cargo run