diff --git a/server/src/alias_validator.rs b/server/src/alias_validator.rs new file mode 100644 index 0000000000..d6e02f09c9 --- /dev/null +++ b/server/src/alias_validator.rs @@ -0,0 +1,115 @@ +use std::fmt::{self, Display}; + +#[derive(Debug, Default)] +pub struct AliasValidator { + banned_substrings: Vec, +} + +impl AliasValidator { + pub fn new(banned_substrings: Vec) -> Self { + let banned_substrings = banned_substrings + .iter() + .map(|string| string.to_lowercase()) + .collect(); + + AliasValidator { banned_substrings } + } + + pub fn validate(&self, alias: &str) -> Result<(), ValidatorError> { + let lowercase_alias = alias.to_lowercase(); + + for banned_word in self.banned_substrings.iter() { + if lowercase_alias.contains(banned_word) { + return Err(ValidatorError::Forbidden( + alias.to_owned(), + banned_word.to_owned(), + )); + } + } + Ok(()) + } +} + +#[derive(Debug, PartialEq)] +pub enum ValidatorError { + Forbidden(String, String), +} + +impl Display for ValidatorError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Forbidden(name, _) => write!( + formatter, + "Character name \"{}\" contains a banned word", + name + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn multiple_matches() { + let banned_substrings = vec!["bad".to_owned(), "worse".to_owned()]; + let validator = AliasValidator::new(banned_substrings); + + let bad_alias = "Badplayery Mc WorsePlayeryFace"; + let result = validator.validate(bad_alias); + + assert_eq!( + result, + Err(ValidatorError::Forbidden( + bad_alias.to_owned(), + "bad".to_owned() + )) + ); + } + + #[test] + fn single_lowercase_match() { + let banned_substrings = vec!["blue".to_owned()]; + let validator = AliasValidator::new(banned_substrings); + + let bad_alias = "blueName"; + let result = validator.validate(bad_alias); + + assert_eq!( + result, + Err(ValidatorError::Forbidden( + bad_alias.to_owned(), + "blue".to_owned() + )) + ); + } + + #[test] + fn single_case_insensitive_match() { + let banned_substrings = vec!["GrEEn".to_owned()]; + let validator = AliasValidator::new(banned_substrings); + + let bad_alias = "gReenName"; + let result = validator.validate(bad_alias); + + assert_eq!( + result, + Err(ValidatorError::Forbidden( + bad_alias.to_owned(), + "green".to_owned() + )) + ); + } + + #[test] + fn mp_matches() { + let banned_substrings = vec!["orange".to_owned()]; + let validator = AliasValidator::new(banned_substrings); + + let good_alias = "ReasonableName"; + let result = validator.validate(good_alias); + + assert_eq!(result, Ok(())); + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 4ed6b6272f..dadab8890b 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::option_map_unit_fn)] #![feature(drain_filter, option_zip)] +pub mod alias_validator; pub mod auth_provider; pub mod chunk_generator; pub mod client; @@ -20,6 +21,7 @@ pub mod sys; pub use crate::{error::Error, events::Event, input::Input, settings::ServerSettings}; use crate::{ + alias_validator::AliasValidator, auth_provider::AuthProvider, chunk_generator::ChunkGenerator, client::{Client, RegionSubscription}, @@ -137,6 +139,30 @@ impl Server { state.ecs_mut().register::(); state.ecs_mut().register::(); + //Alias validator + let banned_words_paths = &settings.banned_words_files; + let mut banned_words = Vec::new(); + for path in banned_words_paths { + let mut list = match std::fs::File::open(&path) { + Ok(file) => match ron::de::from_reader(&file) { + Ok(vec) => vec, + Err(error) => { + tracing::warn!(?error, ?file, "Couldn't deserialize banned words file"); + return Err(Error::Other(error.to_string())); + }, + }, + Err(error) => { + tracing::warn!(?error, ?path, "couldn't open banned words file"); + return Err(Error::Other(error.to_string())); + }, + }; + banned_words.append(&mut list); + } + let banned_words_count = banned_words.len(); + tracing::debug!(?banned_words_count); + tracing::trace!(?banned_words); + state.ecs_mut().insert(AliasValidator::new(banned_words)); + #[cfg(feature = "worldgen")] let world = World::generate(settings.world_seed, WorldOpts { seed_elements: true, diff --git a/server/src/settings.rs b/server/src/settings.rs index 9e0d0f637a..8bc0c6f3a5 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -25,6 +25,7 @@ pub struct ServerSettings { pub map_file: Option, pub persistence_db_dir: String, pub max_view_distance: Option, + pub banned_words_files: Vec, } impl Default for ServerSettings { @@ -63,6 +64,7 @@ impl Default for ServerSettings { whitelist: Vec::new(), persistence_db_dir: "saves".to_owned(), max_view_distance: Some(30), + banned_words_files: Vec::new(), } } } diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index f9a4918723..8e8f604810 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -1,7 +1,7 @@ use super::SysTimer; use crate::{ - auth_provider::AuthProvider, client::Client, persistence::character::CharacterLoader, - ServerSettings, CLIENT_TIMEOUT, + alias_validator::AliasValidator, auth_provider::AuthProvider, client::Client, + persistence::character::CharacterLoader, ServerSettings, CLIENT_TIMEOUT, }; use common::{ comp::{ @@ -55,6 +55,7 @@ impl Sys { players: &mut WriteStorage<'_, Player>, controllers: &mut WriteStorage<'_, Controller>, settings: &Read<'_, ServerSettings>, + alias_validator: &ReadExpect<'_, AliasValidator>, ) -> Result<(), crate::error::Error> { loop { let msg = client.recv().await?; @@ -347,7 +348,14 @@ impl Sys { } }, ClientMsg::CreateCharacter { alias, tool, body } => { - if let Some(player) = players.get(entity) { + if let Err(error) = alias_validator.validate(&alias) { + tracing::debug!( + ?error, + ?alias, + "denied alias as it contained a banned word" + ); + client.notify(ServerMsg::CharacterActionError(error.to_string())); + } else if let Some(player) = players.get(entity) { character_loader.create_character( entity, player.uuid().to_string(), @@ -413,6 +421,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Client>, WriteStorage<'a, Controller>, Read<'a, ServerSettings>, + ReadExpect<'a, AliasValidator>, ); #[allow(clippy::match_ref_pats)] // TODO: Pending review in #587 @@ -443,6 +452,7 @@ impl<'a> System<'a> for Sys { mut clients, mut controllers, settings, + alias_validator, ): Self::SystemData, ) { timer.start(); @@ -502,6 +512,7 @@ impl<'a> System<'a> for Sys { &mut players, &mut controllers, &settings, + &alias_validator, ).fuse() => err, ) });