From c707ed1d044073f55ce86b56d4b00b829b85cacd Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sat, 22 Oct 2022 04:50:13 +0200 Subject: [PATCH] Proxy Support (#48) This PR adds Proxy Support. Closes #39 Co-authored-by: Ryan Johnson --- Cargo.toml | 1 + README.md | 2 +- examples/conway.rs | 6 +- performance_tests/players/src/main.rs | 4 +- src/client.rs | 21 +- src/client/event.rs | 8 +- src/config.rs | 77 ++++++- src/lib.rs | 12 +- src/protocol/packets/c2s.rs | 4 +- src/protocol/packets/s2c.rs | 8 + src/server.rs | 195 +++-------------- src/server/login.rs | 302 ++++++++++++++++++++++++++ 12 files changed, 455 insertions(+), 185 deletions(-) create mode 100644 src/server/login.rs diff --git a/Cargo.toml b/Cargo.toml index 11e822a..fff0ff4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ cfb8 = "0.7.1" flate2 = "1.0.24" flume = "0.10.14" futures = "0.3.24" +hmac = "0.12.1" log = "0.4.17" num = "0.4.0" paste = "1.0.9" diff --git a/README.md b/README.md index e02b6ab..230bfc6 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ place. Here are some noteworthy achievements: - [x] Items - [ ] Inventory - [ ] Block entities -- [ ] Proxy support +- [x] Proxy support ([Velocity](https://velocitypowered.com/), [Bungeecord](https://www.spigotmc.org/wiki/bungeecord/) and [Waterfall](https://docs.papermc.io/waterfall)) - [ ] Sounds, particles, etc. - [ ] Utilities for continuous collision detection diff --git a/examples/conway.rs b/examples/conway.rs index 5f4b3bc..8c17ada 100644 --- a/examples/conway.rs +++ b/examples/conway.rs @@ -1,5 +1,5 @@ use std::mem; -use std::net::SocketAddr; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::atomic::{AtomicUsize, Ordering}; use log::LevelFilter; @@ -66,6 +66,10 @@ impl Config for Game { MAX_PLAYERS + 64 } + fn address(&self) -> SocketAddr { + SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 25565).into() // TODO remove + } + fn dimensions(&self) -> Vec { vec![Dimension { fixed_time: Some(6000), diff --git a/performance_tests/players/src/main.rs b/performance_tests/players/src/main.rs index 1ea0892..4f46dc8 100644 --- a/performance_tests/players/src/main.rs +++ b/performance_tests/players/src/main.rs @@ -44,8 +44,8 @@ impl Config for Game { MAX_PLAYERS + 64 } - fn online_mode(&self) -> bool { - false + fn connection_mode(&self) -> ConnectionMode { + ConnectionMode::Offline } fn outgoing_packet_capacity(&self) -> usize { diff --git a/src/client.rs b/src/client.rs index 5e7e732..fcca223 100644 --- a/src/client.rs +++ b/src/client.rs @@ -34,8 +34,8 @@ pub use crate::protocol::packets::s2c::play::SetTitleAnimationTimes; use crate::protocol::packets::s2c::play::{ AcknowledgeBlockChange, ClearTitles, CombatDeath, CustomSoundEffect, DisconnectPlay, EntityAnimationS2c, EntityAttributesProperty, EntityEvent, GameEvent, GameStateChangeReason, - KeepAliveS2c, LoginPlay, OpenScreen, PlayerPositionLookFlags, RemoveEntities, ResourcePackS2c, - Respawn, S2cPlayPacket, SetActionBarText, SetCenterChunk, SetContainerContent, + KeepAliveS2c, LoginPlay, OpenScreen, PlayerPositionLookFlags, PluginMessageS2c, RemoveEntities, + ResourcePackS2c, Respawn, S2cPlayPacket, SetActionBarText, SetCenterChunk, SetContainerContent, SetDefaultSpawnPosition, SetEntityMetadata, SetEntityVelocity, SetExperience, SetHeadRotation, SetHealth, SetRenderDistance, SetSubtitleText, SetTitleText, SoundCategory, SynchronizePlayerPosition, SystemChatMessage, TeleportEntity, UnloadChunk, UpdateAttributes, @@ -390,6 +390,16 @@ impl Client { self.msgs_to_send.push(msg.into()); } + pub fn send_plugin_message(&mut self, channel: Ident, data: Vec) { + send_packet( + &mut self.send, + PluginMessageS2c { + channel, + data: RawBytes(data), + }, + ); + } + /// Gets the absolute position of this client in the world it is located /// in. pub fn position(&self) -> Vec3 { @@ -930,7 +940,6 @@ impl Client { window_id: c.window_id, }) } - C2sPlayPacket::PluginMessageC2s(_) => {} C2sPlayPacket::EditBook(_) => {} C2sPlayPacket::QueryEntityTag(_) => {} C2sPlayPacket::Interact(p) => { @@ -1107,6 +1116,12 @@ impl Client { }) } } + C2sPlayPacket::PluginMessageC2s(p) => { + self.events.push_back(ClientEvent::PluginMessageReceived { + channel: p.channel, + data: p.data, + }); + } C2sPlayPacket::ProgramJigsawBlock(_) => {} C2sPlayPacket::ProgramStructureBlock(_) => {} C2sPlayPacket::UpdateSign(_) => {} diff --git a/src/client/event.rs b/src/client/event.rs index 9cc791b..c13174b 100644 --- a/src/client/event.rs +++ b/src/client/event.rs @@ -7,6 +7,7 @@ use crate::block_pos::BlockPos; use crate::config::Config; use crate::entity::types::Pose; use crate::entity::{Entity, EntityEvent, EntityId, TrackedData}; +use crate::ident::Ident; use crate::inventory::Inventory; use crate::item::ItemStack; use crate::protocol::packets::c2s::play::ClickContainerMode; @@ -14,7 +15,7 @@ pub use crate::protocol::packets::c2s::play::{ BlockFace, ChatMode, DisplayedSkinParts, Hand, MainHand, ResourcePackC2s as ResourcePackStatus, }; pub use crate::protocol::packets::s2c::play::GameMode; -use crate::protocol::{Slot, SlotId, VarInt}; +use crate::protocol::{RawBytes, Slot, SlotId, VarInt}; /// Represents an action performed by a client. /// @@ -131,6 +132,10 @@ pub enum ClientEvent { /// Sequence number sequence: VarInt, }, + PluginMessageReceived { + channel: Ident, + data: RawBytes, + }, ResourcePackStatusChanged(ResourcePackStatus), /// The client closed a screen. This occurs when the client closes their /// inventory, closes a chest inventory, etc. @@ -331,6 +336,7 @@ pub fn handle_event_default( ClientEvent::SteerBoat { .. } => {} ClientEvent::Digging { .. } => {} ClientEvent::InteractWithBlock { .. } => {} + ClientEvent::PluginMessageReceived { .. } => {} ClientEvent::ResourcePackStatusChanged(_) => {} ClientEvent::CloseScreen { window_id } => { if let Some(window) = &client.open_inventory { diff --git a/src/config.rs b/src/config.rs index d54af04..2ace070 100644 --- a/src/config.rs +++ b/src/config.rs @@ -77,21 +77,15 @@ pub trait Config: Sized + Send + Sync + 'static { STANDARD_TPS } - /// Called once at startup to get the "online mode" option, which determines - /// if client authentication and encryption should take place. - /// - /// When online mode is disabled, malicious clients can give themselves any - /// username and UUID they want, potentially gaining privileges they - /// might not otherwise have. Additionally, encryption is only enabled in - /// online mode. For these reasons online mode should only be disabled - /// for development purposes and enabled on servers exposed to the - /// internet. + /// Called once at startup to get the connection mode option, which + /// determines if client authentication and encryption should take place + /// and if the server should get the player data from a proxy. /// /// # Default Implementation /// - /// Returns `true`. - fn online_mode(&self) -> bool { - true + /// Returns [`ConnectionMode::Online`]. + fn connection_mode(&self) -> ConnectionMode { + ConnectionMode::Online } /// Called once at startup to get the "prevent-proxy-connections" option, @@ -256,6 +250,10 @@ pub trait Config: Sized + Send + Sync + 'static { /// no connections to the server will be made until this function returns. /// /// This method is called from within a tokio runtime. + /// + /// # Default Implementation + /// + /// The default implementation does nothing. fn init(&self, server: &mut Server) {} /// Called once at the beginning of every server update (also known as @@ -298,6 +296,61 @@ pub enum ServerListPing<'a> { Ignore, } +/// Describes how new connections to the server are handled. +#[non_exhaustive] +#[derive(Clone, PartialEq, Default)] +pub enum ConnectionMode { + /// The "online mode" fetches all player data (username, UUID, and skin) + /// from the [configured session server] and enables encryption. + /// + /// This mode should be used for all publicly exposed servers which are not + /// behind a proxy. + /// + /// [configured session server]: Config::format_session_server_url + #[default] + Online, + /// Disables client authentication with the configured session server. + /// Clients can join with any username and UUID they choose, potentially + /// gaining privileges they would not otherwise have. Additionally, + /// encryption is disabled and Minecraft's default skins will be used. + /// + /// This mode should be used for development purposes only and not for + /// publicly exposed servers. + Offline, + /// This mode should be used under one of the following situations: + /// - The server is behind a [BungeeCord]/[Waterfall] proxy with IP + /// forwarding enabled. + /// - The server is behind a [Velocity] proxy configured to use the `legacy` + /// forwarding mode. + /// + /// All player data (username, UUID, and skin) is fetched from the proxy, + /// but no attempt is made to stop connections originating from + /// elsewhere. As a result, you must ensure clients connect through the + /// proxy and are unable to connect to the server directly. Otherwise, + /// clients can use any username or UUID they choose similar to + /// [`ConnectionMode::Offline`]. + /// + /// To protect against this, a firewall can be used. However, + /// [`ConnectionMode::Velocity`] is recommended as a secure alternative. + /// + /// [BungeeCord]: https://www.spigotmc.org/wiki/bungeecord/ + /// [Waterfall]: https://github.com/PaperMC/Waterfall + /// [Velocity]: https://velocitypowered.com/ + BungeeCord, + /// This mode is used when the server is behind a [Velocity] proxy + /// configured with the forwarding mode `modern`. + /// + /// All player data (username, UUID, and skin) is fetched from the proxy and + /// all connections originating from outside Velocity are blocked. + /// + /// [Velocity]: https://velocitypowered.com/ + Velocity { + /// The secret key used to prevent connections from outside Velocity. + /// The proxy and Valence must be configured to use the same secret key. + secret: String, + }, +} + /// Represents an individual entry in the player sample. #[derive(Clone, Debug, Serialize)] pub struct PlayerSampleEntry<'a> { diff --git a/src/lib.rs b/src/lib.rs index 079dc66..8b99a13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,7 +133,7 @@ pub mod prelude { pub use block::{BlockKind, BlockPos, BlockState, PropName, PropValue}; pub use chunk::{Chunk, ChunkPos, Chunks, LoadedChunk, UnloadedChunk}; pub use client::{handle_event_default, Client, ClientEvent, ClientId, Clients, GameMode}; - pub use config::{Config, PlayerSampleEntry, ServerListPing}; + pub use config::{Config, ConnectionMode, PlayerSampleEntry, ServerListPing}; pub use dimension::{Dimension, DimensionId}; pub use entity::{Entities, Entity, EntityEvent, EntityId, EntityKind, TrackedData}; pub use ident::{Ident, IdentError}; @@ -173,6 +173,16 @@ pub const VERSION_NAME: &str = "1.19.2"; /// You should avoid using this namespace in your own identifiers. pub const LIBRARY_NAMESPACE: &str = "valence"; +/// The most recent version of the [Velocity] proxy which has been tested to +/// work with Valence. The elements of the tuple are (major, minor, patch) +/// version numbers. +/// +/// See [`Config::connection_mode`] to configure the proxy used with Valence. +/// +/// [Velocity]: https://velocitypowered.com/ +/// [`Config::connection_mode`]: config::Config::connection_mode +pub const SUPPORTED_VELOCITY_VERSION: (u16, u16, u16) = (3, 1, 2); + /// A discrete unit of time where 1 tick is the duration of a /// single game update. /// diff --git a/src/protocol/packets/c2s.rs b/src/protocol/packets/c2s.rs index 202d75f..79a9c9d 100644 --- a/src/protocol/packets/c2s.rs +++ b/src/protocol/packets/c2s.rs @@ -8,7 +8,9 @@ pub mod handshake { def_struct! { Handshake { protocol_version: VarInt, - server_address: BoundedString<0, 255>, + // by the minecraft protocol this is specified as a BoundedString<0, 255> but due + // issues with Bungeecord ip forwarding this limit is removed here and checked when handling the handshake + server_address: String, server_port: u16, next_state: HandshakeNextState, } diff --git a/src/protocol/packets/s2c.rs b/src/protocol/packets/s2c.rs index d6112b7..e670774 100644 --- a/src/protocol/packets/s2c.rs +++ b/src/protocol/packets/s2c.rs @@ -275,6 +275,13 @@ pub mod play { } } + def_struct! { + PluginMessageS2c { + channel: Ident, + data: RawBytes, + } + } + def_struct! { CustomSoundEffect { name: Ident, @@ -738,6 +745,7 @@ pub mod play { BlockUpdate = 9, BossBar = 10, ClearTitles = 13, + PluginMessageS2c = 22, SetContainerContent = 17, SetContainerProperty = 18, SetContainerSlot = 19, diff --git a/src/server.rs b/src/server.rs index e6781b5..3234e53 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,24 +2,19 @@ use std::error::Error; use std::iter::FusedIterator; -use std::net::SocketAddr; +use std::net::{IpAddr, SocketAddr}; use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use std::{io, thread}; -use anyhow::{bail, ensure, Context}; +use anyhow::{ensure, Context}; use flume::{Receiver, Sender}; -use num::BigInt; use rand::rngs::OsRng; use rayon::iter::ParallelIterator; -use reqwest::{Client as HttpClient, StatusCode}; -use rsa::{PaddingScheme, PublicKeyParts, RsaPrivateKey}; -use serde::Deserialize; +use reqwest::Client as HttpClient; +use rsa::{PublicKeyParts, RsaPrivateKey}; use serde_json::{json, Value}; -use sha1::digest::Update; -use sha1::Sha1; -use sha2::{Digest, Sha256}; use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; use tokio::net::{TcpListener, TcpStream}; use tokio::runtime::{Handle, Runtime}; @@ -29,7 +24,7 @@ use valence_nbt::{compound, Compound, List}; use crate::biome::{validate_biomes, Biome, BiomeId}; use crate::client::{Client, Clients}; -use crate::config::{Config, ServerListPing}; +use crate::config::{Config, ConnectionMode, ServerListPing}; use crate::dimension::{validate_dimensions, Dimension, DimensionId}; use crate::entity::Entities; use crate::inventory::Inventories; @@ -37,21 +32,19 @@ use crate::player_list::PlayerLists; use crate::player_textures::SignedPlayerTextures; use crate::protocol::codec::{Decoder, Encoder}; use crate::protocol::packets::c2s::handshake::{Handshake, HandshakeNextState}; -use crate::protocol::packets::c2s::login::{EncryptionResponse, LoginStart, VerifyTokenOrMsgSig}; +use crate::protocol::packets::c2s::login::LoginStart; use crate::protocol::packets::c2s::play::C2sPlayPacket; use crate::protocol::packets::c2s::status::{PingRequest, StatusRequest}; -use crate::protocol::packets::s2c::login::{ - DisconnectLogin, EncryptionRequest, LoginSuccess, SetCompression, -}; +use crate::protocol::packets::s2c::login::{DisconnectLogin, LoginSuccess, SetCompression}; use crate::protocol::packets::s2c::play::S2cPlayPacket; use crate::protocol::packets::s2c::status::{PingResponse, StatusResponse}; -use crate::protocol::packets::Property; -use crate::protocol::{BoundedArray, BoundedString, VarInt}; -use crate::text::Text; +use crate::protocol::{BoundedString, VarInt}; use crate::util::valid_username; use crate::world::Worlds; use crate::{ident, Ticks, PROTOCOL_VERSION, VERSION_NAME}; +mod login; + /// Contains the entire state of a running Minecraft server, accessible from /// within the [update](crate::config::Config::update) loop. pub struct Server { @@ -89,7 +82,7 @@ struct SharedServerInner { cfg: C, address: SocketAddr, tick_rate: Ticks, - online_mode: bool, + connection_mode: ConnectionMode, max_connections: usize, incoming_packet_capacity: usize, outgoing_packet_capacity: usize, @@ -132,7 +125,7 @@ pub struct NewClientData { /// have a skin or cape. pub textures: Option, /// The remote address of the new client. - pub remote_addr: SocketAddr, + pub remote_addr: IpAddr, } struct NewClientMessage { @@ -178,9 +171,9 @@ impl SharedServer { self.0.tick_rate } - /// Gets whether online mode is enabled on this server. - pub fn online_mode(&self) -> bool { - self.0.online_mode + /// Gets the connection mode of the server. + pub fn connection_mode(&self) -> &ConnectionMode { + &self.0.connection_mode } /// Gets the maximum number of connections allowed to the server at once. @@ -311,7 +304,7 @@ fn setup_server(cfg: C) -> anyhow::Result> { ensure!(tick_rate > 0, "tick rate must be greater than zero"); - let online_mode = cfg.online_mode(); + let connection_mode = cfg.connection_mode(); let incoming_packet_capacity = cfg.incoming_packet_capacity(); @@ -360,7 +353,7 @@ fn setup_server(cfg: C) -> anyhow::Result> { cfg, address, tick_rate, - online_mode, + connection_mode, max_connections, incoming_packet_capacity, outgoing_packet_capacity, @@ -539,7 +532,13 @@ async fn handle_connection( // TODO: peek stream for 0xFE legacy ping - let handshake = c.dec.read_packet::().await?; + let handshake: Handshake = c.dec.read_packet().await?; + + ensure!( + matches!(server.connection_mode(), ConnectionMode::BungeeCord) + || handshake.server_address.chars().count() <= 255, + "handshake server address is too long" + ); match handshake.next_state { HandshakeNextState::Status => handle_status(server, &mut c, remote_addr, handshake) @@ -615,9 +614,9 @@ async fn handle_status( Ok(()) } -/// Handle the login process and return the new player's data if successful. -async fn handle_login( - server: &SharedServer, +/// Handle the login process and return the new client's data if successful. +async fn handle_login( + server: &SharedServer, c: &mut Codec, remote_addr: SocketAddr, handshake: Handshake, @@ -635,109 +634,11 @@ async fn handle_login( ensure!(valid_username(&username), "invalid username '{username}'"); - let (uuid, textures) = if server.0.online_mode { - let my_verify_token: [u8; 16] = rand::random(); - - c.enc - .write_packet(&EncryptionRequest { - server_id: Default::default(), // Always empty - public_key: server.0.public_key_der.to_vec(), - verify_token: my_verify_token.to_vec().into(), - }) - .await?; - - let EncryptionResponse { - shared_secret: BoundedArray(encrypted_shared_secret), - token_or_sig, - } = c.dec.read_packet().await?; - - let shared_secret = server - .0 - .rsa_key - .decrypt(PaddingScheme::PKCS1v15Encrypt, &encrypted_shared_secret) - .context("failed to decrypt shared secret")?; - - let _opt_signature = match token_or_sig { - VerifyTokenOrMsgSig::VerifyToken(BoundedArray(encrypted_verify_token)) => { - let verify_token = server - .0 - .rsa_key - .decrypt(PaddingScheme::PKCS1v15Encrypt, &encrypted_verify_token) - .context("failed to decrypt verify token")?; - - ensure!( - my_verify_token.as_slice() == verify_token, - "verify tokens do not match" - ); - None - } - VerifyTokenOrMsgSig::MsgSig(sig) => Some(sig), - }; - - let crypt_key: [u8; 16] = shared_secret - .as_slice() - .try_into() - .context("shared secret has the wrong length")?; - - c.enc.enable_encryption(&crypt_key); - c.dec.enable_encryption(&crypt_key); - - #[derive(Debug, Deserialize)] - struct AuthResponse { - id: String, - name: String, - properties: Vec, - } - - let hash = Sha1::new() - .chain(&shared_secret) - .chain(&server.0.public_key_der) - .finalize(); - - let hex_hash = auth_digest(&hash); - - let url = C::format_session_server_url( - server.config(), - server, - &username, - &hex_hash, - &remote_addr.ip(), - ); - - let resp = server.0.http_client.get(url).send().await?; - - match resp.status() { - StatusCode::OK => {} - StatusCode::NO_CONTENT => { - let reason = Text::translate("multiplayer.disconnect.unverified_username"); - c.enc.write_packet(&DisconnectLogin { reason }).await?; - bail!("Could not verify username"); - } - status => { - bail!("session server GET request failed (status code {status})"); - } - } - - let data: AuthResponse = resp.json().await?; - - ensure!(data.name == username, "usernames do not match"); - - let uuid = Uuid::parse_str(&data.id).context("failed to parse player's UUID")?; - - let textures = match data.properties.into_iter().find(|p| p.name == "textures") { - Some(p) => SignedPlayerTextures::from_base64( - p.value, - p.signature.context("missing signature for textures")?, - )?, - None => bail!("failed to find textures in auth response"), - }; - - (uuid, Some(textures)) - } else { - // Derive the player's UUID from a hash of their username. - let uuid = Uuid::from_slice(&Sha256::digest(&username)[..16]).unwrap(); - - (uuid, None) + let ncd = match server.connection_mode() { + ConnectionMode::Online => login::online(server, c, remote_addr, username).await?, + ConnectionMode::Offline => login::offline(remote_addr, username)?, + ConnectionMode::BungeeCord => login::bungeecord(&handshake.server_address, username)?, + ConnectionMode::Velocity { secret } => login::velocity(c, username, secret).await?, }; let compression_threshold = 256; @@ -750,13 +651,6 @@ async fn handle_login( c.enc.enable_compression(compression_threshold); c.dec.enable_compression(compression_threshold); - let ncd = NewClientData { - uuid, - username, - textures, - remote_addr, - }; - if let Err(reason) = server.0.cfg.login(server, &ncd).await { log::info!("Disconnect at login: \"{reason}\""); c.enc.write_packet(&DisconnectLogin { reason }).await?; @@ -825,28 +719,3 @@ async fn handle_play( Ok(()) } - -fn auth_digest(bytes: &[u8]) -> String { - BigInt::from_signed_bytes_be(bytes).to_str_radix(16) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn auth_digest_correct() { - assert_eq!( - auth_digest(&Sha1::digest("Notch")), - "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48" - ); - assert_eq!( - auth_digest(&Sha1::digest("jeb_")), - "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1" - ); - assert_eq!( - auth_digest(&Sha1::digest("simon")), - "88e16a1019277b15d58faf0541e11910eb756f6" - ); - } -} diff --git a/src/server/login.rs b/src/server/login.rs new file mode 100644 index 0000000..943d6b4 --- /dev/null +++ b/src/server/login.rs @@ -0,0 +1,302 @@ +//! Contains login procedures for the different [`ConnectionMode`]s. +//! +//! [`ConnectionMode`]: crate::config::ConnectionMode + +use std::net::SocketAddr; + +use anyhow::{anyhow, bail, ensure, Context}; +use hmac::digest::Update; +use hmac::{Hmac, Mac}; +use num::BigInt; +use reqwest::StatusCode; +use rsa::PaddingScheme; +use serde::Deserialize; +use sha1::Sha1; +use sha2::{Digest, Sha256}; +use uuid::Uuid; + +use crate::config::Config; +use crate::ident; +use crate::player_textures::SignedPlayerTextures; +use crate::protocol::packets::c2s::login::{ + EncryptionResponse, LoginPluginResponse, VerifyTokenOrMsgSig, +}; +use crate::protocol::packets::s2c::login::{ + DisconnectLogin, EncryptionRequest, LoginPluginRequest, +}; +use crate::protocol::packets::Property; +use crate::protocol::{BoundedArray, BoundedString, Decode, RawBytes, VarInt}; +use crate::server::{Codec, NewClientData, SharedServer}; +use crate::text::Text; + +/// Login sequence for +/// [`ConnectionMode::Online`](crate::config::ConnectionMode). +pub(super) async fn online( + server: &SharedServer, + c: &mut Codec, + remote_addr: SocketAddr, + username: String, +) -> anyhow::Result { + let my_verify_token: [u8; 16] = rand::random(); + + c.enc + .write_packet(&EncryptionRequest { + server_id: Default::default(), // Always empty + public_key: server.0.public_key_der.to_vec(), + verify_token: my_verify_token.to_vec().into(), + }) + .await?; + + let EncryptionResponse { + shared_secret: BoundedArray(encrypted_shared_secret), + token_or_sig, + } = c.dec.read_packet().await?; + + let shared_secret = server + .0 + .rsa_key + .decrypt(PaddingScheme::PKCS1v15Encrypt, &encrypted_shared_secret) + .context("failed to decrypt shared secret")?; + + let _opt_signature = match token_or_sig { + VerifyTokenOrMsgSig::VerifyToken(BoundedArray(encrypted_verify_token)) => { + let verify_token = server + .0 + .rsa_key + .decrypt(PaddingScheme::PKCS1v15Encrypt, &encrypted_verify_token) + .context("failed to decrypt verify token")?; + + ensure!( + my_verify_token.as_slice() == verify_token, + "verify tokens do not match" + ); + None + } + VerifyTokenOrMsgSig::MsgSig(sig) => Some(sig), + }; + + let crypt_key: [u8; 16] = shared_secret + .as_slice() + .try_into() + .context("shared secret has the wrong length")?; + + c.enc.enable_encryption(&crypt_key); + c.dec.enable_encryption(&crypt_key); + + #[derive(Debug, Deserialize)] + struct AuthResponse { + id: String, + name: String, + properties: Vec, + } + + let hash = Sha1::new() + .chain(&shared_secret) + .chain(&server.0.public_key_der) + .finalize(); + + let url = server.config().format_session_server_url( + server, + &username, + &auth_digest(&hash), + &remote_addr.ip(), + ); + + let resp = server.0.http_client.get(url).send().await?; + + match resp.status() { + StatusCode::OK => {} + StatusCode::NO_CONTENT => { + let reason = Text::translate("multiplayer.disconnect.unverified_username"); + c.enc.write_packet(&DisconnectLogin { reason }).await?; + bail!("session server could not verify username"); + } + status => { + bail!("session server GET request failed (status code {status})"); + } + } + + let data: AuthResponse = resp.json().await?; + + ensure!(data.name == username, "usernames do not match"); + + let uuid = Uuid::parse_str(&data.id).context("failed to parse player's UUID")?; + + let textures = match data.properties.into_iter().find(|p| p.name == "textures") { + Some(p) => SignedPlayerTextures::from_base64( + p.value, + p.signature.context("missing signature for textures")?, + )?, + None => bail!("failed to find textures in auth response"), + }; + + Ok(NewClientData { + uuid, + username, + textures: Some(textures), + remote_addr: remote_addr.ip(), + }) +} + +/// Login sequence for +/// [`ConnectionMode::Offline`](crate::config::ConnectionMode). +pub(super) fn offline(remote_addr: SocketAddr, username: String) -> anyhow::Result { + Ok(NewClientData { + // Derive the client's UUID from a hash of their username. + uuid: Uuid::from_slice(&Sha256::digest(&username)[..16])?, + username, + textures: None, + remote_addr: remote_addr.ip(), + }) +} + +/// Login sequence for +/// [`ConnectionMode::BungeeCord`](crate::config::ConnectionMode). +pub(super) fn bungeecord(server_address: &str, username: String) -> anyhow::Result { + // Get data from server_address field of the handshake + let [_, client_ip, uuid, properties]: [&str; 4] = server_address + .split('\0') + .take(4) + .collect::>() + .try_into() + .map_err(|_| anyhow!("malformed BungeeCord server address data"))?; + + // Read properties and get textures + let properties: Vec = + serde_json::from_str(properties).context("failed to parse BungeeCord player properties")?; + + let mut textures = None; + for prop in properties { + if prop.name == "textures" { + textures = Some( + SignedPlayerTextures::from_base64( + prop.value, + prop.signature + .context("missing player textures signature")?, + ) + .context("failed to parse signed player textures")?, + ); + break; + } + } + + Ok(NewClientData { + uuid: uuid.parse()?, + username, + textures, + remote_addr: client_ip.parse()?, + }) +} + +fn auth_digest(bytes: &[u8]) -> String { + BigInt::from_signed_bytes_be(bytes).to_str_radix(16) +} + +pub(super) async fn velocity( + c: &mut Codec, + username: String, + velocity_secret: &str, +) -> anyhow::Result { + const VELOCITY_MIN_SUPPORTED_VERSION: u8 = 1; + const VELOCITY_MODERN_FORWARDING_WITH_KEY_V2: i32 = 3; + + let message_id = 0; + + // Send Player Info Request into the Plugin Channel + c.enc + .write_packet(&LoginPluginRequest { + message_id: VarInt(message_id), + channel: ident!("velocity:player_info"), + data: RawBytes(vec![VELOCITY_MIN_SUPPORTED_VERSION]), + }) + .await?; + + // Get Response + let plugin_response: LoginPluginResponse = c.dec.read_packet().await?; + + ensure!( + plugin_response.message_id.0 == message_id, + "mismatched plugin response ID (got {}, expected {message_id})", + plugin_response.message_id.0, + ); + + let data = plugin_response + .data + .context("missing plugin response data")? + .0; + + ensure!(data.len() >= 32, "invalid plugin response data length"); + let (signature, mut data_without_signature) = data.split_at(32); + + // Verify signature + let mut mac = Hmac::::new_from_slice(velocity_secret.as_bytes())?; + Mac::update(&mut mac, data_without_signature); + mac.verify_slice(signature)?; + + // Check Velocity version + let version = VarInt::decode(&mut data_without_signature) + .context("failed to decode velocity version")? + .0; + + // Get client address + let remote_addr = String::decode(&mut data_without_signature)?.parse()?; + + // Get UUID + let uuid = Uuid::decode(&mut data_without_signature)?; + + // Get username and validate + let velocity_username = BoundedString::<0, 16>::decode(&mut data_without_signature)?.0; + ensure!(username == velocity_username, "mismatched usernames"); + + // Read properties and get textures + let mut textures = None; + for prop in Vec::::decode(&mut data_without_signature) + .context("failed to decode velocity player properties")? + { + if prop.name == "textures" { + textures = Some( + SignedPlayerTextures::from_base64( + prop.value, + prop.signature + .context("missing player textures signature")?, + ) + .context("failed to parse signed player textures")?, + ); + break; + } + } + + if version >= VELOCITY_MODERN_FORWARDING_WITH_KEY_V2 { + // TODO + } + + Ok(NewClientData { + uuid, + username, + textures, + remote_addr, + }) +} + +#[cfg(test)] +mod tests { + use sha1::Digest; + + use super::*; + + #[test] + fn auth_digest_correct() { + assert_eq!( + auth_digest(&Sha1::digest("Notch")), + "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48" + ); + assert_eq!( + auth_digest(&Sha1::digest("jeb_")), + "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1" + ); + assert_eq!( + auth_digest(&Sha1::digest("simon")), + "88e16a1019277b15d58faf0541e11910eb756f6" + ); + } +}