diff --git a/pumpkin-api-macros/src/lib.rs b/pumpkin-api-macros/src/lib.rs index 69fa1d2c9..018ccff7f 100644 --- a/pumpkin-api-macros/src/lib.rs +++ b/pumpkin-api-macros/src/lib.rs @@ -88,6 +88,7 @@ pub fn plugin_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { authors: env!("CARGO_PKG_AUTHORS").split(',').map(String::from).collect(), description: env!("CARGO_PKG_DESCRIPTION").to_string(), dependencies: Vec::new(), + permissions: Vec::new(), } }); diff --git a/pumpkin-config/src/lib.rs b/pumpkin-config/src/lib.rs index a07b51db1..419cb7967 100644 --- a/pumpkin-config/src/lib.rs +++ b/pumpkin-config/src/lib.rs @@ -12,6 +12,7 @@ use tracing::{debug, warn}; pub mod fun; pub mod logging; pub mod networking; +pub mod plugins; pub mod recipe; pub mod resource_pack; @@ -22,6 +23,7 @@ pub use networking::auth::AuthenticationConfig; pub use networking::compression::CompressionConfig; pub use networking::lan_broadcast::LANBroadcastConfig; pub use networking::rcon::RCONConfig; +pub use plugins::PluginsConfig; pub use pvp::PVPConfig; pub use server_links::ServerLinksConfig; @@ -74,6 +76,8 @@ pub struct AdvancedConfiguration { pub fun: FunConfig, /// Recipe-related configuration. pub recipe: RecipeConfig, + /// Plugin-related configuration. + pub plugins: PluginsConfig, } /// Basic configuration for core server settings. diff --git a/pumpkin-config/src/plugins.rs b/pumpkin-config/src/plugins.rs new file mode 100644 index 000000000..1489fb612 --- /dev/null +++ b/pumpkin-config/src/plugins.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Default)] +#[serde(default)] +pub struct PluginsConfig { + /// List of permissions that are globally blocked for all plugins. + pub blocked_permissions: Vec, +} diff --git a/pumpkin-plugin-api/src/lib.rs b/pumpkin-plugin-api/src/lib.rs index 5fe5e07c1..406bd393b 100644 --- a/pumpkin-plugin-api/src/lib.rs +++ b/pumpkin-plugin-api/src/lib.rs @@ -7,7 +7,7 @@ //! # Quick start //! //! ```rust,ignore -//! use pumpkin_plugin_api::{Plugin, PluginMetadata, Context, register_plugin}; +//! use pumpkin_plugin_api::{Plugin, PluginMetadata, Context, register_plugin, permissions::permissions}; //! //! struct MyPlugin; //! @@ -20,6 +20,7 @@ //! authors: vec!["you".into()], //! description: "An example plugin.".into(), //! dependencies: vec![], +//! permissions: vec![permissions::NETWORK_DNS.into()], //! } //! } //! } @@ -34,6 +35,10 @@ use crate::{ pub mod commands; pub mod events; +/// Constants for plugin permissions. +/// +/// Use these in your `PluginMetadata` to request access to specific host features. +pub mod permissions; pub mod scheduler; pub mod command { @@ -77,6 +82,8 @@ pub struct PluginMetadata { pub description: String, /// The list of plugin dependencies. pub dependencies: Vec, + /// The list of permissions requested by the plugin. + pub permissions: Vec, } impl wit::exports::pumpkin::plugin::metadata::Guest for Component { @@ -89,6 +96,7 @@ impl wit::exports::pumpkin::plugin::metadata::Guest for Component { authors: metadata.authors, description: metadata.description, dependencies: metadata.dependencies, + permissions: metadata.permissions, } } } diff --git a/pumpkin-plugin-api/src/permissions.rs b/pumpkin-plugin-api/src/permissions.rs new file mode 100644 index 000000000..9578bfd58 --- /dev/null +++ b/pumpkin-plugin-api/src/permissions.rs @@ -0,0 +1,65 @@ +/// Constants for plugin permissions. +/// +/// Use these in your `PluginMetadata` to request access to specific host features. + +/// Allows the plugin to perform DNS resolution. +pub const NETWORK_DNS: &str = "network.dns"; + +/// Allows the plugin to use TCP sockets. +pub const NETWORK_TCP: &str = "network.tcp"; + +/// Allows the plugin to use UDP sockets. +pub const NETWORK_UDP: &str = "network.udp"; + +/// Allows the plugin to initiate TCP connections. +pub const NETWORK_TCP_CONNECT: &str = "network.tcp.connect"; + +/// Allows the plugin to bind TCP listeners (accept inbound connections). +pub const NETWORK_TCP_BIND: &str = "network.tcp.bind"; + +/// Allows the plugin to send and receive UDP packets to specific destinations. +pub const NETWORK_UDP_CONNECT: &str = "network.udp.connect"; + +/// Allows the plugin to bind UDP sockets to local ports. +pub const NETWORK_UDP_BIND: &str = "network.udp.bind"; + +/// Allows the plugin to send datagram on non-connected UDP socket. +pub const NETWORK_UDP_OUTGOING_DATAGRAM: &str = "network.udp.outgoingdatagram"; + +/// Restricts all networking permissions to loopback addresses (localhost) only. +pub const NETWORK_LOOPBACK: &str = "network.loopback"; + +/// Allows the plugin to make outbound TCP/UDP connections. +/// **Warning:** This is a powerful permission. +pub const NETWORK_OUTBOUND: &str = "network.outbound"; + +/// Allows the plugin to read files from the server's file system outside of its data folder. +pub const FS_READ: &str = "fs.read"; + +/// Allows the plugin to write files to the server's file system outside of its data folder. +pub const FS_WRITE: &str = "fs.write"; + +/// Allows the plugin to read files within its own data folder (`plugins/`). +pub const FS_READ_DATA: &str = "fs.read.data"; + +/// Allows the plugin to write files within its own data folder (`plugins/`). +pub const FS_WRITE_DATA: &str = "fs.write.data"; + +/// Allows the plugin to read all environment variables. +pub const SYS_ENV: &str = "sys.env"; + +/// Allows the plugin to read specific environment variables. +/// Used with a prefix like "sys.env.PATH". +pub const SYS_ENV_PREFIX: &str = "sys.env."; + +/// Allows the plugin to read system information (CPU, Memory, OS). +pub const SYS_INFO: &str = "sys.info"; + +/// Allows the plugin to read CPU information. +pub const SYS_INFO_CPU: &str = "sys.info.cpu"; + +/// Allows the plugin to read RAM information. +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"; diff --git a/pumpkin-plugin-wit/v0.1/metadata.wit b/pumpkin-plugin-wit/v0.1/metadata.wit index 697d1af76..b48863704 100644 --- a/pumpkin-plugin-wit/v0.1/metadata.wit +++ b/pumpkin-plugin-wit/v0.1/metadata.wit @@ -11,6 +11,8 @@ interface metadata { description: string, /// Plugin dependencies. dependencies: list, + /// Permissions requested by the plugin. + permissions: list, } get-metadata: func() -> plugin-metadata; diff --git a/pumpkin-plugin-wit/v0.1/server.wit b/pumpkin-plugin-wit/v0.1/server.wit index a84343ca8..790e70860 100644 --- a/pumpkin-plugin-wit/v0.1/server.wit +++ b/pumpkin-plugin-wit/v0.1/server.wit @@ -23,6 +23,15 @@ interface server { end, } + record sys-info { + cpu-count: option, + total-memory: option, + used-memory: option, + os-name: option, + os-version: option, + pumpkin-version: string, + } + variant command-sender { console, player(player), @@ -30,6 +39,10 @@ interface server { /// Represents the global server instance. resource server { + /// Returns system information (CPU, Memory, OS). + /// Fields are returned only if the plugin has the corresponding permissions. + get-sys-info: func() -> sys-info; + /// Returns the current difficulty level of the server. get-difficulty: func() -> difficulty; diff --git a/pumpkin/src/plugin/api/context.rs b/pumpkin/src/plugin/api/context.rs index a41fbc5cf..2323bd6d0 100644 --- a/pumpkin/src/plugin/api/context.rs +++ b/pumpkin/src/plugin/api/context.rs @@ -66,6 +66,11 @@ impl Context { } } + #[must_use] + pub const fn get_metadata(&self) -> &PluginMetadata { + &self.metadata + } + /// Retrieves the data folder path for the plugin, creating it if it does not exist. /// /// # Returns diff --git a/pumpkin/src/plugin/api/mod.rs b/pumpkin/src/plugin/api/mod.rs index b4630ba59..fb2402489 100644 --- a/pumpkin/src/plugin/api/mod.rs +++ b/pumpkin/src/plugin/api/mod.rs @@ -24,6 +24,8 @@ pub struct PluginMetadata { pub description: String, /// The dependencies of the plugin. pub dependencies: Vec, + /// The permissions requested by the plugin. + pub permissions: Vec, } /// This type represents a future for the plugin. diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs index e76b0bbf3..2f413c656 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs @@ -2,9 +2,11 @@ use std::{fs, path::Path, sync::Arc}; use thiserror::Error; use tokio::sync::Mutex; use wasmtime::{Cache, CacheConfig, Engine, Store, component::Component, component::Linker}; -use wasmtime_wasi::WasiCtxBuilder; +use wasmtime_wasi::{WasiCtxBuilder, sockets::SocketAddrUse}; -use crate::plugin::{Context, PluginMetadata, loader::wasm::wasm_host::state::PluginHostState}; +use crate::plugin::{ + Context, PluginMetadata, loader::wasm::wasm_host::state::PluginHostState, permissions, +}; pub mod args; pub mod logging; @@ -144,13 +146,117 @@ impl WasmPlugin { let mut builder = WasiCtxBuilder::new(); + let metadata = context.get_metadata(); + let blocked_permissions = &context.server.advanced_config.plugins.blocked_permissions; + + let filtered_permissions: Vec = metadata + .permissions + .iter() + .filter(|p| !blocked_permissions.iter().any(|blocked| blocked == *p)) + .cloned() + .collect(); + + let has_permission = |p: &str| filtered_permissions.iter().any(|perm| perm == p); + + if has_permission(permissions::NETWORK_DNS) { + builder.allow_ip_name_lookup(true); + } + + let tcp_allowed = has_permission(permissions::NETWORK_TCP); + let udp_allowed = has_permission(permissions::NETWORK_UDP); + let tcp_connect = tcp_allowed || has_permission(permissions::NETWORK_TCP_CONNECT); + let tcp_bind = tcp_allowed || has_permission(permissions::NETWORK_TCP_BIND); + let udp_connect = udp_allowed || has_permission(permissions::NETWORK_UDP_CONNECT); + let udp_bind = udp_allowed || has_permission(permissions::NETWORK_UDP_BIND); + let udp_outgoing_datagram = + udp_allowed || has_permission(permissions::NETWORK_UDP_OUTGOING_DATAGRAM); + + let loopback_only = has_permission(permissions::NETWORK_LOOPBACK); + + builder.allow_tcp(tcp_connect || tcp_bind); + builder.allow_udp(udp_connect || udp_bind); + + builder.socket_addr_check(move |addr, reason| { + Box::pin(async move { + let ok = match reason { + SocketAddrUse::TcpConnect => tcp_connect, + SocketAddrUse::TcpBind => tcp_bind, + SocketAddrUse::UdpConnect => udp_connect, + SocketAddrUse::UdpBind => udp_bind, + SocketAddrUse::UdpOutgoingDatagram => udp_outgoing_datagram, + }; + + if loopback_only { + ok && addr.ip().is_loopback() + } else { + ok + } + }) + }); + + if has_permission(permissions::NETWORK_OUTBOUND) { + builder.inherit_network(); + } + + // --- System Permissions --- + + // Environment Variables + if has_permission(permissions::SYS_ENV) { + builder.inherit_env(); + } else { + for (key, value) in std::env::vars() { + let perm = format!("{}{}", permissions::SYS_ENV_PREFIX, key); + if has_permission(&perm) { + builder.env(key, value); + } + } + } + + let data_folder = context.get_data_folder(); + let preopen_path = + if has_permission(permissions::FS_READ) || has_permission(permissions::FS_WRITE) { + Path::new(".") + } else { + data_folder.as_path() + }; + + // Determine permissions for the preopened directory + let (dir_perms, file_perms) = if has_permission(permissions::FS_WRITE) { + ( + wasmtime_wasi::DirPerms::all(), + wasmtime_wasi::FilePerms::all(), + ) + } else if has_permission(permissions::FS_READ) { + ( + wasmtime_wasi::DirPerms::READ, + wasmtime_wasi::FilePerms::READ, + ) + } else { + // Scoped to data folder + let can_write = has_permission(permissions::FS_WRITE_DATA); + if can_write { + ( + wasmtime_wasi::DirPerms::all(), + wasmtime_wasi::FilePerms::all(), + ) + } else { + // Default to READ if no write permission is given for data folder + // (Plugins should at least be able to read their own config) + ( + wasmtime_wasi::DirPerms::READ, + wasmtime_wasi::FilePerms::READ, + ) + } + }; + builder.preopened_dir( - context.get_data_folder(), - context.get_data_folder().to_string_lossy(), - wasmtime_wasi::DirPerms::all(), - wasmtime_wasi::FilePerms::all(), + preopen_path, + preopen_path.to_string_lossy(), + dir_perms, + file_perms, )?; + store.data_mut().permissions = filtered_permissions; store.data_mut().wasi_ctx = builder.build(); store.data_mut().server = Some(context.server.clone()); diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs index 1ee3ef93b..7956264be 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs @@ -52,6 +52,7 @@ pub struct PluginHostState { pub resource_table: ResourceTable, pub plugin: Option>, pub server: Option>, + pub permissions: Vec, } impl Default for PluginHostState { @@ -69,6 +70,7 @@ impl PluginHostState { resource_table, plugin: None, server: None, + permissions: Vec::new(), } } diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs index 8bc77cec8..c04b02627 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs @@ -62,8 +62,11 @@ pub async fn init_plugin( authors: metadata.authors, description: metadata.description, dependencies: metadata.dependencies, + permissions: metadata.permissions.clone(), }; + store.data_mut().permissions = metadata.permissions.clone(); + Ok(( WasmPlugin { plugin_instance: PluginInstance::V0_1(plugin), diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs index b7dc2cd9e..01c4ec024 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs @@ -6,15 +6,18 @@ use crate::command::CommandSender; use pumpkin::plugin::server::CommandSender as WasmCommandSender; use super::player::text_component_from_resource; -use crate::plugin::loader::wasm::wasm_host::{ - state::{PluginHostState, ServerResource}, - wit::v0_1::pumpkin::{ - self, - plugin::{ - player::Player, - server::{Difficulty, Dimension, Server}, +use crate::plugin::{ + loader::wasm::wasm_host::{ + state::{PluginHostState, ServerResource}, + wit::v0_1::pumpkin::{ + self, + plugin::{ + player::Player, + server::{Difficulty, Dimension, Server, SysInfo}, + }, }, }, + permissions, }; impl PluginHostState { @@ -28,6 +31,42 @@ impl PluginHostState { impl pumpkin::plugin::server::Host for PluginHostState {} impl pumpkin::plugin::server::HostServer for PluginHostState { + async fn get_sys_info(&mut self, _res: Resource) -> wasmtime::Result { + let has_perm = |p: &str| self.permissions.iter().any(|perm| perm == p); + + let mut sys = sysinfo::System::new_all(); + sys.refresh_all(); + + let cpu_count = if has_perm(permissions::SYS_INFO) || has_perm(permissions::SYS_INFO_CPU) { + Some(sys.cpus().len() as u32) + } else { + None + }; + + let (total_memory, used_memory) = + if has_perm(permissions::SYS_INFO) || has_perm(permissions::SYS_INFO_RAM) { + (Some(sys.total_memory()), Some(sys.used_memory())) + } else { + (None, None) + }; + + let (os_name, os_version) = + if has_perm(permissions::SYS_INFO) || has_perm(permissions::SYS_INFO_OS) { + (sysinfo::System::name(), sysinfo::System::os_version()) + } else { + (None, None) + }; + + Ok(SysInfo { + cpu_count, + total_memory, + used_memory, + os_name, + os_version, + pumpkin_version: env!("CARGO_PKG_VERSION").to_string(), + }) + } + async fn get_difficulty(&mut self, res: Resource) -> wasmtime::Result { let resource = self.get_server_res(&res)?; diff --git a/pumpkin/src/plugin/mod.rs b/pumpkin/src/plugin/mod.rs index 37d4477a9..004409750 100644 --- a/pumpkin/src/plugin/mod.rs +++ b/pumpkin/src/plugin/mod.rs @@ -14,10 +14,15 @@ use tokio::{ sync::{Notify, RwLock}, task::JoinHandle, }; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; pub mod api; pub mod loader; +/// Constants for plugin permissions. +/// +/// Plugins can request these permissions in their metadata to access specific +/// host features. +pub mod permissions; use crate::{LOGGER_IMPL, plugin::loader::wasm::WasmPluginLoader, server::Server}; pub use api::*; @@ -462,6 +467,7 @@ impl PluginManager { } /// Spawn initialization for a single plugin + #[expect(clippy::too_many_lines)] async fn spawn_plugin_initialization( &self, mut instance: Box, @@ -541,6 +547,13 @@ impl PluginManager { state_notify.notify_waiters(); info!("Loaded {} ({})", metadata.name, metadata.version); + + if !metadata.permissions.is_empty() { + warn!( + "Plugin \"{}\" uses the following permissions: {:?}", + metadata.name, metadata.permissions + ); + } } Err(e) => { // Handle initialization failure diff --git a/pumpkin/src/plugin/permissions.rs b/pumpkin/src/plugin/permissions.rs new file mode 100644 index 000000000..917539540 --- /dev/null +++ b/pumpkin/src/plugin/permissions.rs @@ -0,0 +1,71 @@ +/// Constants for plugin permissions. +/// +/// Plugins can request these permissions in their metadata to access specific +/// host features. + +/// Allows the plugin to perform DNS resolution (resolving hostnames to IP addresses). +/// +/// Corresponds to the `wasi:sockets/ip-name-lookup` interface. +pub const NETWORK_DNS: &str = "network.dns"; + +/// Allows the plugin to use TCP sockets. +pub const NETWORK_TCP: &str = "network.tcp"; + +/// Allows the plugin to use UDP sockets. +pub const NETWORK_UDP: &str = "network.udp"; + +/// Allows the plugin to initiate TCP connections. +pub const NETWORK_TCP_CONNECT: &str = "network.tcp.connect"; + +/// Allows the plugin to bind TCP listeners (accept inbound connections). +pub const NETWORK_TCP_BIND: &str = "network.tcp.bind"; + +/// Allows the plugin to send and receive UDP packets to specific destinations. +pub const NETWORK_UDP_CONNECT: &str = "network.udp.connect"; + +/// Allows the plugin to bind UDP sockets to local ports. +pub const NETWORK_UDP_BIND: &str = "network.udp.bind"; + +/// Allows the plugin to send datagram on non-connected UDP socket. +pub const NETWORK_UDP_OUTGOING_DATAGRAM: &str = "network.udp.outgoingdatagram"; + +/// Restricts all networking permissions to loopback addresses (localhost) only. +pub const NETWORK_LOOPBACK: &str = "network.loopback"; + +/// Allows the plugin to make outbound TCP/UDP connections. +/// +/// This gives the plugin full access to the host's network interfaces. +/// **Warning:** This is a powerful permission and should only be granted to trusted plugins. +pub const NETWORK_OUTBOUND: &str = "network.outbound"; + +/// Allows the plugin to read files from the server's file system outside of its data folder. +pub const FS_READ: &str = "fs.read"; + +/// Allows the plugin to write files to the server's file system outside of its data folder. +pub const FS_WRITE: &str = "fs.write"; + +/// Allows the plugin to read files within its own data folder (`plugins/`). +/// This is granted by default if any other FS permission is not specified, but can be explicitly requested. +pub const FS_READ_DATA: &str = "fs.read.data"; + +/// Allows the plugin to write files within its own data folder (`plugins/`). +pub const FS_WRITE_DATA: &str = "fs.write.data"; + +/// Allows the plugin to read all environment variables. +pub const SYS_ENV: &str = "sys.env"; + +/// Allows the plugin to read specific environment variables. +/// Used with a prefix like "sys.env.PATH". +pub const SYS_ENV_PREFIX: &str = "sys.env."; + +/// Allows the plugin to read system information (CPU, Memory, OS). +pub const SYS_INFO: &str = "sys.info"; + +/// Allows the plugin to read CPU information. +pub const SYS_INFO_CPU: &str = "sys.info.cpu"; + +/// Allows the plugin to read RAM information. +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";