the silly

This commit is contained in:
Shrecknt 2024-09-11 11:38:11 -07:00
commit 82782182e4
7 changed files with 3048 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

2570
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

19
Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "sun_server"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7.5", features = ["macros", "ws"] }
eyre = "0.6.12"
futures-util = { version = "0.3.30", features = ["sink"] }
mongodb = "3.1.0"
parking_lot = "0.12.3"
reqwest = { version = "0.12.7", features = ["json"] }
serde = "1.0.204"
serde_json = "1.0.128"
thiserror = "1.0.63"
tokio = { version = "1.39.2", features = ["full"] }
url = { version = "2.5.2", features = ["serde"] }
uuid = { version = "1.10.0", features = ["serde"] }
weak-table = "0.3.2"

82
src/auth/mod.rs Normal file
View file

@ -0,0 +1,82 @@
use axum::extract::ws::WebSocket;
use axum::http::StatusCode;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
use crate::REQWEST_CLIENT;
use packet::{
ClientboundPacket, HandshakeRequest, HandshakeResponse, PacketIo, PacketIoError,
ServerboundPacket, WritePacket,
};
pub mod packet;
#[derive(Serialize, Deserialize)]
pub struct GameProfileProperty {
pub name: String,
pub value: String,
pub signature: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct GameProfile {
pub id: Uuid,
pub name: String,
pub properties: Vec<GameProfileProperty>,
}
pub async fn authenticate_socket(
socket: &mut WebSocket,
) -> Result<GameProfile, AuthenticateSocketError> {
let join_id = Uuid::new_v4();
socket
.write_packet(&ClientboundPacket::HandshakeRequest(HandshakeRequest {
join_id,
}))
.await?;
let HandshakeResponse { player_name } = match socket
.read_packet()
.await?
.ok_or(AuthenticateSocketError::ClientDisconnected)?
{
ServerboundPacket::HandshakeResponse(response) => response,
#[allow(unreachable_patterns)]
packet => return Err(AuthenticateSocketError::InvalidPacket(packet)),
};
let url = Url::parse_with_params(
"https://sessionserver.mojang.com/session/minecraft/hasJoined",
&[
("username", player_name.clone()),
("serverId", format!("ssi-{}", join_id)),
],
)?;
let response = REQWEST_CLIENT.get(url).send().await?;
if !response.status().is_success() {
return Err(AuthenticateSocketError::Non200Response(response.status()));
}
response.json().await.map_err(Into::into)
}
#[derive(Debug, Error)]
pub enum AuthenticateSocketError {
#[error("non-200 status code: {0}")]
Non200Response(StatusCode),
#[error("invalid packet: {0:?}")]
InvalidPacket(ServerboundPacket),
#[error("client disconnected")]
ClientDisconnected,
#[error(transparent)]
PacketIo(#[from] PacketIoError),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
UrlParse(#[from] url::ParseError),
}

35
src/auth/packet/mod.rs Normal file
View file

@ -0,0 +1,35 @@
mod traits;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub use self::traits::*;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "op", content = "d", rename_all = "snake_case")]
pub enum ClientboundPacket {
// Authentication
HandshakeRequest(HandshakeRequest),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "op", content = "d", rename_all = "snake_case")]
pub enum ServerboundPacket {
// Authentication
HandshakeResponse(HandshakeResponse),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct HandshakeRequest {
pub join_id: Uuid,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct HandshakeResponse {
pub player_name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PlayerJoin {
pub player_name: String,
}

67
src/auth/packet/traits.rs Normal file
View file

@ -0,0 +1,67 @@
use axum::extract::ws::{Message, WebSocket};
use futures_util::{
stream::{SplitSink, SplitStream},
SinkExt, TryStreamExt,
};
use thiserror::Error;
use super::{ClientboundPacket, ServerboundPacket};
#[derive(Debug, Error)]
pub enum PacketIoError {
#[error("the client has disconnected")]
Disconnectetd,
#[error("the socket is closing")]
SocketClosing,
#[error("the client sent an invalid websocket message: {0:?}")]
InvalidMessage(Message),
#[error(transparent)]
Axum(#[from] axum::Error),
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
}
pub trait PacketIo {
async fn read_packet(&mut self) -> Result<Option<ServerboundPacket>, PacketIoError>;
}
impl PacketIo for WebSocket {
async fn read_packet(&mut self) -> Result<Option<ServerboundPacket>, PacketIoError> {
process_packet(self.try_next().await?.ok_or(PacketIoError::Disconnectetd)?)
}
}
impl PacketIo for SplitStream<WebSocket> {
async fn read_packet(&mut self) -> Result<Option<ServerboundPacket>, PacketIoError> {
process_packet(self.try_next().await?.ok_or(PacketIoError::Disconnectetd)?)
}
}
fn process_packet(message: Message) -> Result<Option<ServerboundPacket>, PacketIoError> {
match message {
Message::Text(text) => Ok(Some(serde_json::from_str(&text)?)),
Message::Ping(_) | Message::Pong(_) => Ok(None),
Message::Close(_) => Err(PacketIoError::SocketClosing),
msg => Err(PacketIoError::InvalidMessage(msg)),
}
}
pub trait WritePacket {
async fn write_packet(&mut self, packet: &ClientboundPacket) -> Result<(), PacketIoError>;
}
impl WritePacket for WebSocket {
async fn write_packet(&mut self, packet: &ClientboundPacket) -> Result<(), PacketIoError> {
let serialized = serde_json::to_string(packet)?;
self.send(Message::Text(serialized)).await?;
Ok(())
}
}
impl WritePacket for SplitSink<WebSocket, Message> {
async fn write_packet(&mut self, packet: &ClientboundPacket) -> Result<(), PacketIoError> {
let serialized = serde_json::to_string(packet)?;
self.send(Message::Text(serialized)).await?;
Ok(())
}
}

274
src/main.rs Normal file
View file

@ -0,0 +1,274 @@
use std::{
collections::HashSet,
net::SocketAddr,
sync::{Arc, LazyLock, Weak},
};
use auth::authenticate_socket;
use axum::{
extract::{
ws::{Message, WebSocket},
Path, State, WebSocketUpgrade,
},
http::header,
response::IntoResponse,
routing::get,
Router,
};
use futures_util::{stream::SplitSink, SinkExt, StreamExt, TryStreamExt};
use mongodb::{
bson::{oid::ObjectId, Bson, Document},
Client, Collection, Database,
};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use serde_json::json;
use uuid::Uuid;
use weak_table::PtrWeakHashSet;
mod auth;
static CONNECTED_MAP: LazyLock<RwLock<HashSet<(MinecraftServer, Uuid)>>> =
LazyLock::new(|| RwLock::new(HashSet::new()));
static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
reqwest::Client::builder()
.user_agent("Samsung Smart Fridge")
.build()
.expect("Failed to initialize Reqwest")
});
static CONNECTIONS: LazyLock<
RwLock<
PtrWeakHashSet<Weak<tokio::sync::Mutex<SplitSink<WebSocket, axum::extract::ws::Message>>>>,
>,
> = LazyLock::new(|| RwLock::new(PtrWeakHashSet::new()));
#[tokio::main]
async fn main() -> eyre::Result<()> {
let mongo = Client::with_uri_str("mongodb://localhost:27017")
.await
.expect("Unable to connect to mongodb database");
let app = Router::new()
.route("/api/cape/:uuid", get(cape_lookup_endpoint))
.route("/api/status/:uuid", get(status_lookup_endpoint))
.route("/api/cape_status/:uuid", get(cape_status_lookup_endpoint))
.route("/api/ws", get(socket_endpoint));
let listener = tokio::net::TcpListener::bind("127.0.0.1:8585")
.await
.unwrap();
axum::serve(listener, app.with_state(mongo.database("sun-server")))
.await
.unwrap();
Ok(())
}
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Clone)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
enum MinecraftServer {
NetworkServer(String),
SocketAddr(SocketAddr),
}
#[derive(Serialize, Deserialize)]
struct Cape {
_id: ObjectId,
uuid: String,
cape: String,
}
#[derive(Serialize, Deserialize)]
struct User {
_id: ObjectId,
uuid: String,
status: Status,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Status {
Connected,
ConnectedSpecial,
ConnectedAdmin,
Disconnected,
NonSun,
}
async fn cape_lookup(uuid: &Uuid, mongo: &Database) -> Option<Cape> {
let capes: Collection<Cape> = mongo.collection("capes");
capes
.find_one(Document::from_iter([(
"uuid".into(),
Bson::String(uuid.to_string()),
)]))
.await
.unwrap()
}
async fn status_lookup(server: &MinecraftServer, uuid: &Uuid, mongo: &Database) -> Option<Status> {
let users: Collection<User> = mongo.collection("users");
let user = users
.find_one(Document::from_iter([(
"uuid".into(),
Bson::String(uuid.to_string()),
)]))
.await
.unwrap();
let Some(user) = user else {
return None;
};
let is_connected = CONNECTED_MAP
.read()
.contains(&(server.clone(), uuid.to_owned()));
Some(if is_connected {
user.status
} else {
Status::Disconnected
})
}
async fn cape_lookup_endpoint(
Path(uuid): Path<Uuid>,
State(mongo): State<Database>,
) -> impl IntoResponse {
let cape_str = cape_lookup(&uuid, &mongo)
.await
.map(|c| c.cape)
.unwrap_or(String::new());
(
[(header::CONTENT_TYPE, "application/json")],
json!({"cape": cape_str}).to_string(),
)
}
async fn status_lookup_endpoint(
Path((server, uuid)): Path<(MinecraftServer, Uuid)>,
State(mongo): State<Database>,
) -> impl IntoResponse {
let status = status_lookup(&server, &uuid, &mongo)
.await
.unwrap_or(Status::NonSun);
(
[(header::CONTENT_TYPE, "application/json")],
json!({"status": status}).to_string(),
)
}
async fn cape_status_lookup_endpoint(
Path((server, uuid)): Path<(MinecraftServer, Uuid)>,
State(mongo): State<Database>,
) -> impl IntoResponse {
let cape_str = cape_lookup(&uuid, &mongo)
.await
.map(|c| c.cape)
.unwrap_or(String::new());
let status = status_lookup(&server, &uuid, &mongo)
.await
.unwrap_or(Status::NonSun);
(
[(header::CONTENT_TYPE, "application/json")],
json!({"cape": cape_str, "status": status}).to_string(),
)
}
#[axum::debug_handler]
async fn socket_endpoint(ws: WebSocketUpgrade) -> impl IntoResponse {
ws.on_upgrade(|socket| handle_socket(socket))
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
enum StatusType {
Connect,
Disconnect,
}
#[derive(Serialize, Deserialize, Clone)]
struct StatusPacket {
server: MinecraftServer,
status_type: StatusType,
}
#[derive(Serialize, Deserialize, Clone)]
struct ChatPacket {
from: String,
contents: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(tag = "op", content = "d", rename_all = "snake_case")]
enum Packet {
Status(StatusPacket),
Chat(ChatPacket),
}
async fn handle_socket(mut socket: WebSocket) {
let player_info = match authenticate_socket(&mut socket).await {
Ok(player_info) => player_info,
Err(err) => {
eprintln!("failed to authenticate socket {err:?}");
return;
}
};
let (socket_w, mut socket_r) = socket.split();
let socket_w = Arc::new(tokio::sync::Mutex::new(socket_w));
CONNECTIONS.write().insert(socket_w.clone().into());
while let Some(message) = socket_r.try_next().await.ok().flatten() {
let Ok(message) = message.into_text() else {
continue;
};
let Ok(message) = serde_json::from_str::<Packet>(&message) else {
continue;
};
match message {
Packet::Status(message) => {
match message.status_type {
StatusType::Connect => CONNECTED_MAP
.write()
.insert((message.server, player_info.id)),
StatusType::Disconnect => CONNECTED_MAP
.write()
.remove(&(message.server, player_info.id)),
};
}
Packet::Chat(message) => {
if message.from.to_lowercase() != player_info.name.to_lowercase() {
return;
}
let Ok(message) = serde_json::to_string(&Packet::Chat(message)) else {
return;
};
let connections = CONNECTIONS.read().iter().collect::<Vec<_>>();
for connection in connections {
let _ = connection
.lock()
.await
.send(Message::Text(message.clone()))
.await;
}
}
};
}
let to_remove = CONNECTED_MAP
.read()
.iter()
.filter_map(|c| {
if c.1 == player_info.id {
Some(c.to_owned())
} else {
None
}
})
.collect::<Vec<_>>();
let mut connected_map_lock = CONNECTED_MAP.write();
for r in to_remove {
connected_map_lock.remove(&r);
}
// expensive but I don't care atm
CONNECTIONS.write().remove_expired();
}