feat: chat spam prevention
This commit is contained in:
parent
2816d7162e
commit
423522f33e
@ -2,19 +2,89 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Configuration for in-game chat behaviour.
|
||||
///
|
||||
/// Controls chat formatting and display.
|
||||
/// Controls chat formatting, display, and anti-spam protection.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct ChatConfig {
|
||||
/// The custom chat format.
|
||||
/// `Note`: it does not apply when secure chat is enabled.
|
||||
pub format: String,
|
||||
/// Anti-spam protection settings for player chat and commands.
|
||||
pub anti_spam: AntiSpamConfig,
|
||||
}
|
||||
|
||||
impl Default for ChatConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: "<{DISPLAYNAME}> {MESSAGE}".to_string(),
|
||||
anti_spam: AntiSpamConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for chat and command anti-spam protection.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct AntiSpamConfig {
|
||||
/// Whether anti-spam protection is enabled.
|
||||
pub enabled: bool,
|
||||
/// The spam counter threshold at which a player will be kicked.
|
||||
/// Vanilla default is 200 ticks.
|
||||
pub spam_threshold: u32,
|
||||
/// The amount added to the spam counter for each chat message or command sent.
|
||||
/// Vanilla default is 20 ticks.
|
||||
pub message_cost: u32,
|
||||
/// The amount decayed from the spam counter per server tick.
|
||||
/// Vanilla default is 1 tick.
|
||||
pub decay_per_tick: u32,
|
||||
/// Whether operators/admins bypass the anti-spam check.
|
||||
pub ops_bypass: bool,
|
||||
}
|
||||
|
||||
impl Default for AntiSpamConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
spam_threshold: 200,
|
||||
message_cost: 20,
|
||||
decay_per_tick: 1,
|
||||
ops_bypass: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_chat_config() {
|
||||
let config = ChatConfig::default();
|
||||
assert_eq!(config.format, "<{DISPLAYNAME}> {MESSAGE}");
|
||||
assert!(config.anti_spam.enabled);
|
||||
assert_eq!(config.anti_spam.spam_threshold, 200);
|
||||
assert_eq!(config.anti_spam.message_cost, 20);
|
||||
assert_eq!(config.anti_spam.decay_per_tick, 1);
|
||||
assert!(config.anti_spam.ops_bypass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_custom_anti_spam() {
|
||||
let toml_str = r#"
|
||||
format = "<{NAME}> {MESSAGE}"
|
||||
[anti_spam]
|
||||
enabled = false
|
||||
spam_threshold = 100
|
||||
message_cost = 10
|
||||
decay_per_tick = 2
|
||||
ops_bypass = false
|
||||
"#;
|
||||
let config: ChatConfig = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(config.format, "<{NAME}> {MESSAGE}");
|
||||
assert!(!config.anti_spam.enabled);
|
||||
assert_eq!(config.anti_spam.spam_threshold, 100);
|
||||
assert_eq!(config.anti_spam.message_cost, 10);
|
||||
assert_eq!(config.anti_spam.decay_per_tick, 2);
|
||||
assert!(!config.anti_spam.ops_bypass);
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ pub mod recipe;
|
||||
/// Resource pack configuration options.
|
||||
pub mod resource_pack;
|
||||
|
||||
pub use chat::ChatConfig;
|
||||
pub use chat::{AntiSpamConfig, ChatConfig};
|
||||
pub use commands::{CommandOverride, CommandsConfig};
|
||||
pub use networking::auth::AuthenticationConfig;
|
||||
pub use networking::bedrock::BedrockConfig;
|
||||
|
||||
@ -59,3 +59,50 @@ impl ClientPacket for CEncryptionRequest<'_> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> crate::ServerPacket<'a> for CEncryptionRequest<'a> {
|
||||
fn read(
|
||||
read: &mut &'a [u8],
|
||||
version: &JavaMinecraftVersion,
|
||||
) -> Result<Self, crate::ReadingError> {
|
||||
use crate::ser::{NetworkReadExt, NetworkReadSliceExt};
|
||||
let server_id = read.get_str_bounded_borrowed(20)?;
|
||||
let public_key_len = read.get_var_int()?.0 as usize;
|
||||
let public_key = read.read_slice_borrowed(public_key_len)?;
|
||||
let verify_token_len = read.get_var_int()?.0 as usize;
|
||||
let verify_token = read.read_slice_borrowed(verify_token_len)?;
|
||||
let should_authenticate = if version >= &JavaMinecraftVersion::V_1_19 {
|
||||
read.get_bool()?
|
||||
} else {
|
||||
true
|
||||
};
|
||||
Ok(Self {
|
||||
server_id,
|
||||
public_key,
|
||||
verify_token,
|
||||
should_authenticate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ServerPacket;
|
||||
|
||||
#[test]
|
||||
fn encryption_request_roundtrip() {
|
||||
let packet =
|
||||
CEncryptionRequest::new("test_server", b"public_key_bytes", b"verify_1234", true);
|
||||
let mut buf = Vec::new();
|
||||
let version = JavaMinecraftVersion::V_1_21_4;
|
||||
packet.write_packet_data(&mut buf, &version).unwrap();
|
||||
|
||||
let mut slice = buf.as_slice();
|
||||
let read_packet = CEncryptionRequest::read(&mut slice, &version).unwrap();
|
||||
assert_eq!(read_packet.server_id, packet.server_id);
|
||||
assert_eq!(read_packet.public_key, packet.public_key);
|
||||
assert_eq!(read_packet.verify_token, packet.verify_token);
|
||||
assert_eq!(read_packet.should_authenticate, packet.should_authenticate);
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,3 +36,33 @@ impl ClientPacket for CSetCompression {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> crate::ServerPacket<'a> for CSetCompression {
|
||||
fn read(
|
||||
read: &mut &'a [u8],
|
||||
_version: &JavaMinecraftVersion,
|
||||
) -> Result<Self, crate::ReadingError> {
|
||||
use crate::ser::NetworkReadExt;
|
||||
Ok(Self {
|
||||
threshold: read.get_var_int()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ServerPacket;
|
||||
|
||||
#[test]
|
||||
fn set_compression_roundtrip() {
|
||||
let packet = CSetCompression::new(crate::VarInt(256));
|
||||
let mut buf = Vec::new();
|
||||
let version = JavaMinecraftVersion::V_1_21_4;
|
||||
packet.write_packet_data(&mut buf, &version).unwrap();
|
||||
|
||||
let mut slice = buf.as_slice();
|
||||
let read_packet = CSetCompression::read(&mut slice, &version).unwrap();
|
||||
assert_eq!(read_packet.threshold.0, 256);
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,3 +35,33 @@ impl ClientPacket for CKeepAlive {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> crate::ServerPacket<'a> for CKeepAlive {
|
||||
fn read(
|
||||
read: &mut &'a [u8],
|
||||
_version: &JavaMinecraftVersion,
|
||||
) -> Result<Self, crate::ReadingError> {
|
||||
use crate::ser::NetworkReadExt;
|
||||
Ok(Self {
|
||||
keep_alive_id: read.get_i64_be()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ServerPacket;
|
||||
|
||||
#[test]
|
||||
fn keep_alive_roundtrip() {
|
||||
let packet = CKeepAlive::new(1234567890123456789);
|
||||
let mut buf = Vec::new();
|
||||
let version = JavaMinecraftVersion::V_1_21_4;
|
||||
packet.write_packet_data(&mut buf, &version).unwrap();
|
||||
|
||||
let mut slice = buf.as_slice();
|
||||
let read_packet = CKeepAlive::read(&mut slice, &version).unwrap();
|
||||
assert_eq!(read_packet.keep_alive_id, 1234567890123456789);
|
||||
}
|
||||
}
|
||||
|
||||
@ -755,6 +755,8 @@ pub struct Player {
|
||||
pub bedrock_spawned: AtomicBool,
|
||||
/// The amount of time (in ticks) the client has to report having finished loading before being timed out.
|
||||
pub client_loaded_timeout: AtomicU32,
|
||||
/// Counter for tracking chat and command spam. Decays each server tick.
|
||||
pub chat_spam_tick_count: AtomicU32,
|
||||
/// Item usage tracking for bows, crossbows, etc.
|
||||
pub using_item: AtomicBool,
|
||||
pub item_use_start_time: AtomicI32,
|
||||
@ -995,6 +997,7 @@ impl Player {
|
||||
client_loaded: AtomicBool::new(false),
|
||||
bedrock_spawned: AtomicBool::new(false),
|
||||
client_loaded_timeout: AtomicU32::new(60),
|
||||
chat_spam_tick_count: AtomicU32::new(0),
|
||||
// Item usage tracking
|
||||
using_item: AtomicBool::new(false),
|
||||
item_use_start_time: AtomicI32::new(0),
|
||||
@ -2498,6 +2501,16 @@ impl Player {
|
||||
self.tick_health().await;
|
||||
self.tick_maps(server).await;
|
||||
|
||||
// Anti-spam counter decay
|
||||
let anti_spam = &server.advanced_config.chat.anti_spam;
|
||||
if anti_spam.enabled && anti_spam.decay_per_tick > 0 {
|
||||
let _ = self.chat_spam_tick_count.fetch_update(
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
|count| Some(count.saturating_sub(anti_spam.decay_per_tick)),
|
||||
);
|
||||
}
|
||||
|
||||
// Timeout/keep alive handling
|
||||
self.tick_client_load_timeout();
|
||||
// Idle timeout handling
|
||||
@ -3503,6 +3516,46 @@ impl Player {
|
||||
self.last_action_time.store(std::time::Instant::now());
|
||||
}
|
||||
|
||||
/// Checks whether sending a chat message or command constitutes spam.
|
||||
///
|
||||
/// Increments the player's spam counter by `message_cost`. If the counter
|
||||
/// exceeds `spam_threshold`, the player is kicked with the vanilla
|
||||
/// `disconnect.spam` message and this method returns `true`.
|
||||
pub async fn check_chat_spam(&self, server: &Server) -> bool {
|
||||
let anti_spam = &server.advanced_config.chat.anti_spam;
|
||||
if !anti_spam.enabled {
|
||||
return false;
|
||||
}
|
||||
|
||||
if anti_spam.ops_bypass && self.permission_lvl.load() > PermissionLvl::Zero {
|
||||
return false;
|
||||
}
|
||||
|
||||
let new_count = self
|
||||
.chat_spam_tick_count
|
||||
.fetch_add(anti_spam.message_cost, Ordering::SeqCst)
|
||||
+ anti_spam.message_cost;
|
||||
|
||||
if new_count > anti_spam.spam_threshold {
|
||||
warn!(
|
||||
"Player {} kicked for spamming (spam score: {}/{})",
|
||||
self.gameprofile.name, new_count, anti_spam.spam_threshold
|
||||
);
|
||||
self.kick(
|
||||
DisconnectReason::Kicked,
|
||||
TextComponent::translate_cross(
|
||||
translation::java::DISCONNECT_SPAM,
|
||||
translation::bedrock::DISCONNECT_SPAM,
|
||||
[],
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn can_food_heal(&self) -> bool {
|
||||
let health = self.living_entity.health.load();
|
||||
let max_health = self.living_entity.get_max_health();
|
||||
@ -6698,4 +6751,42 @@ mod tests {
|
||||
|
||||
assert_eq!(read_root_vehicle(&nbt), Some(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anti_spam_counter_decay_and_threshold() {
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
let counter = AtomicU32::new(0);
|
||||
let message_cost = 20u32;
|
||||
let spam_threshold = 200u32;
|
||||
let decay_per_tick = 1u32;
|
||||
|
||||
// 10 messages -> count = 200 (within threshold)
|
||||
for _ in 0..10 {
|
||||
counter.fetch_add(message_cost, Ordering::SeqCst);
|
||||
}
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 200);
|
||||
assert!(counter.load(Ordering::SeqCst) <= spam_threshold);
|
||||
|
||||
// 11th message -> count = 220 (exceeds threshold)
|
||||
counter.fetch_add(message_cost, Ordering::SeqCst);
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 220);
|
||||
assert!(counter.load(Ordering::SeqCst) > spam_threshold);
|
||||
|
||||
// Simulate 25 ticks of decay
|
||||
for _ in 0..25 {
|
||||
let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |count| {
|
||||
Some(count.saturating_sub(decay_per_tick))
|
||||
});
|
||||
}
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 195);
|
||||
assert!(counter.load(Ordering::SeqCst) <= spam_threshold);
|
||||
|
||||
// Simulate decay down past 0 (saturating at 0)
|
||||
for _ in 0..250 {
|
||||
let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |count| {
|
||||
Some(count.saturating_sub(decay_per_tick))
|
||||
});
|
||||
}
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,9 @@ extern crate pumpkin_macros;
|
||||
|
||||
use crate::crash::CrashReport;
|
||||
use crate::data::VanillaData;
|
||||
use crate::logging::{GzipRollingLogger, PumpkinCommandCompleter, ReadlineLogWrapper};
|
||||
use crate::logging::{
|
||||
ConsoleWriter, GzipRollingLogger, PumpkinCommandCompleter, ReadlineLogWrapper,
|
||||
};
|
||||
use crate::net::bedrock::{
|
||||
BedrockClient,
|
||||
nethernet::{NetherNetListener, load_or_create_identity_key},
|
||||
@ -69,7 +71,7 @@ pub type LoggerOption = Option<(ReadlineLogWrapper, LevelFilter, LoggingConfig)>
|
||||
pub static LOGGER_IMPL: LazyLock<Arc<OnceLock<LoggerOption>>> =
|
||||
LazyLock::new(|| Arc::new(OnceLock::new()));
|
||||
|
||||
#[expect(clippy::print_stderr)]
|
||||
#[expect(clippy::print_stderr, clippy::too_many_lines)]
|
||||
pub fn init_logger(advanced_config: &AdvancedConfiguration) {
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::fmt;
|
||||
@ -107,7 +109,7 @@ pub fn init_logger(advanced_config: &AdvancedConfiguration) {
|
||||
};
|
||||
|
||||
let (logger, rl): (
|
||||
Box<dyn std::io::Write + Send + 'static>,
|
||||
ConsoleWriter,
|
||||
Option<Editor<PumpkinCommandCompleter, FileHistory>>,
|
||||
) = if advanced_config.commands.use_tty && stdin().is_terminal() {
|
||||
let rl_config = Config::builder()
|
||||
@ -120,17 +122,21 @@ pub fn init_logger(advanced_config: &AdvancedConfiguration) {
|
||||
match Editor::with_config(rl_config) {
|
||||
Ok(mut rl) => {
|
||||
rl.set_helper(Some(helper));
|
||||
(Box::new(std::io::stdout()), Some(rl))
|
||||
let printer = rl.create_external_printer().ok().map(|p| {
|
||||
let boxed: Box<dyn rustyline::ExternalPrinter + Send> = Box::new(p);
|
||||
boxed
|
||||
});
|
||||
(ConsoleWriter::new(printer), Some(rl))
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Failed to initialize console input ({e}); falling back to simple logger"
|
||||
);
|
||||
(Box::new(std::io::stdout()), None)
|
||||
(ConsoleWriter::new(None), None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(Box::new(std::io::stdout()), None)
|
||||
(ConsoleWriter::new(None), None)
|
||||
};
|
||||
|
||||
let fmt_layer = fmt::layer()
|
||||
@ -336,15 +342,8 @@ impl PumpkinServer {
|
||||
}
|
||||
};
|
||||
let _ = server.bedrock_private_key.set(identity_key.clone());
|
||||
let oidc_verifier = if config.online_mode && config.authentication.enabled {
|
||||
let Some((issuer, keys)) = server.bedrock_oidc_keys.get() else {
|
||||
error!("Bedrock OIDC keys should be initialized before binding NetherNet");
|
||||
return None;
|
||||
};
|
||||
Some(Arc::new((issuer.clone(), keys.clone())))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let oidc_verifier = (config.online_mode && config.authentication.enabled)
|
||||
.then(|| server.bedrock_oidc_keys.clone());
|
||||
match NetherNetListener::bind(
|
||||
config.nethernet.address,
|
||||
ice_socket,
|
||||
@ -791,8 +790,8 @@ fn setup_console(mut rl: Editor<PumpkinCommandCompleter, FileHistory>, server: A
|
||||
&line,
|
||||
)
|
||||
.await;
|
||||
let _ = tx_reply.send(1).await;
|
||||
}
|
||||
let _ = tx_reply.send(1).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -90,6 +90,58 @@ pub struct ReadlineLogWrapper {
|
||||
readline: std::sync::Mutex<Option<Editor<PumpkinCommandCompleter, FileHistory>>>,
|
||||
}
|
||||
|
||||
pub struct ConsoleWriter {
|
||||
printer: Option<Box<dyn rustyline::ExternalPrinter + Send>>,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ConsoleWriter {
|
||||
#[must_use]
|
||||
pub fn new(printer: Option<Box<dyn rustyline::ExternalPrinter + Send>>) -> Self {
|
||||
Self {
|
||||
printer,
|
||||
buffer: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for ConsoleWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
if let Some(ref mut printer) = self.printer {
|
||||
self.buffer.extend_from_slice(buf);
|
||||
while let Some(pos) = self.buffer.iter().position(|&b| b == b'\n') {
|
||||
let line_bytes: Vec<u8> = self.buffer.drain(..=pos).collect();
|
||||
let msg = String::from_utf8_lossy(&line_bytes).into_owned();
|
||||
if printer.print(msg).is_err() {
|
||||
let mut stdout = io::stdout().lock();
|
||||
let _ = stdout.write_all(line_bytes.as_slice());
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
}
|
||||
Ok(buf.len())
|
||||
} else {
|
||||
io::stdout().write(buf)
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
if let Some(ref mut printer) = self.printer {
|
||||
if !self.buffer.is_empty() {
|
||||
let buffer = std::mem::take(&mut self.buffer);
|
||||
let msg = String::from_utf8_lossy(&buffer).into_owned();
|
||||
if printer.print(msg).is_err() {
|
||||
let mut stdout = io::stdout().lock();
|
||||
let _ = stdout.write_all(buffer.as_slice());
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
io::stdout().flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GzipRollingLoggerData {
|
||||
pub current_day_of_month: u8,
|
||||
pub last_rotate_time: time::OffsetDateTime,
|
||||
|
||||
@ -36,7 +36,7 @@ use pumpkin_util::p384::{
|
||||
use serde_json::{Value, json};
|
||||
use tokio::{
|
||||
net::TcpListener,
|
||||
sync::{Mutex, RwLock, mpsc},
|
||||
sync::{Mutex, OnceCell, RwLock, mpsc},
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, info, trace, warn};
|
||||
@ -83,7 +83,7 @@ struct EndpointState {
|
||||
incoming: mpsc::Sender<IncomingSession>,
|
||||
identity_key: Arc<SigningKey>,
|
||||
require_client_identity: bool,
|
||||
oidc_verifier: Option<Arc<(String, Jwks)>>,
|
||||
oidc_verifier: Option<Arc<OnceCell<(String, Jwks)>>>,
|
||||
stun_servers: Arc<[String]>,
|
||||
ice_local_addr: SocketAddr,
|
||||
external_ip: Option<IpAddr>,
|
||||
@ -97,7 +97,7 @@ impl NetherNetListener {
|
||||
external_ip: Option<IpAddr>,
|
||||
identity_key: Arc<SigningKey>,
|
||||
require_client_identity: bool,
|
||||
oidc_verifier: Option<Arc<(String, Jwks)>>,
|
||||
oidc_verifier: Option<Arc<OnceCell<(String, Jwks)>>>,
|
||||
stun_servers: Vec<String>,
|
||||
) -> std::io::Result<Self> {
|
||||
let listener = TcpListener::bind(address).await?;
|
||||
@ -269,7 +269,7 @@ async fn negotiate_inner(
|
||||
let (offer, client_public_key) = authenticate_client_offer(
|
||||
offer,
|
||||
state.require_client_identity,
|
||||
state.oidc_verifier.as_deref(),
|
||||
state.oidc_verifier.as_ref().and_then(|c| c.get()),
|
||||
)?;
|
||||
trace!(
|
||||
%address,
|
||||
|
||||
@ -8,6 +8,10 @@ impl BedrockClient {
|
||||
server: &Arc<Server>,
|
||||
packet: SCommandRequest<'_>,
|
||||
) {
|
||||
player.update_last_action_time();
|
||||
if player.check_chat_spam(server).await {
|
||||
return;
|
||||
}
|
||||
let player_clone = player.clone();
|
||||
let server_clone = server.clone();
|
||||
let command = packet.command.strip_prefix('/').unwrap_or(&packet.command);
|
||||
|
||||
@ -8,6 +8,10 @@ impl BedrockClient {
|
||||
player: &Arc<Player>,
|
||||
packet: SText<'_>,
|
||||
) {
|
||||
player.update_last_action_time();
|
||||
if player.check_chat_spam(server).await {
|
||||
return;
|
||||
}
|
||||
let gameprofile = &player.gameprofile;
|
||||
|
||||
send_cancellable! {{
|
||||
|
||||
@ -9,6 +9,9 @@ impl JavaClient {
|
||||
command: &SChatCommand<'_>,
|
||||
) {
|
||||
player.update_last_action_time();
|
||||
if player.check_chat_spam(server).await {
|
||||
return;
|
||||
}
|
||||
let player_clone = player.clone();
|
||||
let server_clone = server.clone();
|
||||
send_cancellable! {{
|
||||
|
||||
@ -30,6 +30,10 @@ impl JavaClient {
|
||||
return;
|
||||
}
|
||||
|
||||
if player.check_chat_spam(server).await {
|
||||
return;
|
||||
}
|
||||
|
||||
send_cancellable! {{
|
||||
server;
|
||||
PlayerChatEvent::new(player.clone(), chat_message.message.to_string(), vec![]);
|
||||
|
||||
@ -86,7 +86,7 @@ pub struct Server {
|
||||
/// Handles cryptographic keys for secure communication.
|
||||
key_store: OnceCell<Arc<KeyStore>>,
|
||||
/// Bedrock OIDC provider keys, fetched on startup for 1.26.10+ token validation.
|
||||
pub bedrock_oidc_keys: OnceCell<(String, pumpkin_util::jwt::Jwks)>,
|
||||
pub bedrock_oidc_keys: Arc<OnceCell<(String, pumpkin_util::jwt::Jwks)>>,
|
||||
/// Cached Bedrock server private key (process-lifetime). Generated on first Bedrock login and reused.
|
||||
pub bedrock_private_key: OnceCell<Arc<pumpkin_util::p384::ecdsa::SigningKey>>,
|
||||
/// Manages server status information.
|
||||
@ -241,21 +241,6 @@ impl Server {
|
||||
|
||||
let tick_rate_manager = Arc::new(ServerTickRateManager::new(basic_config.tps));
|
||||
|
||||
let mojang_keys_task = tokio::spawn({
|
||||
let auth_config = advanced_config.networking.java.authentication.clone();
|
||||
let allow_chat = basic_config.allow_chat_reports;
|
||||
async move {
|
||||
if allow_chat {
|
||||
fetch_mojang_public_keys(&auth_config).unwrap_or_else(|e| {
|
||||
error!("Failed to fetch Mojang keys: {e}");
|
||||
Vec::new()
|
||||
})
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let dimensions = {
|
||||
let mut dimensions = vec![Dimension::OVERWORLD];
|
||||
if basic_config.allow_nether {
|
||||
@ -293,7 +278,7 @@ impl Server {
|
||||
block_registry: block_registry.clone(),
|
||||
item_registry: super::item::items::default_registry(),
|
||||
key_store: OnceCell::new(),
|
||||
bedrock_oidc_keys: OnceCell::new(),
|
||||
bedrock_oidc_keys: Arc::new(OnceCell::new()),
|
||||
bedrock_private_key: OnceCell::new(),
|
||||
listing,
|
||||
branding: CachedBranding::new(),
|
||||
@ -328,14 +313,75 @@ impl Server {
|
||||
}),
|
||||
);
|
||||
|
||||
// Fetch / generate keys in background tasks to avoid blocking startup
|
||||
let server_clone = server.clone();
|
||||
tokio::spawn(async move {
|
||||
server_clone
|
||||
.key_store
|
||||
.get_or_init(|| async { Arc::new(KeyStore::new()) })
|
||||
.await;
|
||||
let key_store = tokio::task::spawn_blocking(|| Arc::new(KeyStore::new()))
|
||||
.await
|
||||
.unwrap_or_else(|_| Arc::new(KeyStore::new()));
|
||||
let _ = server_clone.key_store.set(key_store);
|
||||
});
|
||||
|
||||
if server.basic_config.allow_chat_reports {
|
||||
let server_clone = server.clone();
|
||||
tokio::spawn(async move {
|
||||
let auth_config = server_clone
|
||||
.advanced_config
|
||||
.networking
|
||||
.java
|
||||
.authentication
|
||||
.clone();
|
||||
let keys = tokio::task::spawn_blocking(move || {
|
||||
fetch_mojang_public_keys(&auth_config).unwrap_or_else(|e| {
|
||||
error!("Failed to fetch Mojang keys: {e}");
|
||||
Vec::new()
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
server_clone.mojang_public_keys.store(Arc::new(keys));
|
||||
});
|
||||
}
|
||||
|
||||
if server.advanced_config.networking.bedrock.online_mode
|
||||
&& server
|
||||
.advanced_config
|
||||
.networking
|
||||
.bedrock
|
||||
.authentication
|
||||
.enabled
|
||||
{
|
||||
let server_clone = server.clone();
|
||||
tokio::spawn(async move {
|
||||
let auth = server_clone
|
||||
.advanced_config
|
||||
.networking
|
||||
.bedrock
|
||||
.authentication
|
||||
.clone();
|
||||
let keys = match tokio::task::spawn_blocking(move || {
|
||||
pumpkin_util::jwt::fetch_oidc_jwks(
|
||||
auth.url.as_deref(),
|
||||
auth.connect_timeout,
|
||||
auth.read_timeout,
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(keys)) => keys,
|
||||
Ok(Err(error)) => {
|
||||
error!("Failed to fetch Bedrock OIDC keys: {error}");
|
||||
(String::new(), pumpkin_util::jwt::Jwks { keys: Vec::new() })
|
||||
}
|
||||
Err(join_err) => {
|
||||
error!("Bedrock OIDC key task failed: {join_err}");
|
||||
(String::new(), pumpkin_util::jwt::Jwks { keys: Vec::new() })
|
||||
}
|
||||
};
|
||||
let _ = server_clone.bedrock_oidc_keys.set(keys);
|
||||
});
|
||||
}
|
||||
|
||||
let world_loader = |dim: Dimension| {
|
||||
let path = world_path.clone();
|
||||
let registry = block_registry.clone();
|
||||
@ -365,8 +411,7 @@ impl Server {
|
||||
world_futures.push(world_loader(dim.clone()));
|
||||
}
|
||||
|
||||
let (worlds_results, keys) =
|
||||
tokio::join!(futures::future::join_all(world_futures), mojang_keys_task);
|
||||
let worlds_results = futures::future::join_all(world_futures).await;
|
||||
|
||||
let mut worlds_vec = Vec::new();
|
||||
for world in worlds_results.into_iter().flatten() {
|
||||
@ -390,31 +435,8 @@ impl Server {
|
||||
}
|
||||
|
||||
server.worlds.store(Arc::new(worlds_vec));
|
||||
if let Ok(k) = keys {
|
||||
server.mojang_public_keys.store(Arc::new(k));
|
||||
}
|
||||
|
||||
info!("All worlds loaded successfully.");
|
||||
|
||||
if server.advanced_config.networking.bedrock.online_mode {
|
||||
server
|
||||
.bedrock_oidc_keys
|
||||
.get_or_init(|| async {
|
||||
tokio::task::block_in_place(|| {
|
||||
let auth = &server.advanced_config.networking.bedrock.authentication;
|
||||
pumpkin_util::jwt::fetch_oidc_jwks(
|
||||
auth.url.as_deref(),
|
||||
auth.connect_timeout,
|
||||
auth.read_timeout,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|error| {
|
||||
error!("Failed to fetch Bedrock OIDC keys: {error}");
|
||||
(String::new(), pumpkin_util::jwt::Jwks { keys: Vec::new() })
|
||||
})
|
||||
})
|
||||
.await;
|
||||
}
|
||||
server
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user