Proxy Support (#48)

This PR adds Proxy Support.

Closes #39

Co-authored-by: Ryan Johnson <ryanj00a@gmail.com>
This commit is contained in:
Tert0 2022-10-22 04:50:13 +02:00 committed by GitHub
parent 4253928b4d
commit c707ed1d04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 455 additions and 185 deletions

View file

@ -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"

View file

@ -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

View file

@ -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<Dimension> {
vec![Dimension {
fixed_time: Some(6000),

View file

@ -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 {

View file

@ -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<C: Config> Client<C> {
self.msgs_to_send.push(msg.into());
}
pub fn send_plugin_message(&mut self, channel: Ident<String>, data: Vec<u8>) {
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<f64> {
@ -930,7 +940,6 @@ impl<C: Config> Client<C> {
window_id: c.window_id,
})
}
C2sPlayPacket::PluginMessageC2s(_) => {}
C2sPlayPacket::EditBook(_) => {}
C2sPlayPacket::QueryEntityTag(_) => {}
C2sPlayPacket::Interact(p) => {
@ -1107,6 +1116,12 @@ impl<C: Config> Client<C> {
})
}
}
C2sPlayPacket::PluginMessageC2s(p) => {
self.events.push_back(ClientEvent::PluginMessageReceived {
channel: p.channel,
data: p.data,
});
}
C2sPlayPacket::ProgramJigsawBlock(_) => {}
C2sPlayPacket::ProgramStructureBlock(_) => {}
C2sPlayPacket::UpdateSign(_) => {}

View file

@ -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<String>,
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<C: Config>(
ClientEvent::SteerBoat { .. } => {}
ClientEvent::Digging { .. } => {}
ClientEvent::InteractWithBlock { .. } => {}
ClientEvent::PluginMessageReceived { .. } => {}
ClientEvent::ResourcePackStatusChanged(_) => {}
ClientEvent::CloseScreen { window_id } => {
if let Some(window) = &client.open_inventory {

View file

@ -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<Self>) {}
/// 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> {

View file

@ -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.
///

View file

@ -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,
}

View file

@ -275,6 +275,13 @@ pub mod play {
}
}
def_struct! {
PluginMessageS2c {
channel: Ident<String>,
data: RawBytes,
}
}
def_struct! {
CustomSoundEffect {
name: Ident<String>,
@ -738,6 +745,7 @@ pub mod play {
BlockUpdate = 9,
BossBar = 10,
ClearTitles = 13,
PluginMessageS2c = 22,
SetContainerContent = 17,
SetContainerProperty = 18,
SetContainerSlot = 19,

View file

@ -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<C: Config> {
@ -89,7 +82,7 @@ struct SharedServerInner<C: Config> {
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<SignedPlayerTextures>,
/// The remote address of the new client.
pub remote_addr: SocketAddr,
pub remote_addr: IpAddr,
}
struct NewClientMessage {
@ -178,9 +171,9 @@ impl<C: Config> SharedServer<C> {
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<C: Config>(cfg: C) -> anyhow::Result<SharedServer<C>> {
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<C: Config>(cfg: C) -> anyhow::Result<SharedServer<C>> {
cfg,
address,
tick_rate,
online_mode,
connection_mode,
max_connections,
incoming_packet_capacity,
outgoing_packet_capacity,
@ -539,7 +532,13 @@ async fn handle_connection<C: Config>(
// TODO: peek stream for 0xFE legacy ping
let handshake = c.dec.read_packet::<Handshake>().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<C: Config>(
Ok(())
}
/// Handle the login process and return the new player's data if successful.
async fn handle_login<C: Config>(
server: &SharedServer<C>,
/// Handle the login process and return the new client's data if successful.
async fn handle_login(
server: &SharedServer<impl Config>,
c: &mut Codec,
remote_addr: SocketAddr,
handshake: Handshake,
@ -635,109 +634,11 @@ async fn handle_login<C: Config>(
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<Property>,
}
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: Config>(
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<C: Config>(
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"
);
}
}

302
src/server/login.rs Normal file
View file

@ -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<impl Config>,
c: &mut Codec,
remote_addr: SocketAddr,
username: String,
) -> anyhow::Result<NewClientData> {
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<Property>,
}
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<NewClientData> {
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<NewClientData> {
// Get data from server_address field of the handshake
let [_, client_ip, uuid, properties]: [&str; 4] = server_address
.split('\0')
.take(4)
.collect::<Vec<_>>()
.try_into()
.map_err(|_| anyhow!("malformed BungeeCord server address data"))?;
// Read properties and get textures
let properties: Vec<Property> =
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<NewClientData> {
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::<Sha256>::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::<Property>::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"
);
}
}