mirror of
https://github.com/timvisee/lazymc.git
synced 2025-05-19 04:40:22 -07:00
Add server whitelist support, use generic server files watcher to reload
This commit is contained in:
parent
69de7a95bf
commit
c477e45553
@ -50,6 +50,9 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
|
||||
#start_timeout = 300
|
||||
#stop_timeout = 150
|
||||
|
||||
# To wake server, user must be in server whitelist if enabled on server.
|
||||
#wake_whitelist = true
|
||||
|
||||
# Block banned IPs as listed in banned-ips.json in server directory.
|
||||
#block_banned_ips = true
|
||||
|
||||
|
@ -198,6 +198,10 @@ pub struct Server {
|
||||
#[serde(default = "u32_150")]
|
||||
pub stop_timeout: u32,
|
||||
|
||||
/// To wake server, user must be in server whitelist if enabled on server.
|
||||
#[serde(default = "bool_true")]
|
||||
pub wake_whitelist: bool,
|
||||
|
||||
/// Block banned IPs as listed in banned-ips.json in server directory.
|
||||
#[serde(default = "bool_true")]
|
||||
pub block_banned_ips: bool,
|
||||
|
@ -7,6 +7,7 @@ pub mod rcon;
|
||||
pub mod server_properties;
|
||||
#[cfg(feature = "lobby")]
|
||||
pub mod uuid;
|
||||
pub mod whitelist;
|
||||
|
||||
/// Minecraft ticks per second.
|
||||
#[allow(unused)]
|
||||
|
107
src/mc/whitelist.rs
Normal file
107
src/mc/whitelist.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Whitelist file name.
|
||||
pub const WHITELIST_FILE: &str = "whitelist.json";
|
||||
|
||||
/// OPs file name.
|
||||
pub const OPS_FILE: &str = "ops.json";
|
||||
|
||||
/// Whitelisted users.
|
||||
///
|
||||
/// Includes list of OPs, which are also automatically whitelisted.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Whitelist {
|
||||
/// Whitelisted users.
|
||||
whitelist: Vec<String>,
|
||||
|
||||
/// OPd users.
|
||||
ops: Vec<String>,
|
||||
}
|
||||
|
||||
impl Whitelist {
|
||||
/// Check whether a user is whitelisted.
|
||||
pub fn is_whitelisted(&self, username: &str) -> bool {
|
||||
self.whitelist.iter().any(|u| u == username) || self.ops.iter().any(|u| u == username)
|
||||
}
|
||||
}
|
||||
|
||||
/// A whitelist user.
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct WhitelistUser {
|
||||
/// Whitelisted username.
|
||||
#[serde(rename = "name", alias = "username")]
|
||||
pub username: String,
|
||||
|
||||
/// Whitelisted UUID.
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
/// An OP user.
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct OpUser {
|
||||
/// OP username.
|
||||
#[serde(rename = "name", alias = "username")]
|
||||
pub username: String,
|
||||
|
||||
/// OP UUID.
|
||||
pub uuid: Option<String>,
|
||||
|
||||
/// OP level.
|
||||
pub level: Option<u32>,
|
||||
|
||||
/// Whether OP can bypass player limit.
|
||||
#[serde(rename = "bypassesPlayerLimit")]
|
||||
pub byapsses_player_limit: Option<bool>,
|
||||
}
|
||||
|
||||
/// Load whitelist from directory.
|
||||
pub fn load_dir(path: &Path) -> Result<Whitelist, Box<dyn Error>> {
|
||||
let whitelist_file = path.join(WHITELIST_FILE);
|
||||
let ops_file = path.join(OPS_FILE);
|
||||
|
||||
// Load whitelist users
|
||||
let whitelist = if whitelist_file.is_file() {
|
||||
load_whitelist(&whitelist_file)?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Load OPd users
|
||||
let ops = if ops_file.is_file() {
|
||||
load_ops(&ops_file)?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
debug!(target: "lazymc", "Loaded {} whitelist and {} OP users", whitelist.len(), ops.len());
|
||||
|
||||
Ok(Whitelist { whitelist, ops })
|
||||
}
|
||||
|
||||
/// Load whitelist from file.
|
||||
fn load_whitelist(path: &Path) -> Result<Vec<String>, Box<dyn Error>> {
|
||||
// Load file contents
|
||||
let contents = fs::read_to_string(path)?;
|
||||
|
||||
// Parse contents
|
||||
let users: Vec<WhitelistUser> = serde_json::from_str(&contents)?;
|
||||
|
||||
// Pluck usernames
|
||||
Ok(users.into_iter().map(|user| user.username).collect())
|
||||
}
|
||||
|
||||
/// Load OPs from file.
|
||||
fn load_ops(path: &Path) -> Result<Vec<String>, Box<dyn Error>> {
|
||||
// Load file contents
|
||||
let contents = fs::read_to_string(path)?;
|
||||
|
||||
// Parse contents
|
||||
let users: Vec<OpUser> = serde_json::from_str(&contents)?;
|
||||
|
||||
// Pluck usernames
|
||||
Ok(users.into_iter().map(|user| user.username).collect())
|
||||
}
|
@ -14,6 +14,7 @@ use tokio::time;
|
||||
|
||||
use crate::config::{Config, Server as ConfigServer};
|
||||
use crate::mc::ban::{BannedIp, BannedIps};
|
||||
use crate::mc::whitelist::Whitelist;
|
||||
use crate::os;
|
||||
use crate::proto::packets::play::join_game::JoinGameData;
|
||||
|
||||
@ -73,6 +74,9 @@ pub struct Server {
|
||||
/// List of banned IPs.
|
||||
banned_ips: RwLock<BannedIps>,
|
||||
|
||||
/// Whitelist if enabled.
|
||||
whitelist: RwLock<Option<Whitelist>>,
|
||||
|
||||
/// Lock for exclusive RCON operations.
|
||||
#[cfg(feature = "rcon")]
|
||||
rcon_lock: Semaphore,
|
||||
@ -346,6 +350,18 @@ impl Server {
|
||||
futures::executor::block_on(async { self.is_banned_ip(ip).await })
|
||||
}
|
||||
|
||||
/// Check whether the given username is whitelisted.
|
||||
///
|
||||
/// Returns `true` if no whitelist is currently used.
|
||||
pub async fn is_whitelisted(&self, username: &str) -> bool {
|
||||
self.whitelist
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.map(|w| w.is_whitelisted(username))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Update the list of banned IPs.
|
||||
pub async fn set_banned_ips(&self, ips: BannedIps) {
|
||||
*self.banned_ips.write().await = ips;
|
||||
@ -355,6 +371,16 @@ impl Server {
|
||||
pub fn set_banned_ips_blocking(&self, ips: BannedIps) {
|
||||
futures::executor::block_on(async { self.set_banned_ips(ips).await })
|
||||
}
|
||||
|
||||
/// Update the whitelist.
|
||||
pub async fn set_whitelist(&self, whitelist: Option<Whitelist>) {
|
||||
*self.whitelist.write().await = whitelist;
|
||||
}
|
||||
|
||||
/// Update the whitelist.
|
||||
pub fn set_whitelist_blocking(&self, whitelist: Option<Whitelist>) {
|
||||
futures::executor::block_on(async { self.set_whitelist(whitelist).await })
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Server {
|
||||
@ -371,6 +397,7 @@ impl Default for Server {
|
||||
keep_online_until: Default::default(),
|
||||
kill_at: Default::default(),
|
||||
banned_ips: Default::default(),
|
||||
whitelist: Default::default(),
|
||||
#[cfg(feature = "rcon")]
|
||||
rcon_lock: Semaphore::new(1),
|
||||
#[cfg(feature = "rcon")]
|
||||
|
@ -1,119 +0,0 @@
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||
|
||||
use crate::config::{Config, Server as ConfigServer};
|
||||
use crate::mc::ban;
|
||||
use crate::server::Server;
|
||||
|
||||
/// File debounce time.
|
||||
const WATCH_DEBOUNCE: Duration = Duration::from_secs(2);
|
||||
|
||||
/// Service to reload banned IPs when its file changes.
|
||||
pub fn service(config: Arc<Config>, server: Arc<Server>) {
|
||||
// TODO: check what happens when file doesn't exist at first?
|
||||
|
||||
// Ensure we need to reload banned IPs
|
||||
if !config.server.block_banned_ips && !config.server.drop_banned_ips {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure server directory is set, it must exist
|
||||
let dir = match ConfigServer::server_directory(&config) {
|
||||
Some(dir) => dir,
|
||||
None => {
|
||||
warn!(target: "lazymc", "Not blocking banned IPs, server directory not configured, unable to find {} file", ban::FILE);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Determine file path, ensure it exists
|
||||
let path = dir.join(crate::mc::ban::FILE);
|
||||
if !path.is_file() {
|
||||
warn!(target: "lazymc", "Not blocking banned IPs, {} file does not exist", ban::FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load banned IPs once
|
||||
match ban::load(&path) {
|
||||
Ok(ips) => server.set_banned_ips_blocking(ips),
|
||||
Err(err) => {
|
||||
error!(target: "lazymc", "Failed to load banned IPs from {}: {}", ban::FILE, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Show warning if 127.0.0.1 is banned
|
||||
if server.is_banned_ip_blocking(&("127.0.0.1".parse().unwrap())) {
|
||||
warn!(target: "lazymc", "Local address 127.0.0.1 IP banned, probably not what you want");
|
||||
warn!(target: "lazymc", "Use '/pardon-ip 127.0.0.1' on the server to unban");
|
||||
}
|
||||
|
||||
// Keep watching
|
||||
while watch(&server, &path) {}
|
||||
}
|
||||
|
||||
/// Watch the given file.
|
||||
fn watch(server: &Server, path: &Path) -> bool {
|
||||
// The file must exist
|
||||
if !path.is_file() {
|
||||
warn!(target: "lazymc", "File {} does not exist, not watching changes", ban::FILE);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create watcher for banned IPs file
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher =
|
||||
watcher(tx, WATCH_DEBOUNCE).expect("failed to create watcher for banned-ips.json");
|
||||
if let Err(err) = watcher.watch(path, RecursiveMode::NonRecursive) {
|
||||
error!(target: "lazymc", "An error occured while creating watcher for {}: {}", ban::FILE, err);
|
||||
return true;
|
||||
}
|
||||
|
||||
loop {
|
||||
// Take next event
|
||||
let event = rx.recv().unwrap();
|
||||
|
||||
// Decide whether to reload and rewatch
|
||||
let (reload, rewatch) = match event {
|
||||
// Reload on write
|
||||
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::Write(_) => (true, false),
|
||||
|
||||
// Reload and rewatch on rename/remove
|
||||
DebouncedEvent::NoticeRemove(_)
|
||||
| DebouncedEvent::Remove(_)
|
||||
| DebouncedEvent::Rename(_, _)
|
||||
| DebouncedEvent::Rescan
|
||||
| DebouncedEvent::Create(_) => {
|
||||
trace!(target: "lazymc", "File banned-ips.json removed, trying to rewatch after 1 second");
|
||||
thread::sleep(WATCH_DEBOUNCE);
|
||||
(true, true)
|
||||
}
|
||||
|
||||
// Ignore chmod changes
|
||||
DebouncedEvent::Chmod(_) => (false, false),
|
||||
|
||||
// Rewatch on error
|
||||
DebouncedEvent::Error(_, _) => (false, true),
|
||||
};
|
||||
|
||||
// Reload banned IPs
|
||||
if reload {
|
||||
debug!(target: "lazymc", "Reloading list of banned IPs...");
|
||||
match ban::load(path) {
|
||||
Ok(ips) => server.set_banned_ips_blocking(ips),
|
||||
Err(err) => {
|
||||
error!(target: "lazymc", "Failed reload list of banned IPs from {}: {}", ban::FILE, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rewatch
|
||||
if rewatch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
163
src/service/file_watcher.rs
Normal file
163
src/service/file_watcher.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||
|
||||
use crate::config::{Config, Server as ConfigServer};
|
||||
use crate::mc::ban::{self, BannedIps};
|
||||
use crate::mc::whitelist;
|
||||
use crate::server::Server;
|
||||
|
||||
/// File watcher debounce time.
|
||||
const WATCH_DEBOUNCE: Duration = Duration::from_secs(2);
|
||||
|
||||
/// Service to watch server file changes.
|
||||
pub fn service(config: Arc<Config>, server: Arc<Server>) {
|
||||
// Ensure server directory is set, it must exist
|
||||
let dir = match ConfigServer::server_directory(&config) {
|
||||
Some(dir) if dir.is_dir() => dir,
|
||||
_ => {
|
||||
warn!(target: "lazymc", "Server directory doesn't exist, can't watch file changes to reload whitelist and banned IPs");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Keep watching
|
||||
#[allow(clippy::blocks_in_if_conditions)]
|
||||
while {
|
||||
// Update all files once
|
||||
reload_bans(&config, &server, &dir.join(ban::FILE));
|
||||
reload_whitelist(&config, &server, &dir);
|
||||
|
||||
// Watch for changes, update accordingly
|
||||
watch_server(&config, &server, &dir)
|
||||
} {}
|
||||
}
|
||||
|
||||
/// Watch server directory.
|
||||
///
|
||||
/// Returns `true` if we should watch again.
|
||||
#[must_use]
|
||||
fn watch_server(config: &Config, server: &Server, dir: &Path) -> bool {
|
||||
// Directory must exist
|
||||
if !dir.is_dir() {
|
||||
error!(target: "lazymc", "Server directory does not exist at {} anymore, not watching changes", dir.display());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create watcher for directory
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher =
|
||||
watcher(tx, WATCH_DEBOUNCE).expect("failed to create watcher for banned-ips.json");
|
||||
if let Err(err) = watcher.watch(dir, RecursiveMode::NonRecursive) {
|
||||
error!(target: "lazymc", "An error occured while creating watcher for server files: {}", err);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle change events
|
||||
loop {
|
||||
match rx.recv().unwrap() {
|
||||
// Handle file updates
|
||||
DebouncedEvent::Create(ref path)
|
||||
| DebouncedEvent::Write(ref path)
|
||||
| DebouncedEvent::Remove(ref path) => {
|
||||
update(config, server, dir, path);
|
||||
}
|
||||
|
||||
// Handle file updates on both paths for rename
|
||||
DebouncedEvent::Rename(ref before_path, ref after_path) => {
|
||||
update(config, server, dir, before_path);
|
||||
update(config, server, dir, after_path);
|
||||
}
|
||||
|
||||
// Ignore write/remove notices, will receive write/remove event later
|
||||
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::NoticeRemove(_) => {}
|
||||
|
||||
// Ignore chmod changes
|
||||
DebouncedEvent::Chmod(_) => {}
|
||||
|
||||
// Rewatch on rescan
|
||||
DebouncedEvent::Rescan => {
|
||||
debug!(target: "lazymc", "Rescanning server directory files due to file watching problem");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rewatch on error
|
||||
DebouncedEvent::Error(err, _) => {
|
||||
error!(target: "lazymc", "Error occurred while watching server directory for file changes: {}", err);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a file change on the given path.
|
||||
///
|
||||
/// Should be called both when created, changed or removed.
|
||||
fn update(config: &Config, server: &Server, dir: &Path, path: &Path) {
|
||||
// Update bans
|
||||
if path.ends_with(ban::FILE) {
|
||||
reload_bans(config, server, path);
|
||||
}
|
||||
|
||||
// Update whitelist
|
||||
if path.ends_with(whitelist::WHITELIST_FILE) || path.ends_with(whitelist::OPS_FILE) {
|
||||
reload_whitelist(config, server, dir);
|
||||
}
|
||||
|
||||
// TODO: update on server.properties change
|
||||
}
|
||||
|
||||
/// Reload banned IPs.
|
||||
fn reload_bans(config: &Config, server: &Server, path: &Path) {
|
||||
// Bans must be enabled
|
||||
if !config.server.block_banned_ips && !config.server.drop_banned_ips {
|
||||
return;
|
||||
}
|
||||
|
||||
trace!(target: "lazymc", "Reloading banned IPs...");
|
||||
|
||||
// File must exist, clear file otherwise
|
||||
if !path.is_file() {
|
||||
debug!(target: "lazymc", "No banned IPs, {} does not exist", ban::FILE);
|
||||
// warn!(target: "lazymc", "Not blocking banned IPs, {} file does not exist", ban::FILE);
|
||||
server.set_banned_ips_blocking(BannedIps::default());
|
||||
return;
|
||||
}
|
||||
|
||||
// Load and update banned IPs
|
||||
match ban::load(path) {
|
||||
Ok(ips) => server.set_banned_ips_blocking(ips),
|
||||
Err(err) => {
|
||||
debug!(target: "lazymc", "Failed load banned IPs from {}, ignoring: {}", ban::FILE, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Show warning if 127.0.0.1 is banned
|
||||
if server.is_banned_ip_blocking(&("127.0.0.1".parse().unwrap())) {
|
||||
warn!(target: "lazymc", "Local address 127.0.0.1 IP banned, probably not what you want");
|
||||
warn!(target: "lazymc", "Use '/pardon-ip 127.0.0.1' on the server to unban");
|
||||
}
|
||||
}
|
||||
|
||||
/// Reload whitelisted users.
|
||||
fn reload_whitelist(config: &Config, server: &Server, dir: &Path) {
|
||||
// Whitelist must be enabled
|
||||
if !config.server.wake_whitelist {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: whitelist must be enabled in server.properties
|
||||
|
||||
trace!(target: "lazymc", "Reloading whitelisted users...");
|
||||
|
||||
// Load and update whitelisted users
|
||||
match whitelist::load_dir(dir) {
|
||||
Ok(whitelist) => server.set_whitelist_blocking(Some(whitelist)),
|
||||
Err(err) => {
|
||||
debug!(target: "lazymc", "Failed load whitelist from {}, ignoring: {}", dir.display(), err);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
pub mod ban_reload;
|
||||
pub mod file_watcher;
|
||||
pub mod monitor;
|
||||
pub mod probe;
|
||||
pub mod server;
|
||||
|
@ -59,7 +59,7 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
|
||||
tokio::spawn(service::probe::service(config.clone(), server.clone()));
|
||||
tokio::task::spawn_blocking({
|
||||
let (config, server) = (config.clone(), server.clone());
|
||||
|| service::ban_reload::service(config, server)
|
||||
|| service::file_watcher::service(config, server)
|
||||
});
|
||||
|
||||
// Route all incomming connections
|
||||
|
@ -27,6 +27,9 @@ const BAN_MESSAGE_PREFIX: &str = "Your IP address is banned from this server.\nR
|
||||
/// Default ban reason if unknown.
|
||||
const DEFAULT_BAN_REASON: &str = "Banned by an operator.";
|
||||
|
||||
/// The not-whitelisted kick message.
|
||||
const WHITELIST_MESSAGE: &str = "You are not white-listed on this server!";
|
||||
|
||||
/// Server icon file path.
|
||||
const SERVER_ICON_FILE: &str = "server-icon.png";
|
||||
|
||||
@ -159,6 +162,15 @@ pub async fn serve(
|
||||
}
|
||||
}
|
||||
|
||||
// Kick if client is not whitelisted to wake server
|
||||
if let Some(ref username) = username {
|
||||
if !server.is_whitelisted(username).await {
|
||||
info!(target: "lazymc", "User '{}' tried to wake server but is not whitelisted, disconnecting", username);
|
||||
action::kick(&client, WHITELIST_MESSAGE, &mut writer).await?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Start server if not starting yet
|
||||
Server::start(config.clone(), server.clone(), username).await;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user