feat: require plugin permissions confirmation
This commit is contained in:
parent
bae072254f
commit
f683de2bf8
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
Ok(duration) => duration,
|
||||||
|
Err(err) => {
|
||||||
error!("{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()));
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user