Pumpkin/pumpkin/src/net/proxy/velocity.rs
Alexander Medvedev 3303bb7b97 chore(net): change get_string to get_str
changed return value from `String` to `Box<str>` which is smaller and makes more sense since we don't mutate packet data

String — 24 bytes (ptr + len + capacity)
Box<str> — 16 bytes (ptr + len)
2026-05-23 11:21:53 +02:00

148 lines
4.8 KiB
Rust

use arc_swap::ArcSwap;
use bytes::{BufMut, BytesMut};
use hmac::{Hmac, KeyInit, Mac};
use pumpkin_config::networking::proxy::VelocityConfig;
use pumpkin_protocol::{
Property, java::client::login::CLoginPluginRequest, java::server::login::SLoginPluginResponse,
ser::NetworkReadExt,
};
use rand::RngExt;
use sha2::Sha256;
use std::sync::Arc;
/// Proxy implementation for Velocity <https://papermc.io/software/velocity> by `PaperMC`
/// Sadly, `PaperMC` does not care about 3rd parties providing support for Velocity. There is no documentation.
/// I had to understand the code logic by looking at `PaperMC`'s Velocity implementation: <https://github.com/PaperMC/Paper/blob/0cf731589a3b6923542cdfc36dbcee9c47c51076/paper-server/src/main/java/com/destroystokyo/paper/proxy/VelocityProxy.java>
use std::{
io::Read,
net::{IpAddr, SocketAddr},
};
use thiserror::Error;
use tracing::debug;
use crate::net::{GameProfile, java::JavaClient};
type HmacSha256 = Hmac<Sha256>;
const MAX_SUPPORTED_FORWARDING_VERSION: u8 = 4;
const PLAYER_INFO_CHANNEL: &str = "velocity:player_info";
#[derive(Error, Debug)]
pub enum VelocityError {
#[error("No response data received")]
NoData,
#[error("Unable to verify player details")]
FailedVerifyIntegrity,
#[error("Failed to read forward version")]
FailedReadForwardVersion,
#[error("Unsupported forwarding version {0}. Maximum supported version is {1}")]
UnsupportedForwardVersion(u8, u8),
#[error("Failed to read address")]
FailedReadAddress,
#[error("Failed to parse address")]
FailedParseAddress,
#[error("Failed to read game profile name")]
FailedReadProfileName,
#[error("Failed to read game profile UUID")]
FailedReadProfileUUID,
#[error("Failed to read game profile properties")]
FailedReadProfileProperties,
}
pub async fn velocity_login(client: &JavaClient) {
// TODO: Validate the packet transaction id from the plugin response with this
let velocity_message_id: i32 = rand::rng().random();
let mut buf = BytesMut::new();
buf.put_u8(MAX_SUPPORTED_FORWARDING_VERSION);
client
.enqueue_packet(&CLoginPluginRequest::new(
velocity_message_id.into(),
PLAYER_INFO_CHANNEL,
&buf,
))
.await;
}
#[must_use]
pub fn check_integrity(data: (&[u8], &[u8]), secret: &str) -> bool {
let (signature, data_without_signature) = data;
// Our fault, we can panic/expect?
let mut mac =
HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
mac.update(data_without_signature);
mac.verify_slice(signature).is_ok()
}
fn read_game_profile(read: impl Read) -> Result<GameProfile, VelocityError> {
let mut read = read;
let id = read
.get_uuid()
.map_err(|_| VelocityError::FailedReadProfileUUID)?;
let name = read
.get_str()
.map_err(|_| VelocityError::FailedReadProfileName)?;
let properties = read
.get_list(|data| {
let name = data.get_str()?;
let value = data.get_str()?;
let signature = data.get_option(NetworkReadExt::get_str)?;
Ok(Property {
name,
value,
signature,
})
})
.map_err(|_| VelocityError::FailedReadProfileProperties)?;
Ok(GameProfile {
id,
name: name.into_string(),
properties: ArcSwap::new(Arc::from(properties)),
profile_actions: None,
})
}
pub fn receive_velocity_plugin_response(
port: u16,
config: &VelocityConfig,
response: SLoginPluginResponse,
) -> Result<(GameProfile, SocketAddr), VelocityError> {
debug!("Received velocity response");
if let Some(data) = response.data {
let (signature, mut data_without_signature) = data.split_at(32);
if !check_integrity((signature, data_without_signature), &config.secret) {
return Err(VelocityError::FailedVerifyIntegrity);
}
// Check velocity version
let version = data_without_signature
.get_var_int()
.map_err(|_| VelocityError::FailedReadForwardVersion)?;
let version = version.0 as u8;
if version > MAX_SUPPORTED_FORWARDING_VERSION {
return Err(VelocityError::UnsupportedForwardVersion(
version,
MAX_SUPPORTED_FORWARDING_VERSION,
));
}
let addr = data_without_signature
.get_str()
.map_err(|_| VelocityError::FailedReadAddress)?;
let socket_addr: SocketAddr = SocketAddr::new(
addr.parse::<IpAddr>()
.map_err(|_| VelocityError::FailedParseAddress)?,
port,
);
let profile = read_game_profile(&mut data_without_signature)?;
return Ok((profile, socket_addr));
}
Err(VelocityError::NoData)
}