Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add whitelist feature #451

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions pumpkin-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ pub use pvp::PVPConfig;
pub use server_links::ServerLinksConfig;

mod commands;

pub mod op;
mod pvp;
mod server_links;

Expand Down Expand Up @@ -83,6 +81,7 @@ pub struct BasicConfiguration {
pub encryption: bool,
/// The server's description displayed on the status screen.
pub motd: String,
/// The server's tps
pub tps: f32,
/// The default game mode for players.
pub default_gamemode: GameMode,
Expand All @@ -92,6 +91,10 @@ pub struct BasicConfiguration {
pub use_favicon: bool,
/// Path to server favicon
pub favicon_path: String,
/// Whether to enable the whitelist
pub white_list: bool,
/// Whether to enforce the whitelist
pub enforce_whitelist: bool,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both variables are used in the vanilla server.properties file. You can enable the whitelist but not enforce it. If you enforce it, players will be kicked when you reload the whitelist if they are not whitelisted.
From the Minecraft wiki:

white-list

Whether the whitelist is enabled.

With a whitelist enabled, users not on the whitelist cannot connect. Intended for private servers, such as those for real-life friends or strangers carefully selected via an application process, for example.

false - No white list is used.
true - The file whitelist.json is used to generate the white list.

Note: Ops are automatically whitelisted, and there is no need to add them to the whitelist.

enforce-whitelist

Whether to enforce changes to the whitelist.

When this option as well as the whitelist is enabled, players not present on the whitelist get kicked from the server after the server reloads the whitelist file.

false - Online players that are not on the whitelist are not kicked.
true - Online players that are not on the whitelist are kicked.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhh okay, I mean, I'm fine with leaving it then

}

impl Default for BasicConfiguration {
Expand All @@ -114,6 +117,8 @@ impl Default for BasicConfiguration {
scrub_ips: true,
use_favicon: true,
favicon_path: "icon.png".to_string(),
white_list: false,
enforce_whitelist: false,
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion pumpkin/src/command/commands/cmd_op.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::data::op::Op;
use crate::{
command::{
args::{arg_players::PlayersArgumentConsumer, Arg, ConsumedArgs},
Expand All @@ -8,7 +9,7 @@ use crate::{
data::{op_data::OPERATOR_CONFIG, SaveJSONConfiguration},
};
use async_trait::async_trait;
use pumpkin_config::{op::Op, BASIC_CONFIG};
use pumpkin_config::BASIC_CONFIG;
use pumpkin_core::text::TextComponent;
use CommandError::InvalidConsumption;

Expand Down
221 changes: 221 additions & 0 deletions pumpkin/src/command/commands/cmd_whitelist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use crate::command::tree_builder::literal;
use crate::data::player_profile::PlayerProfile;
use crate::data::whitelist_data::WHITELIST_CONFIG;
use crate::server::Server;
use crate::{
command::{
args::{arg_players::PlayersArgumentConsumer, Arg, ConsumedArgs},
tree::CommandTree,
tree_builder::argument,
CommandError, CommandExecutor, CommandSender,
},
data::{ReloadJSONConfiguration, SaveJSONConfiguration},
};
use async_trait::async_trait;
use pumpkin_config::BASIC_CONFIG;
use pumpkin_core::text::TextComponent;
use pumpkin_core::PermissionLvl;
use CommandError::InvalidConsumption;

const NAMES: [&str; 1] = ["whitelist"];
const DESCRIPTION: &str = "Manages the server's whitelist.";
const ARG_TARGET: &str = "player";

struct WhitelistAddExecutor;

#[async_trait]
impl CommandExecutor for WhitelistAddExecutor {
async fn execute<'a>(
&self,
sender: &mut CommandSender<'a>,
_server: &Server,
args: &ConsumedArgs<'a>,
) -> Result<(), CommandError> {
let mut whitelist_config = WHITELIST_CONFIG.write().await;

let Some(Arg::Players(targets)) = args.get(&ARG_TARGET) else {
return Err(InvalidConsumption(Some(ARG_TARGET.into())));
};

// from the command tree, the command can only be executed with one player
let player = &targets[0];

if whitelist_config
.whitelist
.iter()
.any(|p| p.uuid == player.gameprofile.id)
{
let message = format!("Player {} is already whitelisted.", player.gameprofile.name);
let msg = TextComponent::text(message);
sender.send_message(msg).await;
} else {
let whitelist_entry =
PlayerProfile::new(player.gameprofile.id, player.gameprofile.name.clone());

whitelist_config.whitelist.push(whitelist_entry);
whitelist_config.save();

let player_name = &player.gameprofile.name;
let message = format!("Added {player_name} to the whitelist.");
let msg = TextComponent::text(message);
sender.send_message(msg).await;
}

Ok(())
}
}

struct WhitelistRemoveExecutor;

#[async_trait]
impl CommandExecutor for WhitelistRemoveExecutor {
async fn execute<'a>(
&self,
sender: &mut CommandSender<'a>,
server: &crate::server::Server,
args: &ConsumedArgs<'a>,
) -> Result<(), CommandError> {
let mut whitelist_config = WHITELIST_CONFIG.write().await;

let Some(Arg::Players(targets)) = args.get(&ARG_TARGET) else {
return Err(InvalidConsumption(Some(ARG_TARGET.into())));
};

// from the command tree, the command can only be executed with one player
let player = &targets[0];

if let Some(pos) = whitelist_config
.whitelist
.iter()
.position(|p| p.uuid == player.gameprofile.id)
{
whitelist_config.whitelist.remove(pos);
whitelist_config.save();
let is_op = player.permission_lvl.load() >= PermissionLvl::Three;
if *server.white_list.read().await && BASIC_CONFIG.enforce_whitelist && !is_op {
let msg = TextComponent::text("You are not whitelisted anymore.");
player.kick(msg).await;
}
let message = format!("Removed {} from the whitelist.", player.gameprofile.name);
let msg = TextComponent::text(message);
sender.send_message(msg).await;
} else {
let message = format!("Player {} is not whitelisted.", player.gameprofile.name);
let msg = TextComponent::text(message);
sender.send_message(msg).await;
}

Ok(())
}
}

struct WhitelistListExecutor;

#[async_trait]
impl CommandExecutor for WhitelistListExecutor {
async fn execute<'a>(
&self,
sender: &mut CommandSender<'a>,
_server: &crate::server::Server,
_args: &ConsumedArgs<'a>,
) -> Result<(), CommandError> {
let whitelist_names: Vec<String> = WHITELIST_CONFIG
.read()
.await
.whitelist
.iter()
.map(|p| p.name.clone())
.collect();
let message = format!("Whitelisted players: {whitelist_names:?}");
let msg = TextComponent::text(message);
sender.send_message(msg).await;
Ok(())
}
}

struct WhitelistOffExecutor;

#[async_trait]
impl CommandExecutor for WhitelistOffExecutor {
async fn execute<'a>(
&self,
sender: &mut CommandSender<'a>,
server: &Server,
_args: &ConsumedArgs<'a>,
) -> Result<(), CommandError> {
let mut whitelist = server.white_list.write().await;
*whitelist = false;
let msg = TextComponent::text(
"Whitelist is now off. To persist this change, modify the configuration.toml file.",
);
sender.send_message(msg).await;
Ok(())
}
}

struct WhitelistOnExecutor;

#[async_trait]
impl CommandExecutor for WhitelistOnExecutor {
async fn execute<'a>(
&self,
sender: &mut CommandSender<'a>,
server: &Server,
_args: &ConsumedArgs<'a>,
) -> Result<(), CommandError> {
let mut whitelist = server.white_list.write().await;
*whitelist = true;
let msg = TextComponent::text(
"Whitelist is now on. To persist this change, modify the configuration.toml file.",
);
sender.send_message(msg).await;
Ok(())
}
}

struct WhitelistReloadExecutor;

#[async_trait]
impl CommandExecutor for WhitelistReloadExecutor {
async fn execute<'a>(
&self,
sender: &mut CommandSender<'a>,
server: &Server,
_args: &ConsumedArgs<'a>,
) -> Result<(), CommandError> {
let mut whitelist_config = WHITELIST_CONFIG.write().await;
whitelist_config.reload();
// kick all players that are not whitelisted or operator if the whitelist is enforced
if *server.white_list.read().await && BASIC_CONFIG.enforce_whitelist {
for player in server.get_all_players().await {
let is_whitelisted = whitelist_config
.whitelist
.iter()
.any(|p| p.uuid == player.gameprofile.id);
let is_op = player.permission_lvl.load() >= PermissionLvl::Three;
if !is_whitelisted && !is_op {
let msg = TextComponent::text("You are not whitelisted anymore.");
player.kick(msg).await;
}
}
}

let msg = TextComponent::text("Whitelist configuration reloaded.");
sender.send_message(msg).await;
Ok(())
}
}

pub fn init_command_tree() -> CommandTree {
CommandTree::new(NAMES, DESCRIPTION)
.with_child(literal("add").with_child(
argument(ARG_TARGET, PlayersArgumentConsumer).execute(WhitelistAddExecutor),
))
.with_child(literal("remove").with_child(
argument(ARG_TARGET, PlayersArgumentConsumer).execute(WhitelistRemoveExecutor),
))
.with_child(literal("list").execute(WhitelistListExecutor))
.with_child(literal("off").execute(WhitelistOffExecutor))
.with_child(literal("on").execute(WhitelistOnExecutor))
.with_child(literal("reload").execute(WhitelistReloadExecutor))
}
1 change: 1 addition & 0 deletions pumpkin/src/command/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ pub mod cmd_stop;
pub mod cmd_teleport;
pub mod cmd_time;
pub mod cmd_transfer;
pub mod cmd_whitelist;
pub mod cmd_worldborder;
4 changes: 3 additions & 1 deletion pumpkin/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use args::ConsumedArgs;
use async_trait::async_trait;
use commands::{
cmd_clear, cmd_deop, cmd_fill, cmd_gamemode, cmd_give, cmd_help, cmd_kick, cmd_kill, cmd_list,
cmd_op, cmd_pumpkin, cmd_say, cmd_setblock, cmd_stop, cmd_teleport, cmd_time, cmd_worldborder,
cmd_op, cmd_pumpkin, cmd_say, cmd_setblock, cmd_stop, cmd_teleport, cmd_time, cmd_whitelist,
cmd_worldborder,
};
use dispatcher::CommandError;
use pumpkin_core::math::vector3::Vector3;
Expand Down Expand Up @@ -131,6 +132,7 @@ pub fn default_dispatcher() -> CommandDispatcher {
dispatcher.register(cmd_fill::init_command_tree(), PermissionLvl::Two);
dispatcher.register(cmd_op::init_command_tree(), PermissionLvl::Three);
dispatcher.register(cmd_deop::init_command_tree(), PermissionLvl::Three);
dispatcher.register(cmd_whitelist::init_command_tree(), PermissionLvl::Three);

dispatcher
}
Expand Down
23 changes: 16 additions & 7 deletions pumpkin/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use serde::{Deserialize, Serialize};

const DATA_FOLDER: &str = "data/";

pub mod op;
pub mod op_data;
pub mod player_profile;
pub mod whitelist_data;

pub trait LoadJSONConfiguration {
#[must_use]
Expand Down Expand Up @@ -34,7 +37,7 @@ pub trait LoadJSONConfiguration {

if let Err(err) = fs::write(&path, serde_json::to_string_pretty(&content).unwrap()) {
log::error!(
"Couldn't write default data config to {path:?}. Reason: {err}. This is probably caused by a config update. Just delete the old data config and restart.",
"Couldn't write default data config to {path:?}. Reason: {err}. This is probably caused by a config update. Just delete the old data config and restart.",
);
}

Expand Down Expand Up @@ -69,7 +72,7 @@ pub trait SaveJSONConfiguration: LoadJSONConfiguration {
Ok(content) => content,
Err(err) => {
log::warn!(
"Couldn't serialize operator data config to {:?}. Reason: {}",
"Couldn't serialize data config to {:?}. Reason: {}",
path,
err
);
Expand All @@ -78,11 +81,17 @@ pub trait SaveJSONConfiguration: LoadJSONConfiguration {
};

if let Err(err) = std::fs::write(&path, content) {
log::warn!(
"Couldn't write operator config to {:?}. Reason: {}",
path,
err
);
log::warn!("Couldn't write config to {:?}. Reason: {}", path, err);
}
}
}

pub trait ReloadJSONConfiguration: LoadJSONConfiguration {
fn reload(&mut self)
where
Self: Sized + Default + Serialize + for<'de> Deserialize<'de>,
{
let config = Self::load();
*self = config;
}
}
1 change: 1 addition & 0 deletions pumpkin-config/src/op.rs → pumpkin/src/data/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct Op {
}

impl Op {
#[must_use]
pub fn new(
uuid: Uuid,
name: String,
Expand Down
3 changes: 2 additions & 1 deletion pumpkin/src/data/op_data.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::{path::Path, sync::LazyLock};

use pumpkin_config::op;
use serde::{Deserialize, Serialize};

use super::{LoadJSONConfiguration, SaveJSONConfiguration};

use crate::data::op;

pub static OPERATOR_CONFIG: LazyLock<tokio::sync::RwLock<OperatorConfig>> =
LazyLock::new(|| tokio::sync::RwLock::new(OperatorConfig::load()));

Expand Down
15 changes: 15 additions & 0 deletions pumpkin/src/data/player_profile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct PlayerProfile {
pub uuid: Uuid,
pub name: String,
}

impl PlayerProfile {
#[must_use]
pub fn new(uuid: Uuid, name: String) -> Self {
Self { uuid, name }
}
}
Loading
Loading