Compare commits

..

No commits in common. "303aa300748db32258477857fe2481d1c2e4dbb5" and "149a3acc1d8293c9ef3da30220ffdc097a006ee3" have entirely different histories.

9 changed files with 306 additions and 412 deletions

View File

@ -8,8 +8,7 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
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" }
purple_cello_mc_protocol = { git = "https://github.com/PurpleCelloServer/purple_cello_mc_protocol.git", rev = "9ff2e95c66b1d773362936733273350e4bdd399a" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@ -12,11 +12,10 @@ use purple_cello_mc_protocol::{
use crate::status_handle;
use crate::login_handle;
use crate::listener;
use crate::info_messages;
pub async fn handle_client(
client_socket: TcpStream,
mut proxy_info: listener::ProxyInfo,
proxy_info: listener::ProxyInfo,
) {
println!("Accepting Connection");
let backend_addr = proxy_info.formatted_backend_address();
@ -65,7 +64,7 @@ pub async fn handle_client(
match server_conn {
Some(mut server_conn) => {
if login_handle::respond_login(
&mut proxy_info,
&proxy_info,
&mut client_conn,
&mut server_conn,
).await.expect(
@ -81,7 +80,9 @@ pub async fn handle_client(
}
None => {
login::clientbound::Disconnect {
reason: info_messages::BACKEND_DOWN_DISCONNECT
reason: "\"Server Error (Server is down or \
restarting)\nPlease contact the admins if the issue persists:\n\
purplecelloserver@gmail.com\""
.to_string()
}
.write(&mut client_conn)

View File

@ -1,29 +0,0 @@
// 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";

View File

@ -4,30 +4,20 @@ use tokio::net::TcpListener;
use std::error::Error;
use rsa::RsaPrivateKey;
use crate::whitelist::Whitelist;
#[derive(Copy, Clone)]
pub enum OnlineStatus {
Online,
Offline,
}
#[derive(Copy, Clone)]
pub enum AuthenticationMethod {
Mojang,
None,
}
#[derive(Clone)]
pub struct ProxyInfo {
pub proxy_addr: String,
pub proxy_port: u16,
pub online_status: OnlineStatus,
pub backend_addr: String,
pub backend_port: u16,
pub private_key: RsaPrivateKey,
pub online_status: OnlineStatus,
pub authentication_method: AuthenticationMethod,
pub whitelist: Whitelist,
}
impl ProxyInfo {

View File

@ -1,5 +1,11 @@
// 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::{
mc_types::{self, Result, Packet, ProtocolConnection},
@ -7,48 +13,176 @@ use purple_cello_mc_protocol::{
login,
};
use purple_cello_mojang_api::multiplayer_auth;
use crate::listener;
use crate::whitelist::{Player, PlayerAllowed};
async fn check_player(
proxy_info: &mut listener::ProxyInfo,
const EXPIRATION_DURATION: Duration = Duration::from_secs(3600);
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,
client_conn: &mut ProtocolConnection<'_>,
) -> Result<PlayerAllowed> {
match proxy_info.online_status {
listener::OnlineStatus::Online => {
let encryption_request = client_conn.create_encryption_request(
proxy_info.private_key.clone())?;
encryption_request.write(client_conn).await?;
let encryption_response =
login::serverbound::EncryptionResponse::read(
client_conn).await?;
login::serverbound::EncryptionResponse::read(client_conn).await?;
client_conn.handle_encryption_response(encryption_response)?;
let server_id = client_conn.server_id_hash().await?;
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)),
// TODO: Make authentication verification request
Ok(check_player_whitelist(player))
}
fn check_player_offline(player: Player) -> Result<PlayerAllowed> {
Ok(check_player_whitelist(player))
}
pub async fn respond_login(
proxy_info: &mut listener::ProxyInfo,
proxy_info: &listener::ProxyInfo,
client_conn: &mut ProtocolConnection<'_>,
server_conn: &mut ProtocolConnection<'_>,
) -> Result<bool> {
@ -75,7 +209,7 @@ pub async fn respond_login(
}
async fn login_to_proxy(
proxy_info: &mut listener::ProxyInfo,
proxy_info: &listener::ProxyInfo,
client_conn: &mut ProtocolConnection<'_>,
) -> Result<PlayerAllowed> {
println!("Logging into proxy");
@ -86,10 +220,14 @@ async fn login_to_proxy(
let player: Player = Player {
name: start_packet.name,
player_uuid: start_packet.player_uuid,
active: true,
};
check_player(proxy_info, player, client_conn).await
match proxy_info.online_status {
listener::OnlineStatus::Online =>
check_player_online(proxy_info, player, client_conn).await,
listener::OnlineStatus::Offline =>
check_player_offline(player),
}
}
async fn login_to_backend(

View File

@ -8,9 +8,6 @@ mod status_handle;
mod login_handle;
mod client;
mod listener;
mod whitelist;
mod info_messages;
mod motd;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
@ -18,24 +15,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
let offline_info = listener::ProxyInfo{
proxy_addr: "127.0.0.1".to_string(),
proxy_port: 25565,
online_status: listener::OnlineStatus::Offline,
backend_addr: "127.0.0.1".to_string(),
backend_port: 25564,
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{
proxy_addr: "127.0.0.1".to_string(),
proxy_port: 25566,
online_status: listener::OnlineStatus::Online,
backend_addr: "127.0.0.1".to_string(),
backend_port: 25564,
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 =

View File

@ -1,121 +0,0 @@
// 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)
}

View File

@ -1,6 +1,15 @@
// 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 serde_json::Value;
use base64::{Engine as _, engine::general_purpose};
use rand::Rng;
use lazy_static::lazy_static;
use purple_cello_mc_protocol::{
mc_types::{self, Result, Packet, ProtocolConnection},
@ -9,8 +18,13 @@ use purple_cello_mc_protocol::{
};
use crate::listener;
use crate::info_messages;
use crate::motd::{motd, favicon};
const EXPIRATION_DURATION: Duration = Duration::from_secs(3600);
struct CachedMotds {
motd_data: Value,
timestamp: Instant,
}
async fn online_players(
proxy_info: listener::ProxyInfo,
@ -19,6 +33,108 @@ async fn online_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(
proxy_info: listener::ProxyInfo,
client_conn: &mut ProtocolConnection<'_>,
@ -71,8 +187,8 @@ pub async fn respond_status(
protocol: 0,
},
description: mc_types::Chat {
text: info_messages::BACKEND_DOWN_PING
.to_string(),
text: "Server Error (Server may be starting)"
.to_string() + "\nPurple Cello Server",
},
players: status::clientbound::StatusPlayers {
max: 0,

View File

@ -1,191 +0,0 @@
// 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())
}
}
}