the silly
This commit is contained in:
commit
82782182e4
7 changed files with 3048 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
2570
Cargo.lock
generated
Normal file
2570
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal 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
82
src/auth/mod.rs
Normal 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
35
src/auth/packet/mod.rs
Normal 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
67
src/auth/packet/traits.rs
Normal 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
274
src/main.rs
Normal 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();
|
||||||
|
}
|
Loading…
Reference in a new issue