mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 08:22:33 +00:00
404 lines
14 KiB
Rust
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;
|
|
}
|
|
}
|