mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 14:31: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"
|
||||
flume = "0.10.14"
|
||||
futures = "0.3.24"
|
||||
hmac = "0.12.1"
|
||||
log = "0.4.17"
|
||||
num = "0.4.0"
|
||||
paste = "1.0.9"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(_) => {}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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> {
|
||||
|
|
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 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.
|
||||
///
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
195
src/server.rs
195
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<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
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