Compare commits
5 Commits
149a3acc1d
...
303aa30074
Author | SHA1 | Date |
---|---|---|
![]() |
303aa30074 | |
![]() |
ec81b13385 | |
![]() |
2e067be5af | |
![]() |
574f9fa567 | |
![]() |
852934cd84 |
|
@ -8,7 +8,8 @@ license = "MIT"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
purple_cello_mc_protocol = { git = "https://github.com/PurpleCelloServer/purple_cello_mc_protocol.git", rev = "9ff2e95c66b1d773362936733273350e4bdd399a" }
|
purple_cello_mc_protocol = { git = "https://github.com/PurpleCelloServer/purple_cello_mc_protocol.git", rev = "196592ae78e3878843c3dea99323c1477da6d380" }
|
||||||
|
purple_cello_mojang_api = { git = "https://github.com/PurpleCelloServer/mojang_api.git", rev = "08848a0a95b338bdaead303ac646cb3347cf1af6" }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
|
@ -12,10 +12,11 @@ use purple_cello_mc_protocol::{
|
||||||
use crate::status_handle;
|
use crate::status_handle;
|
||||||
use crate::login_handle;
|
use crate::login_handle;
|
||||||
use crate::listener;
|
use crate::listener;
|
||||||
|
use crate::info_messages;
|
||||||
|
|
||||||
pub async fn handle_client(
|
pub async fn handle_client(
|
||||||
client_socket: TcpStream,
|
client_socket: TcpStream,
|
||||||
proxy_info: listener::ProxyInfo,
|
mut proxy_info: listener::ProxyInfo,
|
||||||
) {
|
) {
|
||||||
println!("Accepting Connection");
|
println!("Accepting Connection");
|
||||||
let backend_addr = proxy_info.formatted_backend_address();
|
let backend_addr = proxy_info.formatted_backend_address();
|
||||||
|
@ -64,7 +65,7 @@ pub async fn handle_client(
|
||||||
match server_conn {
|
match server_conn {
|
||||||
Some(mut server_conn) => {
|
Some(mut server_conn) => {
|
||||||
if login_handle::respond_login(
|
if login_handle::respond_login(
|
||||||
&proxy_info,
|
&mut proxy_info,
|
||||||
&mut client_conn,
|
&mut client_conn,
|
||||||
&mut server_conn,
|
&mut server_conn,
|
||||||
).await.expect(
|
).await.expect(
|
||||||
|
@ -80,9 +81,7 @@ pub async fn handle_client(
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
login::clientbound::Disconnect {
|
login::clientbound::Disconnect {
|
||||||
reason: "\"Server Error (Server is down or \
|
reason: info_messages::BACKEND_DOWN_DISCONNECT
|
||||||
restarting)\nPlease contact the admins if the issue persists:\n\
|
|
||||||
purplecelloserver@gmail.com\""
|
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
.write(&mut client_conn)
|
.write(&mut client_conn)
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Yeahbut June 2024
|
||||||
|
|
||||||
|
pub const BACKEND_DOWN_PING: &str = "\
|
||||||
|
Server Error (Server may be starting)\n\
|
||||||
|
Purple Cello Server";
|
||||||
|
|
||||||
|
pub const BACKEND_DOWN_DISCONNECT: &str = "\
|
||||||
|
\"Server Error (Server is down or restarting)\n\
|
||||||
|
Please contact the admins if the issue persists:\n\
|
||||||
|
purplecelloserver@gmail.com\"";
|
||||||
|
|
||||||
|
pub const UUID_MISSING_DISCONNECT: &str = "Invalid UUID! (UUID Missing)";
|
||||||
|
|
||||||
|
pub const WHITELIST_STATUS_INACTIVE_DISCONNECT: &str = "\
|
||||||
|
Whitelist Status Inactive!\n\
|
||||||
|
Please contact the admins to reactivate:\n\
|
||||||
|
purplecelloserver@gmail.com";
|
||||||
|
|
||||||
|
pub const USERNAME_INVALID_DISCONNECT: &str = "\
|
||||||
|
Invalid Username!\n\
|
||||||
|
Please contact the admins to update your username:\n\
|
||||||
|
purplecelloserver@gmail.com";
|
||||||
|
|
||||||
|
pub const UUID_INVALID_DISCONNECT: &str = "Invalid UUID!";
|
||||||
|
|
||||||
|
pub const NOT_WHITELISTED_DISCONNECT: &str = "\
|
||||||
|
Not whitelisted on this server.\n\
|
||||||
|
Please direct whitelist requests to the admins:\n\
|
||||||
|
purplecelloserver@gmail.com";
|
|
@ -4,20 +4,30 @@ use tokio::net::TcpListener;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use rsa::RsaPrivateKey;
|
use rsa::RsaPrivateKey;
|
||||||
|
|
||||||
|
use crate::whitelist::Whitelist;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum OnlineStatus {
|
pub enum OnlineStatus {
|
||||||
Online,
|
Online,
|
||||||
Offline,
|
Offline,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum AuthenticationMethod {
|
||||||
|
Mojang,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ProxyInfo {
|
pub struct ProxyInfo {
|
||||||
pub proxy_addr: String,
|
pub proxy_addr: String,
|
||||||
pub proxy_port: u16,
|
pub proxy_port: u16,
|
||||||
pub online_status: OnlineStatus,
|
|
||||||
pub backend_addr: String,
|
pub backend_addr: String,
|
||||||
pub backend_port: u16,
|
pub backend_port: u16,
|
||||||
pub private_key: RsaPrivateKey,
|
pub private_key: RsaPrivateKey,
|
||||||
|
pub online_status: OnlineStatus,
|
||||||
|
pub authentication_method: AuthenticationMethod,
|
||||||
|
pub whitelist: Whitelist,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProxyInfo {
|
impl ProxyInfo {
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
// Yeahbut December 2023
|
// Yeahbut December 2023
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use serde_json::Value;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
use purple_cello_mc_protocol::{
|
use purple_cello_mc_protocol::{
|
||||||
mc_types::{self, Result, Packet, ProtocolConnection},
|
mc_types::{self, Result, Packet, ProtocolConnection},
|
||||||
|
@ -13,176 +7,48 @@ use purple_cello_mc_protocol::{
|
||||||
login,
|
login,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use purple_cello_mojang_api::multiplayer_auth;
|
||||||
|
|
||||||
use crate::listener;
|
use crate::listener;
|
||||||
|
use crate::whitelist::{Player, PlayerAllowed};
|
||||||
|
|
||||||
const EXPIRATION_DURATION: Duration = Duration::from_secs(3600);
|
async fn check_player(
|
||||||
|
proxy_info: &mut listener::ProxyInfo,
|
||||||
struct CachedWhitelist {
|
|
||||||
whitelist_data: Value,
|
|
||||||
timestamp: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
struct Player {
|
|
||||||
name: String,
|
|
||||||
player_uuid: Option<u128>,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PlayerAllowed {
|
|
||||||
True(Player),
|
|
||||||
False(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_whitelist() -> Value {
|
|
||||||
let file_path = "./whitelist.json";
|
|
||||||
|
|
||||||
let data = match fs::read_to_string(file_path) {
|
|
||||||
Ok(data) => data,
|
|
||||||
Err(_) => return Value::Null,
|
|
||||||
};
|
|
||||||
|
|
||||||
let whitelist_data: Value = match serde_json::from_str(&data) {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(_) => return Value::Null,
|
|
||||||
};
|
|
||||||
|
|
||||||
whitelist_data
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_whitelist() -> Vec<Player> {
|
|
||||||
lazy_static! {
|
|
||||||
static ref WHITELIST_CACHE: Arc<Mutex<Option<CachedWhitelist>>> =
|
|
||||||
Arc::new(Mutex::new(None));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cache = WHITELIST_CACHE.lock().unwrap();
|
|
||||||
|
|
||||||
if let Some(cached_whitelist) = cache.as_ref() {
|
|
||||||
if cached_whitelist.timestamp.elapsed() >= EXPIRATION_DURATION {
|
|
||||||
println!("Refreshing whitelist cache");
|
|
||||||
*cache = Some(CachedWhitelist {
|
|
||||||
whitelist_data: load_whitelist(),
|
|
||||||
timestamp: Instant::now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
*cache = Some(CachedWhitelist {
|
|
||||||
whitelist_data: load_whitelist(),
|
|
||||||
timestamp: Instant::now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let whitelist_data = cache.as_ref().unwrap().whitelist_data.clone();
|
|
||||||
|
|
||||||
std::mem::drop(cache);
|
|
||||||
|
|
||||||
if whitelist_data == Value::Null {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let whitelist_array = match whitelist_data.as_array() {
|
|
||||||
Some(whitelist) => whitelist,
|
|
||||||
None => { return Vec::new(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut whitelist: Vec<Player> = Vec::new();
|
|
||||||
|
|
||||||
for whitelisted_player in whitelist_array {
|
|
||||||
let player_map = match whitelisted_player.as_object() {
|
|
||||||
Some(whitelist) => whitelist,
|
|
||||||
None => { continue; }
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = match player_map.get("name") {
|
|
||||||
Some(name) => {
|
|
||||||
match name.as_str() {
|
|
||||||
Some(name) => name,
|
|
||||||
None => { continue; }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => { continue; }
|
|
||||||
};
|
|
||||||
|
|
||||||
let player_uuid = match player_map.get("uuid") {
|
|
||||||
Some(uuid) => {
|
|
||||||
match uuid.as_str() {
|
|
||||||
Some(uuid) => {
|
|
||||||
match u128::from_str_radix(uuid, 16) {
|
|
||||||
Ok(uuid) => uuid,
|
|
||||||
Err(_) => { continue; }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => { continue; }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => { continue; }
|
|
||||||
};
|
|
||||||
|
|
||||||
whitelist.push(Player {
|
|
||||||
name: name.to_string(),
|
|
||||||
player_uuid: Some(player_uuid),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
whitelist
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_player_whitelist(player: Player) -> PlayerAllowed {
|
|
||||||
|
|
||||||
if player.player_uuid.is_none() {
|
|
||||||
return PlayerAllowed::False("Invalid UUID".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let whitelist = get_whitelist();
|
|
||||||
|
|
||||||
let mut invalid_uuid = false;
|
|
||||||
let mut invalid_username = false;
|
|
||||||
|
|
||||||
for wl_player in whitelist {
|
|
||||||
if wl_player == player {
|
|
||||||
return PlayerAllowed::True(player);
|
|
||||||
} else if wl_player.name == player.name {
|
|
||||||
invalid_uuid = true;
|
|
||||||
} else if wl_player.player_uuid == player.player_uuid {
|
|
||||||
invalid_username = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if invalid_uuid {
|
|
||||||
PlayerAllowed::False("Invalid UUID".to_string())
|
|
||||||
} else if invalid_username {
|
|
||||||
PlayerAllowed::False(
|
|
||||||
"Invalid Username!\nPlease contact the admins to update your \
|
|
||||||
username:\npurplecelloserver@gmail.com".to_string()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
PlayerAllowed::False("Not whitelisted on this server.\n\
|
|
||||||
Please direct whitelist requests to the admins:\n\
|
|
||||||
purplecelloserver@gmail.com".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_player_online(
|
|
||||||
proxy_info: &listener::ProxyInfo,
|
|
||||||
player: Player,
|
player: Player,
|
||||||
client_conn: &mut ProtocolConnection<'_>,
|
client_conn: &mut ProtocolConnection<'_>,
|
||||||
) -> Result<PlayerAllowed> {
|
) -> Result<PlayerAllowed> {
|
||||||
|
match proxy_info.online_status {
|
||||||
|
listener::OnlineStatus::Online => {
|
||||||
let encryption_request = client_conn.create_encryption_request(
|
let encryption_request = client_conn.create_encryption_request(
|
||||||
proxy_info.private_key.clone())?;
|
proxy_info.private_key.clone())?;
|
||||||
encryption_request.write(client_conn).await?;
|
encryption_request.write(client_conn).await?;
|
||||||
let encryption_response =
|
let encryption_response =
|
||||||
login::serverbound::EncryptionResponse::read(client_conn).await?;
|
login::serverbound::EncryptionResponse::read(
|
||||||
|
client_conn).await?;
|
||||||
client_conn.handle_encryption_response(encryption_response)?;
|
client_conn.handle_encryption_response(encryption_response)?;
|
||||||
// TODO: Make authentication verification request
|
let server_id = client_conn.server_id_hash().await?;
|
||||||
Ok(check_player_whitelist(player))
|
match proxy_info.authentication_method {
|
||||||
|
listener::AuthenticationMethod::Mojang => {
|
||||||
|
match multiplayer_auth::joined(
|
||||||
|
&player.name, &server_id, None).await {
|
||||||
|
Ok(_) => Ok(proxy_info.whitelist
|
||||||
|
.check_player_whitelist(player)
|
||||||
|
),
|
||||||
|
Err(_) => Ok(PlayerAllowed::False(
|
||||||
|
"Mojang Authentication Failed".to_string()
|
||||||
|
)),
|
||||||
|
}},
|
||||||
|
listener::AuthenticationMethod::None =>
|
||||||
|
Ok(proxy_info.whitelist.check_player_whitelist(player))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
listener::OnlineStatus::Offline =>
|
||||||
|
Ok(proxy_info.whitelist.check_player_whitelist(player)),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_player_offline(player: Player) -> Result<PlayerAllowed> {
|
|
||||||
Ok(check_player_whitelist(player))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn respond_login(
|
pub async fn respond_login(
|
||||||
proxy_info: &listener::ProxyInfo,
|
proxy_info: &mut listener::ProxyInfo,
|
||||||
client_conn: &mut ProtocolConnection<'_>,
|
client_conn: &mut ProtocolConnection<'_>,
|
||||||
server_conn: &mut ProtocolConnection<'_>,
|
server_conn: &mut ProtocolConnection<'_>,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
|
@ -209,7 +75,7 @@ pub async fn respond_login(
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn login_to_proxy(
|
async fn login_to_proxy(
|
||||||
proxy_info: &listener::ProxyInfo,
|
proxy_info: &mut listener::ProxyInfo,
|
||||||
client_conn: &mut ProtocolConnection<'_>,
|
client_conn: &mut ProtocolConnection<'_>,
|
||||||
) -> Result<PlayerAllowed> {
|
) -> Result<PlayerAllowed> {
|
||||||
println!("Logging into proxy");
|
println!("Logging into proxy");
|
||||||
|
@ -220,14 +86,10 @@ async fn login_to_proxy(
|
||||||
let player: Player = Player {
|
let player: Player = Player {
|
||||||
name: start_packet.name,
|
name: start_packet.name,
|
||||||
player_uuid: start_packet.player_uuid,
|
player_uuid: start_packet.player_uuid,
|
||||||
|
active: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
match proxy_info.online_status {
|
check_player(proxy_info, player, client_conn).await
|
||||||
listener::OnlineStatus::Online =>
|
|
||||||
check_player_online(proxy_info, player, client_conn).await,
|
|
||||||
listener::OnlineStatus::Offline =>
|
|
||||||
check_player_offline(player),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn login_to_backend(
|
async fn login_to_backend(
|
||||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -8,6 +8,9 @@ mod status_handle;
|
||||||
mod login_handle;
|
mod login_handle;
|
||||||
mod client;
|
mod client;
|
||||||
mod listener;
|
mod listener;
|
||||||
|
mod whitelist;
|
||||||
|
mod info_messages;
|
||||||
|
mod motd;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
@ -15,18 +18,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let offline_info = listener::ProxyInfo{
|
let offline_info = listener::ProxyInfo{
|
||||||
proxy_addr: "127.0.0.1".to_string(),
|
proxy_addr: "127.0.0.1".to_string(),
|
||||||
proxy_port: 25565,
|
proxy_port: 25565,
|
||||||
online_status: listener::OnlineStatus::Offline,
|
|
||||||
backend_addr: "127.0.0.1".to_string(),
|
backend_addr: "127.0.0.1".to_string(),
|
||||||
backend_port: 25564,
|
backend_port: 25564,
|
||||||
private_key: private_key.clone(),
|
private_key: private_key.clone(),
|
||||||
|
online_status: listener::OnlineStatus::Offline,
|
||||||
|
authentication_method: listener::AuthenticationMethod::None,
|
||||||
|
whitelist: whitelist::Whitelist::WhitelistOpen(
|
||||||
|
whitelist::WhitelistOpen{}),
|
||||||
};
|
};
|
||||||
let online_info = listener::ProxyInfo{
|
let online_info = listener::ProxyInfo{
|
||||||
proxy_addr: "127.0.0.1".to_string(),
|
proxy_addr: "127.0.0.1".to_string(),
|
||||||
proxy_port: 25566,
|
proxy_port: 25566,
|
||||||
online_status: listener::OnlineStatus::Online,
|
|
||||||
backend_addr: "127.0.0.1".to_string(),
|
backend_addr: "127.0.0.1".to_string(),
|
||||||
backend_port: 25564,
|
backend_port: 25564,
|
||||||
private_key: private_key.clone(),
|
private_key: private_key.clone(),
|
||||||
|
online_status: listener::OnlineStatus::Online,
|
||||||
|
authentication_method: listener::AuthenticationMethod::Mojang,
|
||||||
|
whitelist: whitelist::Whitelist::WhitelistFile(
|
||||||
|
whitelist::WhitelistFile::new("./whitelist.json".to_string())),
|
||||||
};
|
};
|
||||||
|
|
||||||
let listener_offline: listener::TcpListenerWrapper =
|
let listener_offline: listener::TcpListenerWrapper =
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
// Yeahbut June 2024
|
||||||
|
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
|
use rand::Rng;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
// Refresh every 60 minutes
|
||||||
|
const EXPIRATION_DURATION: Duration = Duration::from_secs(3600);
|
||||||
|
|
||||||
|
struct CachedMotds {
|
||||||
|
motd_data: Value,
|
||||||
|
timestamp: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_motds() -> Value {
|
||||||
|
let file_path = "./motd.json";
|
||||||
|
|
||||||
|
let data = match fs::read_to_string(file_path) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => return Value::Null,
|
||||||
|
};
|
||||||
|
|
||||||
|
let motd_data: Value = match serde_json::from_str(&data) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return Value::Null,
|
||||||
|
};
|
||||||
|
|
||||||
|
motd_data
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_motds() -> Value {
|
||||||
|
lazy_static! {
|
||||||
|
static ref MOTDS_CACHE: Arc<Mutex<Option<CachedMotds>>> =
|
||||||
|
Arc::new(Mutex::new(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cache = MOTDS_CACHE.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(cached_motds) = cache.as_ref() {
|
||||||
|
if cached_motds.timestamp.elapsed() >= EXPIRATION_DURATION {
|
||||||
|
println!("Refreshing MOTD cache");
|
||||||
|
*cache = Some(CachedMotds {
|
||||||
|
motd_data: load_motds(),
|
||||||
|
timestamp: Instant::now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*cache = Some(CachedMotds {
|
||||||
|
motd_data: load_motds(),
|
||||||
|
timestamp: Instant::now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let motds = cache.as_ref().unwrap().motd_data.clone();
|
||||||
|
|
||||||
|
std::mem::drop(cache);
|
||||||
|
|
||||||
|
motds
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn motd() -> String {
|
||||||
|
let default = "A Minecraft Server Proxy".to_string();
|
||||||
|
|
||||||
|
let motd_data = get_motds();
|
||||||
|
|
||||||
|
if motd_data == Value::Null {
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
let length1 = motd_data["line1"].as_array().map_or(0, |v| v.len());
|
||||||
|
let length2 = motd_data["line2"].as_array().map_or(0, |v| v.len());
|
||||||
|
|
||||||
|
if length1 == 0 || length2 == 0 {
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let rand1 = rng.gen_range(0..length1) as usize;
|
||||||
|
let rand2 = rng.gen_range(0..length2) as usize;
|
||||||
|
|
||||||
|
let line1: &str = match motd_data["line1"][rand1].as_str() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return default,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Birthdays, Holidays, and Announcements
|
||||||
|
|
||||||
|
let line2: &str = match motd_data["line2"][rand2].as_str() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return default,
|
||||||
|
};
|
||||||
|
|
||||||
|
let line: String = format!("{}\n{}", line1, line2);
|
||||||
|
line
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn favicon() -> Option<String> {
|
||||||
|
let file_path = "./icon.png";
|
||||||
|
|
||||||
|
let mut file = match File::open(file_path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
if let Err(_) = file.read_to_end(&mut buffer) {
|
||||||
|
return None
|
||||||
|
};
|
||||||
|
|
||||||
|
let base64_string = general_purpose::STANDARD_NO_PAD.encode(buffer);
|
||||||
|
let full_string: String =
|
||||||
|
format!("data:image/png;base64,{}", base64_string);
|
||||||
|
|
||||||
|
Some(full_string)
|
||||||
|
}
|
|
@ -1,15 +1,6 @@
|
||||||
// Yeahbut December 2023
|
// Yeahbut December 2023
|
||||||
|
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::io::Read;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use serde_json::Value;
|
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
|
||||||
use rand::Rng;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
use purple_cello_mc_protocol::{
|
use purple_cello_mc_protocol::{
|
||||||
mc_types::{self, Result, Packet, ProtocolConnection},
|
mc_types::{self, Result, Packet, ProtocolConnection},
|
||||||
|
@ -18,13 +9,8 @@ use purple_cello_mc_protocol::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::listener;
|
use crate::listener;
|
||||||
|
use crate::info_messages;
|
||||||
const EXPIRATION_DURATION: Duration = Duration::from_secs(3600);
|
use crate::motd::{motd, favicon};
|
||||||
|
|
||||||
struct CachedMotds {
|
|
||||||
motd_data: Value,
|
|
||||||
timestamp: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn online_players(
|
async fn online_players(
|
||||||
proxy_info: listener::ProxyInfo,
|
proxy_info: listener::ProxyInfo,
|
||||||
|
@ -33,108 +19,6 @@ async fn online_players(
|
||||||
Ok(get_upstream_status(proxy_info, server_conn).await?.players)
|
Ok(get_upstream_status(proxy_info, server_conn).await?.players)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_motds() -> Value {
|
|
||||||
let file_path = "./motd.json";
|
|
||||||
|
|
||||||
let data = match fs::read_to_string(file_path) {
|
|
||||||
Ok(data) => data,
|
|
||||||
Err(_) => return Value::Null,
|
|
||||||
};
|
|
||||||
|
|
||||||
let motd_data: Value = match serde_json::from_str(&data) {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(_) => return Value::Null,
|
|
||||||
};
|
|
||||||
|
|
||||||
motd_data
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_motds() -> Value {
|
|
||||||
lazy_static! {
|
|
||||||
static ref MOTDS_CACHE: Arc<Mutex<Option<CachedMotds>>> =
|
|
||||||
Arc::new(Mutex::new(None));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cache = MOTDS_CACHE.lock().unwrap();
|
|
||||||
|
|
||||||
if let Some(cached_motds) = cache.as_ref() {
|
|
||||||
if cached_motds.timestamp.elapsed() >= EXPIRATION_DURATION {
|
|
||||||
println!("Refreshing MOTD cache");
|
|
||||||
*cache = Some(CachedMotds {
|
|
||||||
motd_data: load_motds(),
|
|
||||||
timestamp: Instant::now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
*cache = Some(CachedMotds {
|
|
||||||
motd_data: load_motds(),
|
|
||||||
timestamp: Instant::now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let motds = cache.as_ref().unwrap().motd_data.clone();
|
|
||||||
|
|
||||||
std::mem::drop(cache);
|
|
||||||
|
|
||||||
motds
|
|
||||||
}
|
|
||||||
|
|
||||||
fn motd() -> String {
|
|
||||||
let default = "A Minecraft Server Proxy".to_string();
|
|
||||||
|
|
||||||
let motd_data = get_motds();
|
|
||||||
|
|
||||||
if motd_data == Value::Null {
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
let length1 = motd_data["line1"].as_array().map_or(0, |v| v.len());
|
|
||||||
let length2 = motd_data["line2"].as_array().map_or(0, |v| v.len());
|
|
||||||
|
|
||||||
if length1 == 0 || length2 == 0 {
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
let rand1 = rng.gen_range(0..length1) as usize;
|
|
||||||
let rand2 = rng.gen_range(0..length2) as usize;
|
|
||||||
|
|
||||||
let line1: &str = match motd_data["line1"][rand1].as_str() {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return default,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Birthdays, Holidays, and Announcements
|
|
||||||
|
|
||||||
let line2: &str = match motd_data["line2"][rand2].as_str() {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return default,
|
|
||||||
};
|
|
||||||
|
|
||||||
let line: String = format!("{}\n{}", line1, line2);
|
|
||||||
line
|
|
||||||
}
|
|
||||||
|
|
||||||
fn favicon() -> Option<String> {
|
|
||||||
let file_path = "./icon.png";
|
|
||||||
|
|
||||||
let mut file = match File::open(file_path) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(_) => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
if let Err(_) = file.read_to_end(&mut buffer) {
|
|
||||||
return None
|
|
||||||
};
|
|
||||||
|
|
||||||
let base64_string = general_purpose::STANDARD_NO_PAD.encode(buffer);
|
|
||||||
let full_string: String =
|
|
||||||
format!("data:image/png;base64,{}", base64_string);
|
|
||||||
|
|
||||||
Some(full_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn respond_status(
|
pub async fn respond_status(
|
||||||
proxy_info: listener::ProxyInfo,
|
proxy_info: listener::ProxyInfo,
|
||||||
client_conn: &mut ProtocolConnection<'_>,
|
client_conn: &mut ProtocolConnection<'_>,
|
||||||
|
@ -187,8 +71,8 @@ pub async fn respond_status(
|
||||||
protocol: 0,
|
protocol: 0,
|
||||||
},
|
},
|
||||||
description: mc_types::Chat {
|
description: mc_types::Chat {
|
||||||
text: "Server Error (Server may be starting)"
|
text: info_messages::BACKEND_DOWN_PING
|
||||||
.to_string() + "\nPurple Cello Server",
|
.to_string(),
|
||||||
},
|
},
|
||||||
players: status::clientbound::StatusPlayers {
|
players: status::clientbound::StatusPlayers {
|
||||||
max: 0,
|
max: 0,
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
// Yeahbut June 2024
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::info_messages;
|
||||||
|
|
||||||
|
// Refresh every 5 minutes
|
||||||
|
const EXPIRATION_DURATION: Duration = Duration::from_secs(300);
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct Player {
|
||||||
|
pub name: String,
|
||||||
|
pub player_uuid: Option<u128>,
|
||||||
|
pub active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PlayerAllowed {
|
||||||
|
True(Player),
|
||||||
|
False(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Whitelist {
|
||||||
|
WhitelistOpen(WhitelistOpen),
|
||||||
|
WhitelistFile(WhitelistFile),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Whitelist {
|
||||||
|
pub fn check_player_whitelist(&mut self ,player: Player) -> PlayerAllowed {
|
||||||
|
match self {
|
||||||
|
Whitelist::WhitelistOpen(wl) => wl.check_player_whitelist(player),
|
||||||
|
Whitelist::WhitelistFile(wl) => wl.check_player_whitelist(player),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WhitelistOpen {}
|
||||||
|
|
||||||
|
impl WhitelistOpen {
|
||||||
|
pub fn check_player_whitelist(&mut self ,player: Player) -> PlayerAllowed {
|
||||||
|
PlayerAllowed::True(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WhitelistFile {
|
||||||
|
file_path: String,
|
||||||
|
whitelist_data: Value,
|
||||||
|
timestamp: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WhitelistFile {
|
||||||
|
pub fn new(file_path: String) -> Self {
|
||||||
|
Self {
|
||||||
|
file_path,
|
||||||
|
whitelist_data: Value::Null,
|
||||||
|
timestamp: Instant::now() - EXPIRATION_DURATION,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&mut self) {
|
||||||
|
let data = match fs::read_to_string(&self.file_path) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.whitelist_data = match serde_json::from_str(&data) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => Value::Null,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.timestamp = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_whitelist(&mut self) -> Vec<Player> {
|
||||||
|
if self.timestamp.elapsed() >= EXPIRATION_DURATION {
|
||||||
|
println!("Refreshing whitelist cache");
|
||||||
|
self.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.whitelist_data == Value::Null {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let whitelist_array = match self.whitelist_data.as_array() {
|
||||||
|
Some(whitelist) => whitelist,
|
||||||
|
None => { return Vec::new(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut whitelist: Vec<Player> = Vec::new();
|
||||||
|
|
||||||
|
for whitelisted_player in whitelist_array {
|
||||||
|
let player_map = match whitelisted_player.as_object() {
|
||||||
|
Some(whitelist) => whitelist,
|
||||||
|
None => { continue; }
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = match player_map.get("name") {
|
||||||
|
Some(name) => {
|
||||||
|
match name.as_str() {
|
||||||
|
Some(name) => name,
|
||||||
|
None => { continue; }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => { continue; }
|
||||||
|
};
|
||||||
|
|
||||||
|
let player_uuid = match player_map.get("uuid") {
|
||||||
|
Some(uuid) => {
|
||||||
|
match uuid.as_str() {
|
||||||
|
Some(uuid) => {
|
||||||
|
match u128::from_str_radix(uuid, 16) {
|
||||||
|
Ok(uuid) => uuid,
|
||||||
|
Err(_) => { continue; }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => { continue; }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => { continue; }
|
||||||
|
};
|
||||||
|
|
||||||
|
let active = match player_map.get("active") {
|
||||||
|
Some(active) => {
|
||||||
|
match active.as_bool() {
|
||||||
|
Some(active) => active,
|
||||||
|
None => { false }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => { false }
|
||||||
|
};
|
||||||
|
|
||||||
|
whitelist.push(Player {
|
||||||
|
name: name.to_string(),
|
||||||
|
player_uuid: Some(player_uuid),
|
||||||
|
active: active,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelist
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_player_whitelist(&mut self ,player: Player) -> PlayerAllowed {
|
||||||
|
|
||||||
|
if player.player_uuid.is_none() {
|
||||||
|
return PlayerAllowed::False(
|
||||||
|
info_messages::UUID_MISSING_DISCONNECT.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let whitelist = self.get_whitelist();
|
||||||
|
|
||||||
|
let mut invalid_uuid = false;
|
||||||
|
let mut invalid_username = false;
|
||||||
|
let mut is_inactive = false;
|
||||||
|
|
||||||
|
for wl_player in whitelist {
|
||||||
|
if wl_player.name == player.name &&
|
||||||
|
wl_player.player_uuid == player.player_uuid {
|
||||||
|
if wl_player.active {
|
||||||
|
return PlayerAllowed::True(player);
|
||||||
|
} else {
|
||||||
|
is_inactive = true;
|
||||||
|
}
|
||||||
|
} else if wl_player.name == player.name &&
|
||||||
|
wl_player.player_uuid != player.player_uuid {
|
||||||
|
invalid_uuid = true;
|
||||||
|
} else if wl_player.player_uuid == player.player_uuid &&
|
||||||
|
wl_player.name != player.name {
|
||||||
|
invalid_username = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_inactive {
|
||||||
|
PlayerAllowed::False(
|
||||||
|
info_messages::WHITELIST_STATUS_INACTIVE_DISCONNECT.to_string())
|
||||||
|
} else if invalid_username {
|
||||||
|
PlayerAllowed::False(
|
||||||
|
info_messages::USERNAME_INVALID_DISCONNECT.to_string())
|
||||||
|
} else if invalid_uuid {
|
||||||
|
PlayerAllowed::False(
|
||||||
|
info_messages::UUID_INVALID_DISCONNECT.to_string())
|
||||||
|
} else {
|
||||||
|
PlayerAllowed::False(
|
||||||
|
info_messages::NOT_WHITELISTED_DISCONNECT.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue