From f683de2bf8f89f17c5b151a88b3c0c57dacff8b5 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sat, 2 May 2026 18:24:52 +0200 Subject: [PATCH] feat: require plugin permissions confirmation --- Cargo.lock | 1 + pumpkin/Cargo.toml | 2 + pumpkin/src/lib.rs | 40 +++++++------ pumpkin/src/logging.rs | 2 +- pumpkin/src/main.rs | 6 +- pumpkin/src/plugin/mod.rs | 95 ++++++++++++++++++++++++++++++- pumpkin/src/plugin/permissions.rs | 45 +++++++++++++++ 7 files changed, 167 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 860e5b26f..ca70e06b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2793,6 +2793,7 @@ dependencies = [ "base64 0.22.1", "bitflags 2.11.1", "bytes", + "colored", "console-subscriber", "crossbeam", "dashmap", diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 05649800f..9ac3a314e 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -52,6 +52,8 @@ serde.workspace = true serde_repr.workspace = true serde_json.workspace = true +colored.workspace = true + bytes.workspace = true rand.workspace = true diff --git a/pumpkin/src/lib.rs b/pumpkin/src/lib.rs index 0d6657fa5..e94de7a04 100644 --- a/pumpkin/src/lib.rs +++ b/pumpkin/src/lib.rs @@ -220,21 +220,6 @@ impl PumpkinServer { 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 { 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" @@ -324,7 +309,7 @@ impl PumpkinServer { } } - pub async fn init_plugins(&self) { + pub async fn init_plugins(&self) -> std::time::Duration { self.server .plugin_manager .set_self_ref(self.server.plugin_manager.clone()) @@ -333,8 +318,12 @@ impl PumpkinServer { .plugin_manager .set_server(self.server.clone()) .await; - if let Err(err) = self.server.plugin_manager.load_plugins().await { - error!("{err}"); + match self.server.plugin_manager.load_plugins().await { + Ok(duration) => duration, + Err(err) => { + error!("{err}"); + std::time::Duration::ZERO + } } } @@ -347,6 +336,21 @@ impl PumpkinServer { } 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 mut master_client_id: u64 = 0; let bedrock_clients = Arc::new(Mutex::new(HashMap::new())); diff --git a/pumpkin/src/logging.rs b/pumpkin/src/logging.rs index df4e44125..63638e6a0 100644 --- a/pumpkin/src/logging.rs +++ b/pumpkin/src/logging.rs @@ -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 // crate, and as such creates a compiler warning. #[allow(dead_code)] - pub(crate) fn return_readline(&self, rl: Editor) { + pub fn return_readline(&self, rl: Editor) { if let Ok(mut result) = self.readline.lock() { let _ = result.insert(rl); } diff --git a/pumpkin/src/main.rs b/pumpkin/src/main.rs index 2e4dbd65d..23f15d687 100644 --- a/pumpkin/src/main.rs +++ b/pumpkin/src/main.rs @@ -121,11 +121,13 @@ async fn main() { }); 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!( "Started server; took {}", - TextComponent::text(format!("{}ms", time.elapsed().as_millis())) + TextComponent::text(format!("{}ms", time_elapsed.as_millis())) .color_named(NamedColor::Gold) .to_pretty_console() ); diff --git a/pumpkin/src/plugin/mod.rs b/pumpkin/src/plugin/mod.rs index 004409750..f7066d022 100644 --- a/pumpkin/src/plugin/mod.rs +++ b/pumpkin/src/plugin/mod.rs @@ -466,6 +466,70 @@ impl PluginManager { 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 #[expect(clippy::too_many_lines)] async fn spawn_plugin_initialization( @@ -599,12 +663,12 @@ impl PluginManager { } /// Load all plugins from the plugin directory - pub async fn load_plugins(&self) -> Result<(), ManagerError> { + pub async fn load_plugins(&self) -> Result { let path = Path::new(PLUGIN_DIR); if !path.exists() { std::fs::create_dir(path)?; - return Ok(()); + return Ok(std::time::Duration::ZERO); } 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))) .collect(); + let mut total_wait_time = std::time::Duration::ZERO; + for name in sorted_names { 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 .spawn_plugin_initialization(instance, metadata, loader_data, loader, path) .await @@ -695,7 +772,7 @@ impl PluginManager { } } - Ok(()) + Ok(total_wait_time) } /// Start loading a plugin asynchronously @@ -706,6 +783,18 @@ impl PluginManager { for loader in self.loaders.read().await.iter() { if loader.can_load(path) { 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 .spawn_plugin_initialization( instance, diff --git a/pumpkin/src/plugin/permissions.rs b/pumpkin/src/plugin/permissions.rs index 7572dc513..e9d632c10 100644 --- a/pumpkin/src/plugin/permissions.rs +++ b/pumpkin/src/plugin/permissions.rs @@ -64,3 +64,48 @@ pub const SYS_INFO_RAM: &str = "sys.info.ram"; /// Allows the plugin to read OS information. 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, + } +}