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",
|
||||
"bitflags 2.11.1",
|
||||
"bytes",
|
||||
"colored",
|
||||
"console-subscriber",
|
||||
"crossbeam",
|
||||
"dashmap",
|
||||
|
||||
@ -52,6 +52,8 @@ serde.workspace = true
|
||||
serde_repr.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
colored.workspace = true
|
||||
|
||||
bytes.workspace = true
|
||||
|
||||
rand.workspace = true
|
||||
|
||||
@ -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()));
|
||||
|
||||
@ -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<PumpkinCommandCompleter, FileHistory>) {
|
||||
pub fn return_readline(&self, rl: Editor<PumpkinCommandCompleter, FileHistory>) {
|
||||
if let Ok(mut result) = self.readline.lock() {
|
||||
let _ = result.insert(rl);
|
||||
}
|
||||
|
||||
@ -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()
|
||||
);
|
||||
|
||||
@ -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<std::time::Duration, ManagerError> {
|
||||
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,
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user