feat: require plugin permissions confirmation

This commit is contained in:
Alexander Medvedev 2026-05-02 18:24:52 +02:00
parent bae072254f
commit f683de2bf8
7 changed files with 167 additions and 24 deletions

1
Cargo.lock generated
View File

@ -2793,6 +2793,7 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bitflags 2.11.1", "bitflags 2.11.1",
"bytes", "bytes",
"colored",
"console-subscriber", "console-subscriber",
"crossbeam", "crossbeam",
"dashmap", "dashmap",

View File

@ -52,6 +52,8 @@ serde.workspace = true
serde_repr.workspace = true serde_repr.workspace = true
serde_json.workspace = true serde_json.workspace = true
colored.workspace = true
bytes.workspace = true bytes.workspace = true
rand.workspace = true rand.workspace = true

View File

@ -220,21 +220,6 @@ impl PumpkinServer {
let rcon = server.advanced_config.networking.rcon.clone(); let rcon = server.advanced_config.networking.rcon.clone();
if server.advanced_config.commands.use_console
&& let Some((wrapper, _, _)) = LOGGER_IMPL.wait()
{
if let Some(rl) = wrapper.take_readline() {
setup_console(rl, server.clone());
} else {
if server.advanced_config.commands.use_tty {
warn!(
"The input is not a TTY; falling back to simple logger and ignoring `use_tty` setting"
);
}
setup_stdin_console(server.clone());
}
}
if rcon.enabled { if rcon.enabled {
warn!( warn!(
"RCON is enabled, but it's highly insecure as it transmits passwords and commands in plain text. This makes it vulnerable to interception and exploitation by anyone on the network" "RCON is enabled, but it's highly insecure as it transmits passwords and commands in plain text. This makes it vulnerable to interception and exploitation by anyone on the network"
@ -324,7 +309,7 @@ impl PumpkinServer {
} }
} }
pub async fn init_plugins(&self) { pub async fn init_plugins(&self) -> std::time::Duration {
self.server self.server
.plugin_manager .plugin_manager
.set_self_ref(self.server.plugin_manager.clone()) .set_self_ref(self.server.plugin_manager.clone())
@ -333,8 +318,12 @@ impl PumpkinServer {
.plugin_manager .plugin_manager
.set_server(self.server.clone()) .set_server(self.server.clone())
.await; .await;
if let Err(err) = self.server.plugin_manager.load_plugins().await { match self.server.plugin_manager.load_plugins().await {
error!("{err}"); Ok(duration) => duration,
Err(err) => {
error!("{err}");
std::time::Duration::ZERO
}
} }
} }
@ -347,6 +336,21 @@ impl PumpkinServer {
} }
pub async fn start(&self) { pub async fn start(&self) {
if self.server.advanced_config.commands.use_console
&& let Some((wrapper, _, _)) = LOGGER_IMPL.wait()
{
if let Some(rl) = wrapper.take_readline() {
setup_console(rl, self.server.clone());
} else {
if self.server.advanced_config.commands.use_tty {
warn!(
"The input is not a TTY; falling back to simple logger and ignoring `use_tty` setting"
);
}
setup_stdin_console(self.server.clone());
}
}
let tasks = Arc::new(TaskTracker::new()); let tasks = Arc::new(TaskTracker::new());
let mut master_client_id: u64 = 0; let mut master_client_id: u64 = 0;
let bedrock_clients = Arc::new(Mutex::new(HashMap::new())); let bedrock_clients = Arc::new(Mutex::new(HashMap::new()));

View File

@ -325,7 +325,7 @@ impl ReadlineLogWrapper {
// This isn't really dead code. It is just only used by the lib and not the bin for this // This isn't really dead code. It is just only used by the lib and not the bin for this
// crate, and as such creates a compiler warning. // crate, and as such creates a compiler warning.
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) fn return_readline(&self, rl: Editor<PumpkinCommandCompleter, FileHistory>) { pub fn return_readline(&self, rl: Editor<PumpkinCommandCompleter, FileHistory>) {
if let Ok(mut result) = self.readline.lock() { if let Ok(mut result) = self.readline.lock() {
let _ = result.insert(rl); let _ = result.insert(rl);
} }

View File

@ -121,11 +121,13 @@ async fn main() {
}); });
let pumpkin_server = PumpkinServer::new(basic_config, advanced_config, vanilla_data).await; let pumpkin_server = PumpkinServer::new(basic_config, advanced_config, vanilla_data).await;
pumpkin_server.init_plugins().await; let plugin_wait_time = pumpkin_server.init_plugins().await;
let time_elapsed = time.elapsed().saturating_sub(plugin_wait_time);
info!( info!(
"Started server; took {}", "Started server; took {}",
TextComponent::text(format!("{}ms", time.elapsed().as_millis())) TextComponent::text(format!("{}ms", time_elapsed.as_millis()))
.color_named(NamedColor::Gold) .color_named(NamedColor::Gold)
.to_pretty_console() .to_pretty_console()
); );

View File

@ -466,6 +466,70 @@ impl PluginManager {
Ok(sorted) Ok(sorted)
} }
/// Ask the server owner if they allow the permissions requested by a plugin
#[expect(clippy::print_stdout)]
fn ask_permission_confirmation(metadata: &PluginMetadata) -> (bool, std::time::Duration) {
use colored::Colorize;
use rustyline::DefaultEditor;
if metadata.permissions.is_empty() {
return (true, std::time::Duration::ZERO);
}
let start_time = std::time::Instant::now();
println!(
"\n{} \"{}\" ({}) requests the following permissions:",
"Plugin".bold(),
metadata.name.cyan(),
metadata.version.green()
);
for permission in &metadata.permissions {
if let Some(description) = permissions::get_permission_description(permission) {
println!(
" - {}: {}",
permission.yellow().bold(),
description.italic()
);
} else {
println!(" - {}", permission.yellow().bold());
}
}
let prompt = format!(
"\n{} [Y/n]: ",
"Do you want to allow these permissions and load the plugin?".bold()
);
let mut rl_taken = if let Some(logger_option) = crate::LOGGER_IMPL.get()
&& let Some((wrapper, _, _)) = logger_option
&& let Some(rl) = wrapper.take_readline()
{
Some((wrapper, rl))
} else {
None
};
let result = if let Some((_, ref mut rl)) = rl_taken {
rl.readline(&prompt).is_ok_and(|line| {
let input = line.trim().to_lowercase();
input == "y" || input == "yes"
})
} else {
let mut rl = DefaultEditor::new().expect("Failed to create rustyline editor");
rl.readline(&prompt).is_ok_and(|line| {
let input = line.trim().to_lowercase();
input == "y" || input == "yes"
})
};
if let Some((wrapper, rl)) = rl_taken {
wrapper.return_readline(rl);
}
(result, start_time.elapsed())
}
/// Spawn initialization for a single plugin /// Spawn initialization for a single plugin
#[expect(clippy::too_many_lines)] #[expect(clippy::too_many_lines)]
async fn spawn_plugin_initialization( async fn spawn_plugin_initialization(
@ -599,12 +663,12 @@ impl PluginManager {
} }
/// Load all plugins from the plugin directory /// Load all plugins from the plugin directory
pub async fn load_plugins(&self) -> Result<(), ManagerError> { pub async fn load_plugins(&self) -> Result<std::time::Duration, ManagerError> {
let path = Path::new(PLUGIN_DIR); let path = Path::new(PLUGIN_DIR);
if !path.exists() { if !path.exists() {
std::fs::create_dir(path)?; std::fs::create_dir(path)?;
return Ok(()); return Ok(std::time::Duration::ZERO);
} }
let mut prepared_plugins = Vec::new(); let mut prepared_plugins = Vec::new();
@ -677,9 +741,22 @@ impl PluginManager {
.map(|(i, m, d, l, p)| (m.name.clone(), (i, m, d, l, p))) .map(|(i, m, d, l, p)| (m.name.clone(), (i, m, d, l, p)))
.collect(); .collect();
let mut total_wait_time = std::time::Duration::ZERO;
for name in sorted_names { for name in sorted_names {
if let Some((instance, metadata, loader_data, loader, path)) = plugins_map.remove(&name) if let Some((instance, metadata, loader_data, loader, path)) = plugins_map.remove(&name)
{ {
let (allowed, wait_time) = Self::ask_permission_confirmation(&metadata);
total_wait_time += wait_time;
if !allowed {
warn!(
"Permission denied for plugin \"{}\", skipping loading.",
metadata.name
);
continue;
}
match self match self
.spawn_plugin_initialization(instance, metadata, loader_data, loader, path) .spawn_plugin_initialization(instance, metadata, loader_data, loader, path)
.await .await
@ -695,7 +772,7 @@ impl PluginManager {
} }
} }
Ok(()) Ok(total_wait_time)
} }
/// Start loading a plugin asynchronously /// Start loading a plugin asynchronously
@ -706,6 +783,18 @@ impl PluginManager {
for loader in self.loaders.read().await.iter() { for loader in self.loaders.read().await.iter() {
if loader.can_load(path) { if loader.can_load(path) {
let (instance, metadata, loader_data) = loader.load(path).await?; let (instance, metadata, loader_data) = loader.load(path).await?;
let (allowed, _wait_time) = Self::ask_permission_confirmation(&metadata);
if !allowed {
warn!(
"Permission denied for plugin \"{}\", skipping loading.",
metadata.name
);
return Err(ManagerError::LoaderError(LoaderError::RuntimeError(
"Permission denied".to_string(),
)));
}
return self return self
.spawn_plugin_initialization( .spawn_plugin_initialization(
instance, instance,

View File

@ -64,3 +64,48 @@ pub const SYS_INFO_RAM: &str = "sys.info.ram";
/// Allows the plugin to read OS information. /// Allows the plugin to read OS information.
pub const SYS_INFO_OS: &str = "sys.info.os"; pub const SYS_INFO_OS: &str = "sys.info.os";
#[must_use]
pub fn get_permission_description(permission: &str) -> Option<&'static str> {
match permission {
NETWORK_DNS => Some(
"Allows the plugin to perform DNS resolution (resolving hostnames to IP addresses).",
),
NETWORK_TCP => Some("Allows the plugin to use TCP sockets."),
NETWORK_UDP => Some("Allows the plugin to use UDP sockets."),
NETWORK_TCP_CONNECT => Some("Allows the plugin to initiate TCP connections."),
NETWORK_TCP_BIND => {
Some("Allows the plugin to bind TCP listeners (accept inbound connections).")
}
NETWORK_UDP_CONNECT => {
Some("Allows the plugin to send and receive UDP packets to specific destinations.")
}
NETWORK_UDP_BIND => Some("Allows the plugin to bind UDP sockets to local ports."),
NETWORK_UDP_OUTGOING_DATAGRAM => {
Some("Allows the plugin to send datagram on non-connected UDP socket.")
}
NETWORK_LOOPBACK => {
Some("Restricts all networking permissions to loopback addresses (localhost) only.")
}
NETWORK_OUTBOUND => {
Some("Allows the plugin to make outbound TCP/UDP connections. (POWERFUL)")
}
FS_READ => Some(
"Allows the plugin to read files from the server's file system outside of its data folder.",
),
FS_WRITE => Some(
"Allows the plugin to write files to the server's file system outside of its data folder.",
),
FS_READ_DATA => Some("Allows the plugin to read files within its own data folder."),
FS_WRITE_DATA => Some("Allows the plugin to write files within its own data folder."),
SYS_ENV => Some("Allows the plugin to read all environment variables."),
SYS_INFO => Some("Allows the plugin to read system information (CPU, Memory, OS)."),
SYS_INFO_CPU => Some("Allows the plugin to read CPU information."),
SYS_INFO_RAM => Some("Allows the plugin to read RAM information."),
SYS_INFO_OS => Some("Allows the plugin to read OS information."),
p if p.starts_with(SYS_ENV_PREFIX) => {
Some("Allows the plugin to read specific environment variables.")
}
_ => None,
}
}