Merge pull request #3 from saturnclientmc/rewritten

Rewritten
This commit is contained in:
2026-02-23 10:06:49 -08:00
committed by GitHub
22 changed files with 2642 additions and 1948 deletions
+7 -1
View File
@@ -16,4 +16,10 @@ target/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
token.key
token.key
# Added by cargo
/target
*.db
Generated
+1874 -993
View File
File diff suppressed because it is too large Load Diff
+12 -7
View File
@@ -1,11 +1,16 @@
[package]
name = "auraclient-api"
name = "server"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
base64 = "0.22.1"
minreq = { version = "2.13.2", features = ["https", "urlencoding", "json-using-serde"] }
mongodb = { version = "3.2.1", features = ["sync"] }
openssl = "0.10.72"
serde = { version ="1.0.218", features = ["derive"] }
reqwest = { version = "0.13.2", features = ["json"] }
serde = { version = "1.0.228", features = ["serde_derive"] }
serde_json = "1.0.149"
session-rs = "0.1.2"
sqlx = { version = "0.8.6", features = [
"sqlite",
"runtime-tokio-native-tls",
"macros",
] }
tokio = "1.49.0"
+57
View File
@@ -0,0 +1,57 @@
# -------------------------
# 1️⃣ Build Stage
# -------------------------
FROM rust:1.76 as builder
WORKDIR /app
# Install dependencies required for sqlx + native-tls
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
ca-certificates \
sqlite3 \
libsqlite3-dev \
&& rm -rf /var/lib/apt/lists/*
# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
# Copy real source
COPY . .
# Build release binary
RUN cargo build --release
# -------------------------
# 2️⃣ Runtime Stage
# -------------------------
FROM debian:bookworm-slim
WORKDIR /app
# Install runtime deps only
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
sqlite3 \
libsqlite3-0 \
&& rm -rf /var/lib/apt/lists/*
# Copy compiled binary
COPY --from=builder /app/target/release/server /app/server
# Create data directory
RUN mkdir -p /data
# Expose port (change if needed)
EXPOSE 8080
# Declare volume for SQLite database
VOLUME ["/data"]
# Run the server
CMD ["./server"]
+1 -30
View File
@@ -1,32 +1,3 @@
# SaturnClient API
# SaturnClient API (v2)
The api for AuraClient which is a Minecraft mod Client
# Handshake
In order to communicate with the server, you will have to make a handshake, here is how
```
<MOJANG-SESSION-TOKEN>
```
The session token is stored on your minecraft client when it communicates with Mojang servers, as you might understand, this token is a private token used for authentication with Mojang servers, Why do we need it? Because our server needs to understand that whoever logs in with SaturnClient is you and when you make changes in your client, the changes get synced with others, Example: Changing Cloak.
# Protocol base
Every request should be only one line
```
method@key1=value1@key2=value2
```
# Error handling
request
```
invalid-method
```
response
```
!Invalid method
```
On a response `!` means it's an error.
# Methods
### `set_cloak`
- `cloak` -> `string`
+107 -1
View File
@@ -1,3 +1,109 @@
pub const CLOAKS: &[&str] = &["mercedes_flow", "glitch", "crimson_mark", "bmw", "amg", "amg_petronas", "ferrari", "redbull", "black_hole_amethyst", "black_hole_flame", "black_hole_white"];
use sqlx::SqlitePool;
use std::sync::Arc;
use crate::{types::UUID, user};
pub const CLOAKS: &[&str] = &[
"glitch",
"mercedes_flow",
"crimson_mark",
"bmw",
"amg",
"amg_petronas",
"ferrari",
"redbull",
"black_hole_amethyst",
"black_hole_flame",
"black_hole_white",
];
pub const HATS: &[&str] = &["horns_black", "horns_white", "halo", "halo_black"];
pub enum CosmeticKind {
Hat,
Cloak,
}
// Buy a cosmetic
pub async fn buy(
kind: CosmeticKind,
uuid: UUID,
item_id: String,
pool: Arc<SqlitePool>,
) -> Result<String, String> {
// Lock once
let uuid = uuid.lock().await.clone();
let mut user = user::get(&uuid, &pool).await?;
match kind {
CosmeticKind::Hat => {
if !HATS.contains(&item_id.as_str()) {
return Err("Hat does not exist".into());
}
if !user.hats.contains(&item_id) {
user.hats.push(item_id.clone());
}
}
CosmeticKind::Cloak => {
if !CLOAKS.contains(&item_id.as_str()) {
return Err("Cloak does not exist".into());
}
if !user.cloaks.contains(&item_id) {
user.cloaks.push(item_id.clone());
}
}
}
sqlx::query("UPDATE users SET cloaks = ?, hats = ? WHERE uuid = ?")
.bind(serde_json::to_string(&user.cloaks).map_err(|e| e.to_string())?)
.bind(serde_json::to_string(&user.hats).map_err(|e| e.to_string())?)
.bind(&uuid)
.execute(pool.as_ref())
.await
.map_err(|e| e.to_string())?;
Ok(format!("Item {} bought successfully!", item_id))
}
// Equip a cosmetic
pub async fn equip(
kind: CosmeticKind,
uuid: UUID,
item_id: String,
pool: Arc<SqlitePool>,
) -> Result<String, String> {
// Lock once
let uuid = uuid.lock().await.clone();
let mut user = user::get(&uuid, &pool).await?;
match kind {
CosmeticKind::Hat => {
if !user.hats.contains(&item_id) {
return Err("You don't own this hat".into());
}
user.hat = item_id.clone();
}
CosmeticKind::Cloak => {
if !user.cloaks.contains(&item_id) {
return Err("You don't own this cloak".into());
}
user.cloak = item_id.clone();
}
}
sqlx::query("UPDATE users SET cloak = ?, hat = ? WHERE uuid = ?")
.bind(&user.cloak)
.bind(&user.hat)
.bind(&uuid)
.execute(pool.as_ref())
.await
.map_err(|e| e.to_string())?;
Ok(format!("Item {} equipped successfully!", item_id))
}
-25
View File
@@ -1,25 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Player {
pub uuid: String,
pub cloaks: Vec<String>,
pub cloak: String,
pub hats: Vec<String>,
pub hat: String,
pub coins: u32,
pub online: bool,
}
pub struct Database {
pub players: mongodb::sync::Collection<Player>,
}
impl Database {
pub fn new(client: &mongodb::sync::Client) -> Self {
let db = client.database("saturnclient");
Self {
players: db.collection("players"),
}
}
}
-163
View File
@@ -1,163 +0,0 @@
use std::{
fmt::Display,
io::{BufRead, BufReader, Write},
net::TcpStream,
};
use openssl::{
pkey::Private,
rsa::Rsa,
symm::{decrypt, encrypt, Cipher},
};
use crate::response::Error;
fn aes_encrypt(key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error> {
let cipher = Cipher::aes_256_ecb();
encrypt(cipher, key, None, plaintext)
.map_err(|e| Error::EncryptionError(format!("Failed to encrypt: {}", e)))
}
fn aes_decrypt(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error> {
let cipher = Cipher::aes_256_ecb();
decrypt(cipher, key, None, ciphertext)
.map_err(|e| Error::EncryptionError(format!("Failed to decrypt: {}", e)))
}
#[allow(deprecated)]
pub fn handshake(mut stream: TcpStream, rsa: Rsa<Private>) -> Result<ETcp, Error> {
let stream_clone = stream
.try_clone()
.map_err(|e| Error::NetworkError(format!("Failed to clone stream: {}", e)))?;
let mut reader = BufReader::new(stream_clone);
let public_key = rsa
.public_key_to_pem()
.map_err(|e| Error::EncryptionError(format!("Failed to get public key: {}", e)))?;
stream
.write_all(&public_key)
.map_err(|e| Error::NetworkError(format!("Failed to write public key: {}", e)))?;
stream
.flush()
.map_err(|e| Error::NetworkError(format!("Failed to flush stream: {}", e)))?;
let mut aes_encoded = String::new();
reader
.read_line(&mut aes_encoded)
.map_err(|e| Error::NetworkError(format!("Failed to read AES key: {}", e)))?;
let encrypted_data = base64::decode(aes_encoded.trim())
.map_err(|e| Error::EncryptionError(format!("Failed to decode base64: {}", e)))?;
let mut aes_decrypted = vec![0; 256];
let aes_len = rsa
.private_decrypt(
&encrypted_data,
&mut aes_decrypted,
openssl::rsa::Padding::PKCS1,
)
.map_err(|e| Error::EncryptionError(format!("Failed to decrypt AES key: {}", e)))?;
println!("len: {}", aes_len);
aes_decrypted.truncate(aes_len);
println!(
"Decrypted AES key ({} bytes): {:?}",
aes_decrypted.len(),
aes_decrypted
);
Ok(ETcp {
stream,
aes: aes_decrypted,
reader,
})
}
pub struct ETcp {
stream: TcpStream,
reader: BufReader<TcpStream>,
aes: Vec<u8>,
}
impl ETcp {
#[allow(deprecated)]
pub fn send<T: Display>(&mut self, d: T) -> Result<(), Error> {
let encrypted = aes_encrypt(&self.aes, d.to_string().as_bytes())?;
let encoded = base64::encode(encrypted) + "\n";
self.stream
.write_all(encoded.as_bytes())
.map_err(|e| Error::NetworkError(format!("Failed to write to stream: {}", e)))?;
self.stream
.flush()
.map_err(|e| Error::NetworkError(format!("Failed to flush stream: {}", e)))
}
#[allow(deprecated)]
pub fn read(&mut self) -> Result<Option<String>, Error> {
let mut v_encoded = String::new();
if self
.reader
.read_line(&mut v_encoded)
.map_err(|e| Error::NetworkError(format!("Failed to read line: {}", e)))?
== 0
{
return Ok(None);
}
let encrypted_data = base64::decode(v_encoded.trim())
.map_err(|e| Error::EncryptionError(format!("Failed to decode base64: {}", e)))?;
let decrypted = aes_decrypt(&self.aes, &encrypted_data)?;
let text = String::from_utf8(decrypted)
.map_err(|e| Error::EncryptionError(format!("Failed to decode UTF-8: {}", e)))?;
Ok(Some(text))
}
pub fn close(&self) -> Result<(), Error> {
self.stream
.shutdown(std::net::Shutdown::Both)
.map_err(|e| Error::NetworkError(format!("Failed to shutdown stream: {}", e)))
}
pub fn try_clone(&mut self) -> Result<Self, Error> {
let stream = self
.stream
.try_clone()
.map_err(|e| Error::NetworkError(format!("Failed to clone stream: {}", e)))?;
let stream_clone = stream
.try_clone()
.map_err(|e| Error::NetworkError(format!("Failed to clone stream: {}", e)))?;
Ok(ETcp {
reader: BufReader::new(stream_clone),
stream,
aes: self.aes.clone(),
})
}
}
impl Clone for ETcp {
fn clone(&self) -> Self {
let stream = self
.stream
.try_clone()
.map_err(|e| Error::NetworkError(format!("Failed to clone stream: {}", e)))
.unwrap();
let stream_clone = stream
.try_clone()
.map_err(|e| Error::NetworkError(format!("Failed to clone stream: {}", e)))
.unwrap();
ETcp {
reader: BufReader::new(stream_clone),
stream,
aes: self.aes.clone(),
}
}
}
+197 -98
View File
@@ -1,114 +1,213 @@
use std::{
net::{TcpListener, TcpStream},
sync::Arc,
time::{Duration, Instant},
};
mod cosmetics;
mod methods;
mod types;
mod user;
use openssl::{pkey::Private, rsa::Rsa};
use std::{collections::HashMap, pin::Pin, sync::Arc};
use crate::methods::SocketMap;
use session_rs::server::SessionServer;
use tokio::sync::Mutex;
pub mod cosmetics;
pub mod database;
pub mod encryption;
pub mod methods;
pub mod parser;
pub mod response;
use crate::types::SessionMap;
fn main() -> Result<(), response::Error> {
let rsa = Rsa::generate(2048).map_err(|e| {
response::Error::EncryptionError(format!("Failed to generate RSA keys: {}", e))
})?;
#[tokio::main(flavor = "current_thread")]
async fn main() -> session_rs::Result<()> {
let pool = Arc::new(user::init_db().await);
let server = SessionServer::bind("127.0.0.1:8080").await?;
let client =
mongodb::sync::Client::with_uri_str("mongodb://admin:[email protected]/").map_err(|e| {
response::Error::DatabaseError(format!("Failed to connect to MongoDB: {}", e))
})?;
let database = Arc::new(database::Database::new(&client));
let sockets = SocketMap::default();
let sessions: SessionMap = Arc::new(Mutex::new(HashMap::new()));
let listener = TcpListener::bind("0.0.0.0:8080").map_err(|e| {
response::Error::NetworkError(format!("Failed to bind to port 8080: {}", e))
})?;
server
.session_loop({
let pool = Arc::clone(&pool);
move |session, _| {
let pool = Arc::clone(&pool);
let sessions = Arc::clone(&sessions);
println!("Server listening on port 8080");
Box::pin(async move {
println!("Connected");
let uuid = Arc::new(Mutex::new(String::new()));
let name = Arc::new(Mutex::new(String::new()));
for stream_result in listener.incoming() {
let stream = match stream_result {
Ok(stream) => stream,
Err(e) => {
eprintln!("Failed to accept connection: {}", e);
continue;
}
};
session
.on_close({
let session = session.clone();
let uuid = uuid.clone();
let sessions = Arc::clone(&sessions);
println!("New connection");
let database = Arc::clone(&database);
let sockets = Arc::clone(&sockets);
let rsa = rsa.clone();
move || {
let uuid = uuid.clone();
let sessions = Arc::clone(&sessions);
let session = session.clone();
std::thread::spawn(move || {
if let Err(e) = handle_client(stream, rsa, database, sockets) {
eprintln!("Client error: {}", e);
}
});
}
Box::pin(async move {
let uuid_lock = uuid.lock().await;
if !uuid_lock.is_empty() {
if let Some(sessions) =
sessions.lock().await.get_mut(uuid_lock.as_str())
{
sessions.remove(&session);
}
}
Ok(())
})
}
})
.await;
Ok(())
}
fn handle_client(
stream: TcpStream,
rsa: Rsa<Private>,
database: Arc<database::Database>,
sockets: SocketMap,
) -> Result<(), response::Error> {
let mut stream = encryption::handshake(stream, rsa)?;
let mut last_activity = Instant::now();
match methods::Session::new(stream.try_clone()?, database, sockets) {
Ok((session, res)) => {
stream.send(res)?;
loop {
// Check for inactivity
if last_activity.elapsed() > Duration::from_secs(60) {
println!(
"[MOJANG] {} inactive for too long",
session.local_player.name
session.start_ping(
tokio::time::Duration::from_secs(30),
tokio::time::Duration::from_secs(5),
);
methods::player::logout(&session)?;
println!("[MOJANG] {} went offline", session.local_player.name);
break;
}
match stream.read()? {
None => {
// Client disconnected
println!("[MOJANG] {} disconnected", session.local_player.name);
methods::player::logout(&session)?;
println!("[MOJANG] {} went offline", session.local_player.name);
break;
}
Some(request_string) => {
last_activity = Instant::now();
let (method, params) = parser::parse(&request_string)?;
let response = session.handle_request(&method, &params);
session
.on_request::<methods::Auth, _>({
let pool = Arc::clone(&pool);
let uuid = Arc::clone(&uuid);
let sessions = Arc::clone(&sessions);
let name = Arc::clone(&name);
let session = session.clone();
stream.send(match response {
Ok(response) => response.to_string(),
Err(e) => format!("!{e}"),
})?;
}
}
move |_, token| {
methods::auth::authenticate(
sessions.clone(),
session.clone(),
name.clone(),
uuid.clone(),
token,
pool.clone(),
)
}
})
.await;
session
.on_request::<methods::BuyCloak, _>({
let uuid = Arc::clone(&uuid);
let pool = Arc::clone(&pool);
move |_, item_id| {
let pool = Arc::clone(&pool);
let item_id = item_id.clone();
cosmetics::buy(
cosmetics::CosmeticKind::Cloak,
uuid.clone(),
item_id,
pool,
)
}
})
.await;
session
.on_request::<methods::BuyHat, _>({
let uuid = Arc::clone(&uuid);
let pool = Arc::clone(&pool);
move |_, item_id| {
let pool = Arc::clone(&pool);
let item_id = item_id.clone();
cosmetics::buy(
cosmetics::CosmeticKind::Hat,
uuid.clone(),
item_id,
pool,
)
}
})
.await;
session
.on_request::<methods::SetCloak, _>({
let uuid = Arc::clone(&uuid);
let pool = Arc::clone(&pool);
move |_, item_id| {
let pool = Arc::clone(&pool);
let item_id = item_id.clone();
cosmetics::equip(
cosmetics::CosmeticKind::Cloak,
uuid.clone(),
item_id,
pool,
)
}
})
.await;
session
.on_request::<methods::SetHat, _>({
let uuid = Arc::clone(&uuid);
let pool = Arc::clone(&pool);
move |_, item_id| {
let pool = Arc::clone(&pool);
let item_id = item_id.clone();
cosmetics::equip(
cosmetics::CosmeticKind::Hat,
uuid.clone(),
item_id,
pool,
)
}
})
.await;
session
.on_request::<methods::Emote, _>({
let sessions = Arc::clone(&sessions);
let uuid = Arc::clone(&uuid);
move |_, emote| {
let sessions = Arc::clone(&sessions);
let uuid = Arc::clone(&uuid);
methods::emote::send_emote(sessions, uuid, emote)
}
})
.await;
session
.on_request::<methods::GetPlayer, _>({
let sessions = Arc::clone(&sessions);
let pool = Arc::clone(&pool);
move |_, uuid| {
methods::user::get_user(
sessions.clone(),
uuid.clone(),
pool.clone(),
)
}
})
.await;
session
.on_request::<methods::SendPlayer, _>({
let sessions = Arc::clone(&sessions);
let pool = Arc::clone(&pool);
let uuid = Arc::clone(&uuid);
let name = Arc::clone(&name);
move |_, targets| {
methods::user::send_user(
sessions.clone(),
name.clone(),
uuid.clone(),
targets.targets.clone(),
pool.clone(),
)
}
})
.await;
Ok::<(), session_rs::Error>(())
}) as Pin<Box<dyn Future<Output = _> + Send>>
}
}
Err(e) => {
stream.send(format!("!{e}"))?;
println!("Disconnected");
stream.close()?;
}
}
Ok(())
})
.await
}
+64
View File
@@ -0,0 +1,64 @@
use std::sync::Arc;
use session_rs::session::Session;
use sqlx::SqlitePool;
use crate::{
types::{SessionMap, UUID},
user::User,
};
pub async fn authenticate(
sessions: SessionMap,
session: Session,
name: UUID,
uuid: UUID,
session_token: String,
pool: Arc<SqlitePool>,
) -> Result<User, String> {
if !uuid.lock().await.is_empty() {
return Err(format!("Already authenticated"));
}
let client = reqwest::Client::new();
let response = client
.get("https://api.minecraftservices.com/minecraft/profile")
.bearer_auth(&session_token)
.send()
.await
.map_err(|_| "Failed to validate session".to_string())?;
if !response.status().is_success() {
return Err(format!(
"Authentication failed with code {}",
response.status()
));
}
let auth = response
.json::<crate::types::MinecraftAuthResponse>()
.await
.map_err(|_| "Unable to parse auth response".to_string())
.and_then(|auth| {
let id = crate::types::format_uuid(&auth.id)?;
Ok(crate::types::MinecraftAuthResponse {
name: auth.name,
id,
})
})?;
*uuid.lock().await = auth.id.clone();
*name.lock().await = auth.name.clone();
sessions
.lock()
.await
.entry(auth.id.clone())
.or_default()
.insert(session);
println!("{:?}", auth);
crate::user::get_put(&auth.id, &pool).await
}
-63
View File
@@ -1,63 +0,0 @@
use mongodb::bson::doc;
use crate::{methods::Session, response::Result};
pub fn buy_cloak(session: &Session, cloak: String) -> Result {
let filter = doc! {
"uuid": session.local_player.id.clone()
};
if let Some(doc) = session.database.players.find_one(filter.clone()).run()? {
if doc.coins >= 100 && !doc.cloaks.contains(&cloak) {
session
.database
.players
.update_one(
filter,
doc! { "$inc": { "coins": -100 }, "$push": { "cloaks": cloak.clone() } },
)
.run()?;
Ok(crate::response::Response::SuccessfulTransaction(format!(
"cloak={cloak}"
)))
} else {
Err(crate::response::Error::TransactionError(
"Not enough coins".to_string(),
))
}
} else {
Err(crate::response::Error::DatabaseError(
"Player not found".to_string(),
))
}
}
pub fn buy_hat(session: &Session, hat: String) -> Result {
let filter = doc! {
"uuid": session.local_player.id.clone()
};
if let Some(doc) = session.database.players.find_one(filter.clone()).run()? {
if doc.coins >= 50 && !doc.hats.contains(&hat) {
session
.database
.players
.update_one(
filter,
doc! { "$inc": { "coins": -50 }, "$push": { "hats": hat.clone() } },
)
.run()?;
Ok(crate::response::Response::SuccessfulTransaction(format!(
"hat={hat}"
)))
} else {
Err(crate::response::Error::TransactionError(
"Not enough coins".to_string(),
))
}
} else {
Err(crate::response::Error::DatabaseError(
"Player not found".to_string(),
))
}
}
+29 -9
View File
@@ -1,14 +1,34 @@
use crate::response::{Response, Result};
use crate::types::{SessionMap, UUID};
use super::Session;
pub async fn send_emote(
sessions: SessionMap,
uuid: UUID,
emote: crate::types::EmoteRequest,
) -> Result<(), String> {
let emote_event = crate::types::EventEmote {
from: uuid.lock().await.clone(),
emote: emote.emote,
};
pub fn emote(session: &Session, emote: String, notify: Vec<&str>) -> Result {
if notify.len() > 0 {
session.notify(
&notify,
&format!("emote@uuid={}@name={emote}", session.local_player.id),
)?;
for target in emote.targets {
if let Some(sessions) = sessions.lock().await.get_mut(&target) {
let emote_event = emote_event.clone();
let mut bad_sessions = Vec::new();
for s in sessions.iter() {
if s.notify::<crate::methods::EmoteEvent>(emote_event.clone())
.await
.is_err()
{
bad_sessions.push(s.clone());
}
}
for s in bad_sessions {
sessions.remove(&s);
}
}
}
Ok(Response::Success)
Ok(())
}
+100 -181
View File
@@ -1,197 +1,116 @@
mod buy;
mod emote;
pub mod player;
mod set_cloak;
mod set_hat;
use std::{
collections::HashMap,
sync::{mpsc, Arc, Mutex},
};
pub mod auth;
pub mod emote;
pub mod user;
use serde::{Deserialize, Serialize};
use session_rs::Method;
use crate::{
encryption::ETcp,
parser::ParamMap,
response::{PlayerResponse, Response, Result},
types::{EmoteRequest, EventEmote, PlayerStream},
user::User,
};
#[derive(Debug, Deserialize, Serialize)]
pub struct LocalPlayer {
pub id: String,
pub name: String,
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Auth;
impl Method for Auth {
const NAME: &'static str = "auth";
type Request = String;
type Response = User;
type Error = String;
}
pub type SocketMap = Arc<Mutex<HashMap<String, ETcp>>>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetCloak;
pub struct Session {
pub session_token: String,
pub database: Arc<crate::database::Database>,
pub local_player: LocalPlayer,
sockets: SocketMap,
impl Method for SetCloak {
const NAME: &'static str = "set_cloak";
type Request = String;
type Response = String;
type Error = String;
}
impl Session {
pub fn new(
mut stream: ETcp,
database: Arc<crate::database::Database>,
sockets: SocketMap,
) -> Result<(Self, Response)> {
let (token_send, token_recv) =
mpsc::channel::<std::result::Result<String, crate::response::Error>>();
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetHat;
let stream_2 = stream.try_clone()?;
std::thread::spawn(move || match stream.read() {
Ok(Some(session_token)) => token_send.send(Ok(session_token)),
_ => token_send.send(Err(crate::response::Error::InvalidHandshake(
"Failed to read session token".to_string(),
))),
});
match token_recv.recv_timeout(std::time::Duration::from_secs(20)) {
Ok(Ok(session_token)) => {
// Validate session id
let response = minreq::get("https://api.minecraftservices.com/minecraft/profile")
.with_header("Authorization", &format!("Bearer {session_token}"))
.send()
.map_err(|_| {
crate::response::Error::InvalidSession(
"Failed to validate session".to_string(),
)
})?;
// If the session is invalid, return an error
if response.status_code != 200 {
return Err(crate::response::Error::InvalidSession(format!(
"Invalid session status code: {}",
response.status_code
)));
}
// Parse the player data
let local_player: LocalPlayer = response.json().map_err(|_| {
crate::response::Error::InvalidSession(
"Failed to parse player data".to_string(),
)
})?;
println!("[MOJANG] {} successfully logged on", &local_player.name);
let session = Self {
session_token,
database,
local_player,
sockets,
};
// Capture the player
match player::login(&session) {
Ok(player) => {
session.add_socket(stream_2);
Ok((session, player))
}
Err(e) => Err(e),
}
}
Err(mpsc::RecvTimeoutError::Timeout) => Err(crate::response::Error::Timeout(
"Session handshake timed out".to_string(),
)),
_ => Err(crate::response::Error::InvalidHandshake(
"Failed to receive handshake response".to_string(),
)),
}
}
pub fn handle_request(&self, method: &str, params: &HashMap<String, String>) -> Result {
match method {
"ping" => Ok(Response::Pong),
"set_cloak" => set_cloak::set_cloak(
self,
params.parse_param("cloak")?,
params
.parse_param::<String>("notify")
.unwrap_or_default()
.split("$")
.collect(),
),
"set_hat" => set_hat::set_hat(
self,
params.parse_param("hat")?,
params
.parse_param::<String>("notify")
.unwrap_or_default()
.split("$")
.collect(),
),
"emote" => emote::emote(
self,
params.parse_param("name")?,
params
.parse_param::<String>("notify")
.unwrap_or_default()
.split("$")
.collect(),
),
"player" => player::player(self, params.parse_param("uuid")?),
"players" => {
let mut players: Vec<PlayerResponse> = Vec::new();
let uuids = params.parse_param::<String>("uuids")?;
if uuids.is_empty() {
return Err(crate::response::Error::InvalidParameter {
param: "uuids".to_string(),
reason: "UUIDs list cannot be empty".to_string(),
});
}
for uuid in uuids.split("$") {
if uuid.is_empty() {
return Err(crate::response::Error::InvalidParameter {
param: "uuids".to_string(),
reason: "UUID cannot be empty".to_string(),
});
}
match player::player(self, uuid.to_string())? {
Response::Player(p) => players.push(p),
_ => {}
}
}
Ok(Response::Players(players))
}
"buy_cloak" => buy::buy_cloak(self, params.parse_param("cloak")?),
"buy_hat" => buy::buy_hat(self, params.parse_param("hat")?),
_ => Err(crate::response::Error::InvalidMethod(method.to_string())),
}
}
impl Method for SetHat {
const NAME: &'static str = "set_hat";
type Request = String;
type Response = String;
type Error = String;
}
impl Session {
pub fn add_socket(&self, sock: ETcp) {
self.sockets
.lock()
.unwrap()
.insert(self.local_player.id.clone(), sock);
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuyCloak;
pub fn remove_from_sockets(&self) {
self.sockets.lock().unwrap().remove(&self.local_player.id);
}
pub fn notify(&self, players: &[&str], message: &str) -> Result<()> {
let mut sockets = self.sockets.lock().unwrap();
for player in players {
if let Some(i) = sockets.get_mut(&player.to_string()) {
i.send(message)
.map_err(|e| crate::response::Error::DatabaseError(format!("{e}")))?;
}
}
Ok(())
}
impl Method for BuyCloak {
const NAME: &'static str = "buy_cloak";
type Request = String;
type Response = String;
type Error = String;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuyHat;
impl Method for BuyHat {
const NAME: &'static str = "buy_hat";
type Request = String;
type Response = String;
type Error = String;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Emote;
impl Method for Emote {
const NAME: &'static str = "emote";
type Request = EmoteRequest;
type Response = ();
type Error = String;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmoteEvent;
impl Method for EmoteEvent {
const NAME: &'static str = "emote_event";
type Request = EventEmote;
type Response = ();
type Error = ();
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetPlayer;
impl Method for GetPlayer {
const NAME: &'static str = "get_player";
type Request = String;
type Response = Option<User>;
type Error = String;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Player;
impl Method for Player {
const NAME: &'static str = "player";
type Request = PlayerStream;
type Response = ();
type Error = ();
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendPlayer;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendPlayerRequest {
pub targets: Vec<String>,
}
impl Method for SendPlayer {
const NAME: &'static str = "send_player";
type Request = SendPlayerRequest;
type Response = ();
type Error = String;
}
-122
View File
@@ -1,122 +0,0 @@
use mongodb::bson::doc;
use crate::{
database::Player,
response::{Response, Result},
};
use super::Session;
pub fn player(session: &Session, uuid: String) -> Result {
// Try to find the player with the given UUID
let mut cursor = session
.database
.players
.find(doc! { "uuid": uuid.clone() })
.run()?;
// Check if there's a document in the cursor
Ok(match cursor.next() {
Some(Ok(player)) => {
if player.online {
Response::Player(crate::response::PlayerResponse::Player {
cloak: player.cloak,
uuid,
cloaks: player.cloaks,
hats: player.hats,
hat: player.hat,
})
} else {
crate::response::Response::Player(crate::response::PlayerResponse::NonSaturnPlayer(
uuid,
))
}
}
_ => crate::response::Response::Player(crate::response::PlayerResponse::NonSaturnPlayer(
uuid,
)),
})
}
pub fn create(session: &Session) -> Result {
let uuid = session.local_player.id.clone();
let player = Player {
uuid: uuid.clone(),
cloaks: Vec::new(),
cloak: "".to_string(),
hats: Vec::new(),
hat: String::new(),
coins: 0,
online: true,
};
session.database.players.insert_one(player.clone()).run()?;
Ok(Response::Player(crate::response::PlayerResponse::Player {
cloak: player.cloak,
uuid,
cloaks: player.cloaks,
hats: player.hats,
hat: player.hat,
}))
}
pub fn login(session: &Session) -> Result {
let uuid = session.local_player.id.clone();
// Try to find the player with the given UUID
let mut cursor = session
.database
.players
.find(doc! { "uuid": uuid.clone() })
.run()?;
// Check if there's a document in the cursor
match cursor.next() {
Some(Ok(mut player)) => {
// Update player's online status
player.online = true;
session
.database
.players
.update_one(
doc! { "uuid": uuid.clone() },
doc! { "$set": { "online": true } },
)
.run()?;
// Player exists, deserialize and return their data
Ok(Response::Player(crate::response::PlayerResponse::Player {
cloak: player.cloak,
uuid,
cloaks: player.cloaks,
hats: player.hats,
hat: player.hat,
}))
}
// No document found or deserialization error
_ => create(session),
}
}
pub fn logout(session: &Session) -> Result {
session.remove_from_sockets();
let uuid = session.local_player.id.clone();
// Update player's online status to false
session
.database
.players
.update_one(
doc! { "uuid": uuid.clone() },
doc! { "$set": { "online": false } },
)
.run()?;
Ok(Response::Player(
crate::response::PlayerResponse::NonSaturnPlayer(uuid),
))
}
-49
View File
@@ -1,49 +0,0 @@
use mongodb::bson::doc;
use crate::response::{Error, Response, Result};
use super::{player, Session};
pub fn set_cloak(session: &Session, cloak: String, notify: Vec<&str>) -> Result {
match player::player(session, session.local_player.id.clone())? {
Response::Player(crate::response::PlayerResponse::Player { cloaks, .. }) => {
if !cloaks.contains(&cloak) && !cloak.is_empty() {
return Err(Error::ValidationError(format!(
"Player does not own cloak: {}",
cloak
)));
}
session
.database
.players
.update_one(
doc! {
"uuid": session.local_player.id.clone(),
},
doc! {
"$set": {
"cloak": cloak.clone(),
},
},
)
.run()
.map_err(|e| Error::DatabaseError(format!("Failed to update cloak: {}", e)))?;
if notify.len() > 0 {
session.notify(
&notify,
&format!(
"update_cloak@uuid={}@cloak={cloak}",
session.local_player.id
),
)?;
}
Ok(Response::Success)
}
_ => Err(Error::ValidationError(
"Invalid player data received".to_string(),
)),
}
}
-46
View File
@@ -1,46 +0,0 @@
use mongodb::bson::doc;
use crate::response::{Error, Response, Result};
use super::{player, Session};
pub fn set_hat(session: &Session, hat: String, notify: Vec<&str>) -> Result {
match player::player(session, session.local_player.id.clone())? {
Response::Player(crate::response::PlayerResponse::Player { hats, .. }) => {
if !hats.contains(&hat) && !hat.is_empty() {
return Err(Error::ValidationError(format!(
"Player does not own hat: {}",
hat
)));
}
session
.database
.players
.update_one(
doc! {
"uuid": session.local_player.id.clone(),
},
doc! {
"$set": {
"hat": hat.clone(),
},
},
)
.run()
.map_err(|e| Error::DatabaseError(format!("Failed to update hat: {}", e)))?;
if notify.len() > 0 {
session.notify(
&notify,
&format!("update_hat@uuid={}@hat={hat}", session.local_player.id),
)?;
}
Ok(Response::Success)
}
_ => Err(Error::ValidationError(
"Invalid player data received".to_string(),
)),
}
}
+60
View File
@@ -0,0 +1,60 @@
use std::sync::Arc;
use sqlx::SqlitePool;
use crate::{
methods,
types::{PlayerStream, SessionMap, UUID},
user::User,
};
pub async fn get_user(
sessions: SessionMap,
uuid: String,
pool: Arc<SqlitePool>,
) -> Result<Option<User>, String> {
if !sessions.lock().await.contains_key(&uuid) {
return Ok(None);
}
Ok(Some(crate::user::get(&uuid, pool.as_ref()).await?))
}
pub async fn send_user(
sessions: SessionMap,
name: UUID,
uuid: UUID,
targets: Vec<String>,
pool: Arc<SqlitePool>,
) -> Result<(), String> {
println!("send player {targets:?}");
let uuid = uuid.lock().await.to_string();
let user = PlayerStream {
player: crate::user::get(&uuid, pool.as_ref()).await?,
uuid: uuid.clone(),
name: name.lock().await.clone(),
};
println!("ready {targets:?}");
for target in targets {
if let Some(sessions) = sessions.lock().await.get_mut(&target) {
let mut bad_sessions = Vec::new();
for s in sessions.iter() {
if s.notify::<methods::Player>(user.clone()).await.is_err() {
bad_sessions.push(s.clone());
}
}
for s in bad_sessions {
sessions.remove(&s);
}
}
}
println!("sent");
Ok(())
}
-49
View File
@@ -1,49 +0,0 @@
use std::{collections::HashMap, str::FromStr};
pub trait ParamMap {
fn parse_param<T: FromStr>(&self, param: &str) -> Result<T, crate::response::Error>;
}
impl ParamMap for HashMap<String, String> {
fn parse_param<T: FromStr>(&self, param: &str) -> Result<T, crate::response::Error> {
match self.get(param) {
Some(value) => T::from_str(value).map_err(|_| crate::response::Error::InvalidParameter {
param: param.to_string(),
reason: "Failed to parse parameter value".to_string(),
}),
None => Err(crate::response::Error::ParameterNotFound(param.to_string())),
}
}
}
pub fn parse(s: &str) -> Result<(String, HashMap<String, String>), crate::response::Error> {
let mut parts = s.split("@");
let method = parts.next()
.ok_or_else(|| crate::response::Error::InvalidRequest("Empty request".to_string()))?
.trim()
.to_lowercase();
if method.is_empty() {
return Err(crate::response::Error::InvalidRequest("Method name cannot be empty".to_string()));
}
let mut params = HashMap::new();
for part in parts {
let key_value: Vec<&str> = part.split("=").collect();
match key_value.as_slice() {
[key, value] => {
let key = key.trim();
if key.is_empty() {
return Err(crate::response::Error::InvalidRequest("Parameter key cannot be empty".to_string()));
}
params.insert(key.to_string(), value.trim().to_string());
},
_ => return Err(crate::response::Error::InvalidRequest(
format!("Invalid parameter format in part: {}", part)
)),
}
}
Ok((method, params))
}
-111
View File
@@ -1,111 +0,0 @@
pub type Result<T = Response> = std::result::Result<T, Error>;
#[derive(Debug, Clone)]
pub enum PlayerResponse {
Player {
uuid: String,
cloaks: Vec<String>,
cloak: String,
hats: Vec<String>,
hat: String,
},
NonSaturnPlayer(String),
}
impl std::fmt::Display for PlayerResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlayerResponse::Player {
uuid,
cloaks,
cloak,
hats,
hat,
} => write!(
f,
"player@cloak={cloak}@uuid={uuid}@cloaks={}@hats={}@hat={hat}@saturn=true",
cloaks.join("$"),
hats.join("$"),
),
PlayerResponse::NonSaturnPlayer(uuid) => write!(f, "@saturn=false@uuid={uuid}"),
}
}
}
#[derive(Debug, Clone)]
pub enum Response {
Pong,
Success,
SuccessfulTransaction(String),
Player(PlayerResponse),
Players(Vec<PlayerResponse>),
}
impl std::fmt::Display for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Response::Pong => write!(f, "Pong"),
Response::Success => write!(f, "Success"),
Response::Player(player) => write!(f, "{player}"),
Response::Players(players) => {
write!(
f,
"{}",
players
.iter()
.map(|p| format!("{p}"))
.collect::<Vec<String>>()
.join("\n")
)
}
Response::SuccessfulTransaction(s) => write!(f, "successful_transaction@{s}"),
}
}
}
#[derive(Debug, Clone)]
pub enum Error {
InvalidRequest(String),
InvalidMethod(String),
InvalidParameter { param: String, reason: String },
ParameterNotFound(String),
InvalidSession(String),
InvalidHandshake(String),
DatabaseError(String),
NetworkError(String),
Timeout(String),
AuthenticationError(String),
ValidationError(String),
EncryptionError(String),
TransactionError(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
Error::InvalidMethod(method) => write!(f, "Invalid method: {}", method),
Error::InvalidParameter { param, reason } => {
write!(f, "Invalid parameter '{}': {}", param, reason)
}
Error::ParameterNotFound(param) => write!(f, "Required parameter not found: {}", param),
Error::InvalidSession(details) => write!(f, "Invalid session: {}", details),
Error::InvalidHandshake(details) => write!(f, "Handshake failed: {}", details),
Error::DatabaseError(details) => write!(f, "Database error: {}", details),
Error::NetworkError(details) => write!(f, "Network error: {}", details),
Error::Timeout(operation) => write!(f, "Operation timed out: {}", operation),
Error::AuthenticationError(details) => write!(f, "Authentication failed: {}", details),
Error::ValidationError(details) => write!(f, "Validation failed: {}", details),
Error::EncryptionError(details) => write!(f, "Encryption error: {}", details),
Error::TransactionError(details) => write!(f, "Transaction error: {}", details),
}
}
}
impl From<mongodb::error::Error> for Error {
fn from(e: mongodb::error::Error) -> Self {
println!("{e}");
Error::DatabaseError(e.to_string())
}
}
impl std::error::Error for Error {}
+7
View File
@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS users (
uuid TEXT PRIMARY KEY,
cloak TEXT DEFAULT "",
hat TEXT DEFAULT "",
cloaks TEXT DEFAULT '[]',
hats TEXT DEFAULT '[]'
);
+55
View File
@@ -0,0 +1,55 @@
use serde::{Deserialize, Serialize};
use session_rs::session::Session;
use tokio::sync::Mutex;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use crate::user::User;
pub type SessionMap = Arc<Mutex<HashMap<String, HashSet<Session>>>>;
pub type UUID = Arc<Mutex<String>>;
#[derive(Debug, Deserialize, Serialize)]
pub struct MinecraftAuthResponse {
pub id: String,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
/// Received from the client to send to the targets
pub struct EmoteRequest {
pub emote: String,
pub targets: Vec<String>,
}
/// To send to the targets
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventEmote {
pub emote: String,
pub from: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayerStream {
pub player: User,
pub uuid: String,
pub name: String,
}
pub fn format_uuid(input: &str) -> Result<String, &'static str> {
if input.len() != 32 {
return Err("Input must be exactly 32 hex characters");
}
Ok(format!(
"{}-{}-{}-{}-{}",
&input[0..8],
&input[8..12],
&input[12..16],
&input[16..20],
&input[20..32],
))
}
+72
View File
@@ -0,0 +1,72 @@
use serde::{Deserialize, Serialize};
use sqlx::{Row, SqlitePool};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub cloak: String,
pub hat: String,
pub cloaks: Vec<String>,
pub hats: Vec<String>,
}
// Initialize SQLite connection pool
pub async fn init_db() -> SqlitePool {
let pool = SqlitePool::connect("sqlite://users.db?mode=rwc")
.await
.expect("Failed to open database");
sqlx::query(include_str!("schema.sql"))
.execute(&pool)
.await
.expect("Failed to run schema");
pool
}
// Get user by UUID
pub async fn get(uuid: &str, pool: &SqlitePool) -> Result<User, String> {
let row = sqlx::query("SELECT cloak, hat, cloaks, hats FROM users WHERE uuid = ?")
.bind(uuid)
.fetch_optional(pool)
.await
.map_err(|e| e.to_string())?;
if let Some(r) = row {
let user = User {
cloak: r.get::<String, _>("cloak"),
hat: r.get::<String, _>("hat"),
cloaks: serde_json::from_str(&r.get::<String, _>("cloaks")).unwrap_or_default(),
hats: serde_json::from_str(&r.get::<String, _>("hats")).unwrap_or_default(),
};
Ok(user)
} else {
Err("User not found".into())
}
}
// Create user if not exists
pub async fn get_put(uuid: &str, pool: &SqlitePool) -> Result<User, String> {
match get(uuid, pool).await {
Ok(user) => Ok(user),
Err(_) => {
let user = User {
cloak: "".into(),
hat: "".into(),
cloaks: vec![],
hats: vec![],
};
sqlx::query(
"INSERT INTO users (uuid, cloak, hat, cloaks, hats) VALUES (?, ?, ?, ?, ?)",
)
.bind(uuid)
.bind(&user.cloak)
.bind(&user.hat)
.bind(serde_json::to_string(&user.cloaks).unwrap())
.bind(serde_json::to_string(&user.hats).unwrap())
.execute(pool)
.await
.map_err(|e| e.to_string())?;
Ok(user)
}
}
}