Files
Pumpkin/pumpkin/src/net/java/login.rs
2026-05-06 11:49:20 +02:00

404 lines
14 KiB
Rust

use pumpkin_data::translation;
use pumpkin_protocol::{
ConnectionState, KnownPack, Label, Link, LinkType,
java::client::{
config::{CConfigAddResourcePack, CConfigServerLinks, CKnownPacks},
login::{CLoginSuccess, CSetCompression},
},
java::server::login::{
SEncryptionResponse, SLoginCookieResponse, SLoginPluginResponse, SLoginStart,
},
};
use pumpkin_util::{text::TextComponent, version::MinecraftVersion};
use tracing::debug;
use uuid::Uuid;
use crate::{
net::{
GameProfile,
authentication::{self, AuthError},
is_valid_player_name,
java::JavaClient,
offline_uuid,
proxy::{bungeecord, velocity},
},
server::Server,
};
impl JavaClient {
pub async fn handle_login_start(&self, server: &Server, login_start: SLoginStart) {
debug!("login start");
// Don't allow new logons when the server is full.
// If `max_players` is set to zero, then there is no max player count enforced.
// TODO: If client is an operator or has otherwise suitable elevated permissions, allow the client to bypass this requirement.
let max_players = server.basic_config.max_players;
if max_players > 0 && server.get_player_count() >= max_players as usize {
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_SERVER_FULL,
translation::java::MULTIPLAYER_DISCONNECT_SERVER_FULL,
[],
))
.await;
return;
}
if !is_valid_player_name(&login_start.name) {
self.kick(TextComponent::text("Invalid characters in username"))
.await;
return;
}
// Default game profile, when no online mode
// TODO: Make offline UUID
let mut gameprofile = self.gameprofile.lock().await;
let proxy = &server.advanced_config.networking.proxy;
if proxy.enabled {
if proxy.velocity.enabled {
velocity::velocity_login(self).await;
} else if proxy.bungeecord.enabled {
match bungeecord::bungeecord_login(
&self.address,
&self.server_address.lock().await,
login_start.name,
)
.await
{
Ok((_ip, profile)) => {
// self.address.lock() = ip;
self.finish_login(&profile).await;
*gameprofile = Some(profile);
}
Err(error) => self.kick(TextComponent::text(error.to_string())).await,
}
}
} else {
let id = if server.basic_config.online_mode {
login_start.uuid
} else {
offline_uuid(&login_start.name).expect("This is very not safe and bad")
};
let profile = GameProfile {
id,
name: login_start.name,
properties: vec![],
profile_actions: None,
};
if server.advanced_config.networking.java_compression.enabled {
self.enable_compression(server).await;
}
if server.basic_config.encryption {
let verify_token: [u8; 4] = rand::random();
// Wait until we have sent the encryption packet to the client
self.send_packet_now(
&server
.encryption_request(&verify_token, server.basic_config.online_mode)
.await,
)
.await;
} else {
self.finish_login(&profile).await;
}
*gameprofile = Some(profile);
}
}
pub async fn handle_encryption_response(
&self,
server: &Server,
encryption_response: SEncryptionResponse,
) {
debug!("Handling encryption");
let shared_secret = server
.decrypt(&encryption_response.shared_secret)
.await
.unwrap();
if let Err(error) = self.set_encryption(&shared_secret).await {
self.kick(TextComponent::text(error.to_string())).await;
return;
}
let mut gameprofile = self.gameprofile.lock().await;
let Some(profile) = gameprofile.as_mut() else {
self.kick(TextComponent::text("No `GameProfile`")).await;
return;
};
if server.basic_config.online_mode {
// Online mode auth
match self
.authenticate(server, &shared_secret, &profile.name)
.await
{
Ok(new_profile) => *profile = new_profile,
Err(error) => {
self.kick(match error {
AuthError::FailedResponse => TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_AUTHSERVERS_DOWN,
translation::java::MULTIPLAYER_DISCONNECT_AUTHSERVERS_DOWN,
[],
),
AuthError::UnverifiedUsername => TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_UNVERIFIED_USERNAME,
translation::java::MULTIPLAYER_DISCONNECT_UNVERIFIED_USERNAME,
[],
),
e => TextComponent::text(e.to_string()),
})
.await;
}
}
}
// Don't allow duplicate UUIDs
if let Some(online_player) = &server.get_player_by_uuid(profile.id) {
debug!(
"Player (IP '{}', username '{}') tried to log in with the same UUID ('{}') as an online player (username '{}')",
&self.address.lock().await,
&profile.name,
&profile.id,
&online_player.gameprofile.name
);
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_DUPLICATE_LOGIN,
translation::java::MULTIPLAYER_DISCONNECT_DUPLICATE_LOGIN,
[],
))
.await;
return;
}
// Don't allow a duplicate username
if let Some(online_player) = &server.get_player_by_name(&profile.name) {
debug!(
"A player (IP '{}', attempted username '{}') tried to log in with the same username as an online player (UUID '{}', username '{}')",
&self.address.lock().await,
&profile.name,
&profile.id,
&online_player.gameprofile.name
);
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_DUPLICATE_LOGIN,
translation::java::MULTIPLAYER_DISCONNECT_DUPLICATE_LOGIN,
[],
))
.await;
return;
}
self.finish_login(profile).await;
}
async fn enable_compression(&self, server: &Server) {
let compression = server
.advanced_config
.networking
.java_compression
.info
.clone();
// We want to wait until we have sent the compression packet to the client
self.send_packet_now(&CSetCompression::new(
compression.threshold.try_into().unwrap(),
))
.await;
self.set_compression(compression).await;
}
async fn finish_login(&self, profile: &GameProfile) {
let packet = CLoginSuccess::new(&profile.id, &profile.name, &profile.properties, false);
self.send_packet_now(&packet).await;
}
async fn authenticate(
&self,
server: &Server,
shared_secret: &[u8],
username: &str,
) -> Result<GameProfile, AuthError> {
let hash = server.digest_secret(shared_secret).await;
let ip = self.address.lock().await.ip();
let profile = authentication::authenticate(
username,
&hash,
&ip,
&server.advanced_config.networking.authentication,
)?;
// Check if the player should join
if let Some(actions) = &profile.profile_actions {
if server
.advanced_config
.networking
.authentication
.player_profile
.allow_banned_players
{
for allowed in &server
.advanced_config
.networking
.authentication
.player_profile
.allowed_actions
{
if !actions.contains(allowed) {
return Err(AuthError::DisallowedAction);
}
}
if !actions.is_empty() {
return Err(AuthError::Banned);
}
} else if !actions.is_empty() {
return Err(AuthError::Banned);
}
}
// Validate textures
for property in &profile.properties {
authentication::validate_textures(
property,
&server.advanced_config.networking.authentication.textures,
)
.map_err(AuthError::TextureError)?;
}
Ok(profile)
}
pub fn handle_login_cookie_response(&self, packet: &SLoginCookieResponse) {
// TODO: allow plugins to access this
debug!(
"Received cookie_response[login]: key: \"{}\", payload_length: \"{:?}\"",
packet.key,
packet.payload.as_ref().map(|p| p.len())
);
}
pub async fn handle_plugin_response(
&self,
server: &Server,
plugin_response: SLoginPluginResponse,
) {
debug!("Handling plugin");
let velocity_config = &server.advanced_config.networking.proxy.velocity;
if velocity_config.enabled {
let mut address = self.address.lock().await;
match velocity::receive_velocity_plugin_response(
address.port(),
velocity_config,
plugin_response,
) {
Ok((profile, new_address)) => {
self.finish_login(&profile).await;
*self.gameprofile.lock().await = Some(profile);
*address = new_address;
drop(address);
}
Err(error) => self.kick(TextComponent::text(error.to_string())).await,
}
}
}
pub async fn handle_login_acknowledged(&self, server: &Server) {
debug!("Handling login acknowledgement");
self.connection_state.store(ConnectionState::Config);
self.send_packet_now(&server.get_branding()).await;
if server.advanced_config.server_links.enabled
&& self.version.load() >= MinecraftVersion::V_1_21
{
let mut links: Vec<Link> = Vec::new();
let bug_report = &server.advanced_config.server_links.bug_report;
if !bug_report.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::BugReport), bug_report));
}
let support = &server.advanced_config.server_links.support;
if !support.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Support), support));
}
let status = &server.advanced_config.server_links.status;
if !status.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Status), status));
}
let feedback = &server.advanced_config.server_links.feedback;
if !feedback.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Feedback), feedback));
}
let community = &server.advanced_config.server_links.community;
if !community.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Community), community));
}
let website = &server.advanced_config.server_links.website;
if !website.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Website), website));
}
let forums = &server.advanced_config.server_links.forums;
if !forums.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Forums), forums));
}
let news = &server.advanced_config.server_links.news;
if !news.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::News), news));
}
let announcements = &server.advanced_config.server_links.announcements;
if !announcements.is_empty() {
links.push(Link::new(
Label::BuiltIn(LinkType::Announcements),
announcements,
));
}
for (key, value) in &server.advanced_config.server_links.custom {
links.push(Link::new(
Label::TextComponent(TextComponent::text(key.clone()).into()),
value,
));
}
self.send_packet_now(&CConfigServerLinks::new(&links)).await;
}
let resource_config = &server.advanced_config.resource_pack.java;
if resource_config.enabled {
let uuid = Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, resource_config.url.as_bytes());
let resource_pack = CConfigAddResourcePack::new(
&uuid,
&resource_config.url,
&resource_config.sha1,
resource_config.force,
if resource_config.prompt_message.is_empty() {
None
} else {
Some(TextComponent::text(resource_config.prompt_message.clone()))
},
);
self.send_packet_now(&resource_pack).await;
} else {
// This will be invoked by our resource pack handler in the case of the above branch.
self.send_known_packs().await;
}
debug!("login acknowledged");
}
/// Send the known data packs to the client.
pub async fn send_known_packs(&self) {
self.send_packet_now(&CKnownPacks::new(&[KnownPack {
namespace: "minecraft",
id: "core",
version: "26.1",
}]))
.await;
}
}