mirror of
https://github.com/italicsjenga/valence.git
synced 2025-01-11 07:11:30 +11:00
Proxy Support (#48)
This PR adds Proxy Support. Closes #39 Co-authored-by: Ryan Johnson <ryanj00a@gmail.com>
This commit is contained in:
parent
4253928b4d
commit
c707ed1d04
|
@ -26,6 +26,7 @@ cfb8 = "0.7.1"
|
||||||
flate2 = "1.0.24"
|
flate2 = "1.0.24"
|
||||||
flume = "0.10.14"
|
flume = "0.10.14"
|
||||||
futures = "0.3.24"
|
futures = "0.3.24"
|
||||||
|
hmac = "0.12.1"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
num = "0.4.0"
|
num = "0.4.0"
|
||||||
paste = "1.0.9"
|
paste = "1.0.9"
|
||||||
|
|
|
@ -43,7 +43,7 @@ place. Here are some noteworthy achievements:
|
||||||
- [x] Items
|
- [x] Items
|
||||||
- [ ] Inventory
|
- [ ] Inventory
|
||||||
- [ ] Block entities
|
- [ ] 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.
|
- [ ] Sounds, particles, etc.
|
||||||
- [ ] Utilities for continuous collision detection
|
- [ ] Utilities for continuous collision detection
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::net::SocketAddr;
|
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
|
@ -66,6 +66,10 @@ impl Config for Game {
|
||||||
MAX_PLAYERS + 64
|
MAX_PLAYERS + 64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn address(&self) -> SocketAddr {
|
||||||
|
SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 25565).into() // TODO remove
|
||||||
|
}
|
||||||
|
|
||||||
fn dimensions(&self) -> Vec<Dimension> {
|
fn dimensions(&self) -> Vec<Dimension> {
|
||||||
vec![Dimension {
|
vec![Dimension {
|
||||||
fixed_time: Some(6000),
|
fixed_time: Some(6000),
|
||||||
|
|
|
@ -44,8 +44,8 @@ impl Config for Game {
|
||||||
MAX_PLAYERS + 64
|
MAX_PLAYERS + 64
|
||||||
}
|
}
|
||||||
|
|
||||||
fn online_mode(&self) -> bool {
|
fn connection_mode(&self) -> ConnectionMode {
|
||||||
false
|
ConnectionMode::Offline
|
||||||
}
|
}
|
||||||
|
|
||||||
fn outgoing_packet_capacity(&self) -> usize {
|
fn outgoing_packet_capacity(&self) -> usize {
|
||||||
|
|
|
@ -34,8 +34,8 @@ pub use crate::protocol::packets::s2c::play::SetTitleAnimationTimes;
|
||||||
use crate::protocol::packets::s2c::play::{
|
use crate::protocol::packets::s2c::play::{
|
||||||
AcknowledgeBlockChange, ClearTitles, CombatDeath, CustomSoundEffect, DisconnectPlay,
|
AcknowledgeBlockChange, ClearTitles, CombatDeath, CustomSoundEffect, DisconnectPlay,
|
||||||
EntityAnimationS2c, EntityAttributesProperty, EntityEvent, GameEvent, GameStateChangeReason,
|
EntityAnimationS2c, EntityAttributesProperty, EntityEvent, GameEvent, GameStateChangeReason,
|
||||||
KeepAliveS2c, LoginPlay, OpenScreen, PlayerPositionLookFlags, RemoveEntities, ResourcePackS2c,
|
KeepAliveS2c, LoginPlay, OpenScreen, PlayerPositionLookFlags, PluginMessageS2c, RemoveEntities,
|
||||||
Respawn, S2cPlayPacket, SetActionBarText, SetCenterChunk, SetContainerContent,
|
ResourcePackS2c, Respawn, S2cPlayPacket, SetActionBarText, SetCenterChunk, SetContainerContent,
|
||||||
SetDefaultSpawnPosition, SetEntityMetadata, SetEntityVelocity, SetExperience, SetHeadRotation,
|
SetDefaultSpawnPosition, SetEntityMetadata, SetEntityVelocity, SetExperience, SetHeadRotation,
|
||||||
SetHealth, SetRenderDistance, SetSubtitleText, SetTitleText, SoundCategory,
|
SetHealth, SetRenderDistance, SetSubtitleText, SetTitleText, SoundCategory,
|
||||||
SynchronizePlayerPosition, SystemChatMessage, TeleportEntity, UnloadChunk, UpdateAttributes,
|
SynchronizePlayerPosition, SystemChatMessage, TeleportEntity, UnloadChunk, UpdateAttributes,
|
||||||
|
@ -390,6 +390,16 @@ impl<C: Config> Client<C> {
|
||||||
self.msgs_to_send.push(msg.into());
|
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
|
/// Gets the absolute position of this client in the world it is located
|
||||||
/// in.
|
/// in.
|
||||||
pub fn position(&self) -> Vec3<f64> {
|
pub fn position(&self) -> Vec3<f64> {
|
||||||
|
@ -930,7 +940,6 @@ impl<C: Config> Client<C> {
|
||||||
window_id: c.window_id,
|
window_id: c.window_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
C2sPlayPacket::PluginMessageC2s(_) => {}
|
|
||||||
C2sPlayPacket::EditBook(_) => {}
|
C2sPlayPacket::EditBook(_) => {}
|
||||||
C2sPlayPacket::QueryEntityTag(_) => {}
|
C2sPlayPacket::QueryEntityTag(_) => {}
|
||||||
C2sPlayPacket::Interact(p) => {
|
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::ProgramJigsawBlock(_) => {}
|
||||||
C2sPlayPacket::ProgramStructureBlock(_) => {}
|
C2sPlayPacket::ProgramStructureBlock(_) => {}
|
||||||
C2sPlayPacket::UpdateSign(_) => {}
|
C2sPlayPacket::UpdateSign(_) => {}
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::block_pos::BlockPos;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::entity::types::Pose;
|
use crate::entity::types::Pose;
|
||||||
use crate::entity::{Entity, EntityEvent, EntityId, TrackedData};
|
use crate::entity::{Entity, EntityEvent, EntityId, TrackedData};
|
||||||
|
use crate::ident::Ident;
|
||||||
use crate::inventory::Inventory;
|
use crate::inventory::Inventory;
|
||||||
use crate::item::ItemStack;
|
use crate::item::ItemStack;
|
||||||
use crate::protocol::packets::c2s::play::ClickContainerMode;
|
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,
|
BlockFace, ChatMode, DisplayedSkinParts, Hand, MainHand, ResourcePackC2s as ResourcePackStatus,
|
||||||
};
|
};
|
||||||
pub use crate::protocol::packets::s2c::play::GameMode;
|
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.
|
/// Represents an action performed by a client.
|
||||||
///
|
///
|
||||||
|
@ -131,6 +132,10 @@ pub enum ClientEvent {
|
||||||
/// Sequence number
|
/// Sequence number
|
||||||
sequence: VarInt,
|
sequence: VarInt,
|
||||||
},
|
},
|
||||||
|
PluginMessageReceived {
|
||||||
|
channel: Ident<String>,
|
||||||
|
data: RawBytes,
|
||||||
|
},
|
||||||
ResourcePackStatusChanged(ResourcePackStatus),
|
ResourcePackStatusChanged(ResourcePackStatus),
|
||||||
/// The client closed a screen. This occurs when the client closes their
|
/// The client closed a screen. This occurs when the client closes their
|
||||||
/// inventory, closes a chest inventory, etc.
|
/// inventory, closes a chest inventory, etc.
|
||||||
|
@ -331,6 +336,7 @@ pub fn handle_event_default<C: Config>(
|
||||||
ClientEvent::SteerBoat { .. } => {}
|
ClientEvent::SteerBoat { .. } => {}
|
||||||
ClientEvent::Digging { .. } => {}
|
ClientEvent::Digging { .. } => {}
|
||||||
ClientEvent::InteractWithBlock { .. } => {}
|
ClientEvent::InteractWithBlock { .. } => {}
|
||||||
|
ClientEvent::PluginMessageReceived { .. } => {}
|
||||||
ClientEvent::ResourcePackStatusChanged(_) => {}
|
ClientEvent::ResourcePackStatusChanged(_) => {}
|
||||||
ClientEvent::CloseScreen { window_id } => {
|
ClientEvent::CloseScreen { window_id } => {
|
||||||
if let Some(window) = &client.open_inventory {
|
if let Some(window) = &client.open_inventory {
|
||||||
|
|
|
@ -77,21 +77,15 @@ pub trait Config: Sized + Send + Sync + 'static {
|
||||||
STANDARD_TPS
|
STANDARD_TPS
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called once at startup to get the "online mode" option, which determines
|
/// Called once at startup to get the connection mode option, which
|
||||||
/// if client authentication and encryption should take place.
|
/// determines if client authentication and encryption should take place
|
||||||
///
|
/// and if the server should get the player data from a proxy.
|
||||||
/// 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.
|
|
||||||
///
|
///
|
||||||
/// # Default Implementation
|
/// # Default Implementation
|
||||||
///
|
///
|
||||||
/// Returns `true`.
|
/// Returns [`ConnectionMode::Online`].
|
||||||
fn online_mode(&self) -> bool {
|
fn connection_mode(&self) -> ConnectionMode {
|
||||||
true
|
ConnectionMode::Online
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called once at startup to get the "prevent-proxy-connections" option,
|
/// 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.
|
/// no connections to the server will be made until this function returns.
|
||||||
///
|
///
|
||||||
/// This method is called from within a tokio runtime.
|
/// This method is called from within a tokio runtime.
|
||||||
|
///
|
||||||
|
/// # Default Implementation
|
||||||
|
///
|
||||||
|
/// The default implementation does nothing.
|
||||||
fn init(&self, server: &mut Server<Self>) {}
|
fn init(&self, server: &mut Server<Self>) {}
|
||||||
|
|
||||||
/// Called once at the beginning of every server update (also known as
|
/// Called once at the beginning of every server update (also known as
|
||||||
|
@ -298,6 +296,61 @@ pub enum ServerListPing<'a> {
|
||||||
Ignore,
|
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.
|
/// Represents an individual entry in the player sample.
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct PlayerSampleEntry<'a> {
|
pub struct PlayerSampleEntry<'a> {
|
||||||
|
|
12
src/lib.rs
12
src/lib.rs
|
@ -133,7 +133,7 @@ pub mod prelude {
|
||||||
pub use block::{BlockKind, BlockPos, BlockState, PropName, PropValue};
|
pub use block::{BlockKind, BlockPos, BlockState, PropName, PropValue};
|
||||||
pub use chunk::{Chunk, ChunkPos, Chunks, LoadedChunk, UnloadedChunk};
|
pub use chunk::{Chunk, ChunkPos, Chunks, LoadedChunk, UnloadedChunk};
|
||||||
pub use client::{handle_event_default, Client, ClientEvent, ClientId, Clients, GameMode};
|
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 dimension::{Dimension, DimensionId};
|
||||||
pub use entity::{Entities, Entity, EntityEvent, EntityId, EntityKind, TrackedData};
|
pub use entity::{Entities, Entity, EntityEvent, EntityId, EntityKind, TrackedData};
|
||||||
pub use ident::{Ident, IdentError};
|
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.
|
/// You should avoid using this namespace in your own identifiers.
|
||||||
pub const LIBRARY_NAMESPACE: &str = "valence";
|
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
|
/// A discrete unit of time where 1 tick is the duration of a
|
||||||
/// single game update.
|
/// single game update.
|
||||||
///
|
///
|
||||||
|
|
|
@ -8,7 +8,9 @@ pub mod handshake {
|
||||||
def_struct! {
|
def_struct! {
|
||||||
Handshake {
|
Handshake {
|
||||||
protocol_version: VarInt,
|
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,
|
server_port: u16,
|
||||||
next_state: HandshakeNextState,
|
next_state: HandshakeNextState,
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,6 +275,13 @@ pub mod play {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def_struct! {
|
||||||
|
PluginMessageS2c {
|
||||||
|
channel: Ident<String>,
|
||||||
|
data: RawBytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def_struct! {
|
def_struct! {
|
||||||
CustomSoundEffect {
|
CustomSoundEffect {
|
||||||
name: Ident<String>,
|
name: Ident<String>,
|
||||||
|
@ -738,6 +745,7 @@ pub mod play {
|
||||||
BlockUpdate = 9,
|
BlockUpdate = 9,
|
||||||
BossBar = 10,
|
BossBar = 10,
|
||||||
ClearTitles = 13,
|
ClearTitles = 13,
|
||||||
|
PluginMessageS2c = 22,
|
||||||
SetContainerContent = 17,
|
SetContainerContent = 17,
|
||||||
SetContainerProperty = 18,
|
SetContainerProperty = 18,
|
||||||
SetContainerSlot = 19,
|
SetContainerSlot = 19,
|
||||||
|
|
195
src/server.rs
195
src/server.rs
|
@ -2,24 +2,19 @@
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::iter::FusedIterator;
|
use std::iter::FusedIterator;
|
||||||
use std::net::SocketAddr;
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::sync::atomic::{AtomicI64, Ordering};
|
use std::sync::atomic::{AtomicI64, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{io, thread};
|
use std::{io, thread};
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Context};
|
use anyhow::{ensure, Context};
|
||||||
use flume::{Receiver, Sender};
|
use flume::{Receiver, Sender};
|
||||||
use num::BigInt;
|
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use rayon::iter::ParallelIterator;
|
use rayon::iter::ParallelIterator;
|
||||||
use reqwest::{Client as HttpClient, StatusCode};
|
use reqwest::Client as HttpClient;
|
||||||
use rsa::{PaddingScheme, PublicKeyParts, RsaPrivateKey};
|
use rsa::{PublicKeyParts, RsaPrivateKey};
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::{json, Value};
|
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::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio::runtime::{Handle, Runtime};
|
use tokio::runtime::{Handle, Runtime};
|
||||||
|
@ -29,7 +24,7 @@ use valence_nbt::{compound, Compound, List};
|
||||||
|
|
||||||
use crate::biome::{validate_biomes, Biome, BiomeId};
|
use crate::biome::{validate_biomes, Biome, BiomeId};
|
||||||
use crate::client::{Client, Clients};
|
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::dimension::{validate_dimensions, Dimension, DimensionId};
|
||||||
use crate::entity::Entities;
|
use crate::entity::Entities;
|
||||||
use crate::inventory::Inventories;
|
use crate::inventory::Inventories;
|
||||||
|
@ -37,21 +32,19 @@ use crate::player_list::PlayerLists;
|
||||||
use crate::player_textures::SignedPlayerTextures;
|
use crate::player_textures::SignedPlayerTextures;
|
||||||
use crate::protocol::codec::{Decoder, Encoder};
|
use crate::protocol::codec::{Decoder, Encoder};
|
||||||
use crate::protocol::packets::c2s::handshake::{Handshake, HandshakeNextState};
|
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::play::C2sPlayPacket;
|
||||||
use crate::protocol::packets::c2s::status::{PingRequest, StatusRequest};
|
use crate::protocol::packets::c2s::status::{PingRequest, StatusRequest};
|
||||||
use crate::protocol::packets::s2c::login::{
|
use crate::protocol::packets::s2c::login::{DisconnectLogin, LoginSuccess, SetCompression};
|
||||||
DisconnectLogin, EncryptionRequest, LoginSuccess, SetCompression,
|
|
||||||
};
|
|
||||||
use crate::protocol::packets::s2c::play::S2cPlayPacket;
|
use crate::protocol::packets::s2c::play::S2cPlayPacket;
|
||||||
use crate::protocol::packets::s2c::status::{PingResponse, StatusResponse};
|
use crate::protocol::packets::s2c::status::{PingResponse, StatusResponse};
|
||||||
use crate::protocol::packets::Property;
|
use crate::protocol::{BoundedString, VarInt};
|
||||||
use crate::protocol::{BoundedArray, BoundedString, VarInt};
|
|
||||||
use crate::text::Text;
|
|
||||||
use crate::util::valid_username;
|
use crate::util::valid_username;
|
||||||
use crate::world::Worlds;
|
use crate::world::Worlds;
|
||||||
use crate::{ident, Ticks, PROTOCOL_VERSION, VERSION_NAME};
|
use crate::{ident, Ticks, PROTOCOL_VERSION, VERSION_NAME};
|
||||||
|
|
||||||
|
mod login;
|
||||||
|
|
||||||
/// Contains the entire state of a running Minecraft server, accessible from
|
/// Contains the entire state of a running Minecraft server, accessible from
|
||||||
/// within the [update](crate::config::Config::update) loop.
|
/// within the [update](crate::config::Config::update) loop.
|
||||||
pub struct Server<C: Config> {
|
pub struct Server<C: Config> {
|
||||||
|
@ -89,7 +82,7 @@ struct SharedServerInner<C: Config> {
|
||||||
cfg: C,
|
cfg: C,
|
||||||
address: SocketAddr,
|
address: SocketAddr,
|
||||||
tick_rate: Ticks,
|
tick_rate: Ticks,
|
||||||
online_mode: bool,
|
connection_mode: ConnectionMode,
|
||||||
max_connections: usize,
|
max_connections: usize,
|
||||||
incoming_packet_capacity: usize,
|
incoming_packet_capacity: usize,
|
||||||
outgoing_packet_capacity: usize,
|
outgoing_packet_capacity: usize,
|
||||||
|
@ -132,7 +125,7 @@ pub struct NewClientData {
|
||||||
/// have a skin or cape.
|
/// have a skin or cape.
|
||||||
pub textures: Option<SignedPlayerTextures>,
|
pub textures: Option<SignedPlayerTextures>,
|
||||||
/// The remote address of the new client.
|
/// The remote address of the new client.
|
||||||
pub remote_addr: SocketAddr,
|
pub remote_addr: IpAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NewClientMessage {
|
struct NewClientMessage {
|
||||||
|
@ -178,9 +171,9 @@ impl<C: Config> SharedServer<C> {
|
||||||
self.0.tick_rate
|
self.0.tick_rate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets whether online mode is enabled on this server.
|
/// Gets the connection mode of the server.
|
||||||
pub fn online_mode(&self) -> bool {
|
pub fn connection_mode(&self) -> &ConnectionMode {
|
||||||
self.0.online_mode
|
&self.0.connection_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the maximum number of connections allowed to the server at once.
|
/// 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");
|
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();
|
let incoming_packet_capacity = cfg.incoming_packet_capacity();
|
||||||
|
|
||||||
|
@ -360,7 +353,7 @@ fn setup_server<C: Config>(cfg: C) -> anyhow::Result<SharedServer<C>> {
|
||||||
cfg,
|
cfg,
|
||||||
address,
|
address,
|
||||||
tick_rate,
|
tick_rate,
|
||||||
online_mode,
|
connection_mode,
|
||||||
max_connections,
|
max_connections,
|
||||||
incoming_packet_capacity,
|
incoming_packet_capacity,
|
||||||
outgoing_packet_capacity,
|
outgoing_packet_capacity,
|
||||||
|
@ -539,7 +532,13 @@ async fn handle_connection<C: Config>(
|
||||||
|
|
||||||
// TODO: peek stream for 0xFE legacy ping
|
// 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 {
|
match handshake.next_state {
|
||||||
HandshakeNextState::Status => handle_status(server, &mut c, remote_addr, handshake)
|
HandshakeNextState::Status => handle_status(server, &mut c, remote_addr, handshake)
|
||||||
|
@ -615,9 +614,9 @@ async fn handle_status<C: Config>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle the login process and return the new player's data if successful.
|
/// Handle the login process and return the new client's data if successful.
|
||||||
async fn handle_login<C: Config>(
|
async fn handle_login(
|
||||||
server: &SharedServer<C>,
|
server: &SharedServer<impl Config>,
|
||||||
c: &mut Codec,
|
c: &mut Codec,
|
||||||
remote_addr: SocketAddr,
|
remote_addr: SocketAddr,
|
||||||
handshake: Handshake,
|
handshake: Handshake,
|
||||||
|
@ -635,109 +634,11 @@ async fn handle_login<C: Config>(
|
||||||
|
|
||||||
ensure!(valid_username(&username), "invalid username '{username}'");
|
ensure!(valid_username(&username), "invalid username '{username}'");
|
||||||
|
|
||||||
let (uuid, textures) = if server.0.online_mode {
|
let ncd = match server.connection_mode() {
|
||||||
let my_verify_token: [u8; 16] = rand::random();
|
ConnectionMode::Online => login::online(server, c, remote_addr, username).await?,
|
||||||
|
ConnectionMode::Offline => login::offline(remote_addr, username)?,
|
||||||
c.enc
|
ConnectionMode::BungeeCord => login::bungeecord(&handshake.server_address, username)?,
|
||||||
.write_packet(&EncryptionRequest {
|
ConnectionMode::Velocity { secret } => login::velocity(c, username, secret).await?,
|
||||||
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 compression_threshold = 256;
|
let compression_threshold = 256;
|
||||||
|
@ -750,13 +651,6 @@ async fn handle_login<C: Config>(
|
||||||
c.enc.enable_compression(compression_threshold);
|
c.enc.enable_compression(compression_threshold);
|
||||||
c.dec.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 {
|
if let Err(reason) = server.0.cfg.login(server, &ncd).await {
|
||||||
log::info!("Disconnect at login: \"{reason}\"");
|
log::info!("Disconnect at login: \"{reason}\"");
|
||||||
c.enc.write_packet(&DisconnectLogin { reason }).await?;
|
c.enc.write_packet(&DisconnectLogin { reason }).await?;
|
||||||
|
@ -825,28 +719,3 @@ async fn handle_play<C: Config>(
|
||||||
|
|
||||||
Ok(())
|
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
302
src/server/login.rs
Normal 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue