🎉WebSocket refactored!

This commit is contained in:
Shiroyasha 2024-10-28 03:19:33 +03:00
parent 7a4f3dc7a5
commit 4c0871e26c
Signed by: shiroyashik
GPG key ID: E4953D3940D7860A
30 changed files with 650 additions and 587 deletions

View file

@ -21,10 +21,10 @@ async fn versions() -> ApiResult<Json<Value>> {
let mut directories = Vec::new();
let mut entries = fs::read_dir(dir_path).await.map_err(|err| internal_and_log(err))?;
let mut entries = fs::read_dir(dir_path).await.map_err(internal_and_log)?;
while let Some(entry) = entries.next_entry().await.map_err(|err| internal_and_log(err))? {
if entry.metadata().await.map_err(|err| internal_and_log(err))?.is_dir() {
while let Some(entry) = entries.next_entry().await.map_err(internal_and_log)? {
if entry.metadata().await.map_err(internal_and_log)?.is_dir() {
if let Some(name) = entry.file_name().to_str() {
let name = name.to_string();
if !name.starts_with('.') {
@ -38,7 +38,7 @@ async fn versions() -> ApiResult<Json<Value>> {
}
async fn hashes(Path(version): Path<String>) -> ApiResult<Json<IndexMap<String, Value>>> {
let map = index_assets(&version).await.map_err(|err| internal_and_log(err))?;
let map = index_assets(&version).await.map_err(internal_and_log)?;
Ok(Json(map))
}
@ -49,7 +49,7 @@ async fn download(Path((version, path)): Path<(String, String)>) -> ApiResult<Ve
return Err(ApiError::NotFound)
};
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await.map_err(|err| internal_and_log(err))?;
file.read_to_end(&mut buffer).await.map_err(internal_and_log)?;
Ok(buffer)
}
@ -65,13 +65,11 @@ async fn index_assets(version: &str) -> anyhow::Result<IndexMap<String, Value>>
Err(_) => continue
};
let path: String;
if cfg!(windows) {
path = entry.path().strip_prefix(version_path.clone())?.to_string_lossy().to_string().replace("\\", "/");
let path: String = if cfg!(windows) {
entry.path().strip_prefix(version_path.clone())?.to_string_lossy().to_string().replace("\\", "/")
} else {
path = entry.path().strip_prefix(version_path.clone())?.to_string_lossy().to_string();
}
entry.path().strip_prefix(version_path.clone())?.to_string_lossy().to_string()
};
map.insert(path, Value::from(hex::encode(digest(&SHA256, &data).as_ref())));
}

View file

@ -5,4 +5,4 @@ pub mod profile;
pub mod info;
pub mod assets;
pub use websocket::handler as ws;
pub use websocket::{initial as ws, SessionMessage};

View file

@ -14,7 +14,7 @@ use crate::{
auth::Token, utils::{calculate_file_sha256, format_uuid},
ApiError, ApiResult, AppState, AVATARS_VAR
};
use super::types::S2CMessage;
use super::websocket::S2CMessage;
pub async fn user_info(
Path(uuid): Path<Uuid>,
@ -85,7 +85,7 @@ pub async fn download_avatar(Path(uuid): Path<Uuid>) -> ApiResult<Vec<u8>> {
return Err(ApiError::NotFound)
};
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await.map_err(|err| internal_and_log(err))?;
file.read_to_end(&mut buffer).await.map_err(internal_and_log)?;
Ok(buffer)
}
@ -103,15 +103,15 @@ pub async fn upload_avatar(
user_info.username
);
let avatar_file = format!("{}/{}.moon", *AVATARS_VAR, user_info.uuid);
let mut file = BufWriter::new(fs::File::create(&avatar_file).await.map_err(|err| internal_and_log(err))?);
io::copy(&mut request_data.as_ref(), &mut file).await.map_err(|err| internal_and_log(err))?;
let mut file = BufWriter::new(fs::File::create(&avatar_file).await.map_err(internal_and_log)?);
io::copy(&mut request_data.as_ref(), &mut file).await.map_err(internal_and_log)?;
}
Ok("ok".to_string())
}
pub async fn equip_avatar(Token(token): Token, State(state): State<AppState>) -> ApiResult<&'static str> {
debug!("[API] S2C : Equip");
let uuid = state.user_manager.get(&token).ok_or_else(|| ApiError::Unauthorized)?.uuid;
let uuid = state.user_manager.get(&token).ok_or(ApiError::Unauthorized)?.uuid;
send_event(&state, &uuid).await;
Ok("ok")
}
@ -124,7 +124,7 @@ pub async fn delete_avatar(Token(token): Token, State(state): State<AppState>) -
user_info.username
);
let avatar_file = format!("{}/{}.moon", *AVATARS_VAR, user_info.uuid);
fs::remove_file(avatar_file).await.map_err(|err| internal_and_log(err))?;
fs::remove_file(avatar_file).await.map_err(internal_and_log)?;
send_event(&state, &user_info.uuid).await;
}
Ok("ok".to_string())
@ -132,16 +132,16 @@ pub async fn delete_avatar(Token(token): Token, State(state): State<AppState>) -
pub async fn send_event(state: &AppState, uuid: &Uuid) {
// To user subscribers
if let Some(broadcast) = state.broadcasts.get(&uuid) {
if broadcast.send(S2CMessage::Event(*uuid).to_vec()).is_err() {
if let Some(broadcast) = state.subscribes.get(uuid) {
if broadcast.send(S2CMessage::Event(*uuid).into()).is_err() {
debug!("[WebSocket] Failed to send Event! There is no one to send. UUID: {uuid}")
};
} else {
debug!("[WebSocket] Failed to send Event! Can't find UUID: {uuid}")
};
// To user
if let Some(session) = state.session.get(&uuid) {
if session.send(S2CMessage::Event(*uuid).to_vec()).await.is_err() {
if let Some(session) = state.session.get(uuid) {
if session.send(super::SessionMessage::Ping(S2CMessage::Event(*uuid).into())).await.is_err() {
debug!("[WebSocket] Failed to send Event! WS doesn't connected? UUID: {uuid}")
};
} else {

View file

@ -1,8 +1 @@
mod c2s;
mod errors;
mod s2c;
pub mod auth;
pub use c2s::C2SMessage;
pub use errors::MessageLoadError;
pub use s2c::S2CMessage;
pub mod auth;

View file

@ -1,234 +0,0 @@
use std::sync::Arc;
use axum::{
extract::{
ws::{Message, WebSocket},
State, WebSocketUpgrade,
},
response::Response,
};
use dashmap::DashMap;
use tracing::{debug, error, info, trace, warn};
use tokio::sync::{
broadcast::{self, Receiver},
mpsc, Notify,
};
use uuid::Uuid;
use crate::AppState;
use super::types::{C2SMessage, S2CMessage};
pub async fn handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response {
ws.on_upgrade(|socket| handle_socket(socket, state))
}
#[derive(Debug, Clone)]
struct WSUser {
username: String,
uuid: Uuid,
}
trait ExtWSUser {
fn name(&self) -> String;
}
impl ExtWSUser for Option<WSUser> {
fn name(&self) -> String {
if let Some(user) = self {
format!(" ({})", user.username)
} else {
String::new()
}
}
}
async fn handle_socket(mut socket: WebSocket, state: AppState) {
debug!("[WebSocket] New unknown connection!");
let mut owner: Option<WSUser> = None; // Information about user
let cutoff: DashMap<Uuid, Arc<Notify>> = DashMap::new(); // Отключение подписки
let (mtx, mut mrx) = mpsc::channel(64); // multiple tx and single receive
let mut bctx: Option<broadcast::Sender<Vec<u8>>> = None; // broadcast tx send
loop {
tokio::select! {
// Main loop what receving messages from WebSocket
Some(msg) = socket.recv() => {
trace!("[WebSocket{}] Raw: {msg:?}", owner.name());
let mut msg = if let Ok(msg) = msg {
if let Message::Close(_) = msg {
info!("[WebSocket{}] Connection successfully closed!", owner.name());
break;
}
msg
} else {
debug!("[WebSocket{}] Receive error! Connection terminated!", owner.name());
break;
};
// Checking ban list
if let Some(ref user) = owner {
if state.user_manager.is_banned(&user.uuid) {
warn!("[WebSocket] Detected banned user with active WebSocket! Sending close with Banned code.");
let _ = socket.send(Message::Binary(S2CMessage::Toast(2, "You're banned!", None).to_vec())).await; // option слищком жирный Some("Reason: Lorum Ipsum interсно сколько влезет~~~ 0w0.")
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
debug!("{:?}", socket.send(Message::Close(Some(axum::extract::ws::CloseFrame { code: 4001, reason: "You're banned!".into() }))).await);
continue;
}
}
// Next is the code for processing msg
let msg_vec = msg.clone().into_data();
let msg_array = msg_vec.as_slice();
if msg_array.len() == 0 { tracing::debug!("[WebSocket{}] Deprecated len 0 msg", owner.name()); continue; };
let newmsg = match C2SMessage::try_from(msg_array) {
Ok(data) => data,
Err(e) => {
error!("[WebSocket{}] This message is not from Figura! {}", owner.name(), e.to_string());
debug!("[WebSocket{}] Broken data: {}", owner.name(), hex::encode(msg_vec));
continue;
// break;
},
};
debug!("[WebSocket{}] MSG: {:?}, HEX: {}", owner.name(), newmsg, hex::encode(newmsg.to_vec()));
match newmsg {
C2SMessage::Token(token) => {
trace!("[WebSocket{}] C2S : Token", owner.name());
let token = String::from_utf8(token.to_vec()).unwrap();
match state.user_manager.get(&token) { // The principle is simple: if there is no token in authenticated, then it's "dirty hacker" :D
Some(t) => {
//username = t.username.clone();
owner = Some(WSUser { username: t.username.clone(), uuid: t.uuid });
state.session.insert(t.uuid, mtx.clone());
msg = Message::Binary(S2CMessage::Auth.to_vec());
match state.broadcasts.get(&t.uuid) {
Some(tx) => {
bctx = Some(tx.to_owned());
},
None => {
let (tx, _rx) = broadcast::channel(64);
state.broadcasts.insert(t.uuid, tx.clone());
bctx = Some(tx.to_owned());
},
};
},
None => {
warn!("[WebSocket] Authentication error! Sending close with Re-auth code.");
debug!("[WebSocket] Tried to log in with {token}"); // Tried to log in with token: {token}
debug!("{:?}", socket.send(Message::Close(Some(axum::extract::ws::CloseFrame { code: 4000, reason: "Re-auth".into() }))).await);
continue;
},
};
},
C2SMessage::Ping(_, _, _) => {
trace!("[WebSocket{}] C2S : Ping", owner.name());
let data = into_s2c_ping(msg_vec, owner.clone().unwrap().uuid);
match bctx.clone().unwrap().send(data) {
Ok(_) => (),
Err(_) => debug!("[WebSocket{}] Failed to send Ping! Maybe there's no one to send", owner.name()),
};
continue;
},
// Subscribing
C2SMessage::Sub(uuid) => { // TODO: Eliminate the possibility of using SUB without authentication
trace!("[WebSocket{}] C2S : Sub", owner.name());
// Ignoring self Sub
if uuid == owner.clone().unwrap().uuid {
continue;
};
let rx = match state.broadcasts.get(&uuid) { // Get sender
Some(rx) => rx.to_owned().subscribe(), // Subscribe on sender to get receiver
None => {
warn!("[WebSocket{}] Attention! The required UUID for subscription was not found!", owner.name());
let (tx, rx) = broadcast::channel(64); // Pre creating broadcast for future
state.broadcasts.insert(uuid, tx); // Inserting into dashmap
rx
},
};
let shutdown = Arc::new(Notify::new()); // Creating new shutdown <Notify>
tokio::spawn(subscribe(mtx.clone(), rx, shutdown.clone())); // <For send pings to >
cutoff.insert(uuid, shutdown);
continue;
},
// Unsubscribing
C2SMessage::Unsub(uuid) => {
trace!("[WebSocket{}] C2S : Unsub", owner.name());
// Ignoring self Unsub
if uuid == owner.clone().unwrap().uuid {
continue;
};
let shutdown = cutoff.remove(&uuid).unwrap().1; // Getting <Notify> from list // FIXME: UNWRAP PANIC! NONE VALUE
shutdown.notify_one(); // Shutdown <subscribe> function
continue;
},
}
// Sending message
debug!("[WebSocket{}] Answering: {msg:?}", owner.name());
if socket.send(msg).await.is_err() {
warn!("[WebSocket{}] Send error! Connection terminated!", owner.name());
break;
}
}
msg = mrx.recv() => {
match socket.send(Message::Binary(msg.clone().unwrap())).await {
Ok(_) => {
debug!("[WebSocketSubscribe{}] Answering: {}", owner.name(), hex::encode(msg.unwrap()));
}
Err(_) => {
warn!("[WebSocketSubscriber{}] Send error! Connection terminated!", owner.name());
break;
}
}
}
}
}
// Closing connection
if let Some(u) = owner {
debug!("[WebSocket ({})] Removing session data", u.username);
state.session.remove(&u.uuid); // FIXME: Temporary solution
// state.broadcasts.remove(&u.uuid); // NOTE: Create broadcasts manager ??
state.user_manager.remove(&u.uuid);
} else {
debug!("[WebSocket] Nothing to remove");
}
}
async fn subscribe(
socket: mpsc::Sender<Vec<u8>>,
mut rx: Receiver<Vec<u8>>,
shutdown: Arc<Notify>,
) {
loop {
tokio::select! {
_ = shutdown.notified() => {
debug!("SUB successfully closed!");
return;
}
msg = rx.recv() => {
let msg = msg.ok();
if let Some(msg) = msg {
if socket.send(msg.clone()).await.is_err() {
debug!("Forced shutdown SUB! Client died?");
return;
};
} else {
debug!("Forced shutdown SUB! Source died?");
return;
}
}
}
}
}
fn into_s2c_ping(buf: Vec<u8>, uuid: Uuid) -> Vec<u8> {
use std::iter::once;
once(1)
.chain(uuid.into_bytes().iter().copied())
.chain(buf.as_slice()[1..].iter().copied())
.collect()
}

View file

@ -0,0 +1,212 @@
use anyhow::bail;
use axum::extract::{ws::{Message, WebSocket}, State};
use dashmap::DashMap;
use tokio::sync::{broadcast, mpsc};
use crate::{auth::Userinfo, AppState};
use super::{processor::*, AuthModeError, S2CMessage, C2SMessage, WSSession, SessionMessage, RADError};
pub async fn initial(
ws: axum::extract::WebSocketUpgrade,
State(state): State<AppState>
) -> axum::response::Response {
ws.on_upgrade(|socket| handle_socket(socket, state))
}
async fn handle_socket(mut ws: WebSocket, state: AppState) {
// Trying authenticate & get user data or dropping connection
match authenticate(&mut ws, &state).await {
Ok(user) => {
// Creating session & creating/getting channels
let mut session = {
let sub_workers_aborthandles = DashMap::new();
// Channel for receiving messages from internal functions.
let (own_tx, own_rx) = mpsc::channel(32);
state.session.insert(user.uuid, own_tx.clone());
// Channel for sending messages to subscribers
let subs_tx = match state.subscribes.get(&user.uuid) {
Some(tx) => tx.clone(),
None => {
tracing::debug!("[Subscribes] Can't find own subs channel for {}, creating new...", user.uuid);
let (subs_tx, _) = broadcast::channel(32);
state.subscribes.insert(user.uuid, subs_tx.clone());
subs_tx
},
};
WSSession { user: user.clone(), own_tx, own_rx, subs_tx, sub_workers_aborthandles }
};
// Starting main worker
match main_worker(&mut session, &mut ws, &state).await {
Ok(_) => (),
Err(kind) => tracing::error!("[WebSocket] Main worker halted due to {}.", kind),
}
for (_, handle) in session.sub_workers_aborthandles {
handle.abort();
}
// Removing session data
state.session.remove(&user.uuid);
state.user_manager.remove(&user.uuid);
},
Err(kind) => {
tracing::info!("[WebSocket] Can't authenticate: {}", kind);
}
}
// Closing connection
if let Err(kind) = ws.close().await { tracing::trace!("[WebSocket] Closing fault: {}", kind) }
}
async fn main_worker(session: &mut WSSession, ws: &mut WebSocket, state: &AppState) -> anyhow::Result<()> {
tracing::debug!("WebSocket control for {} is transferred to the main worker", session.user.username);
loop {
tokio::select! {
external_msg = ws.recv_and_decode() => {
// Getting a value or halt the worker without an error
let external_msg = match external_msg {
Ok(m) => m,
Err(kind) => {
match kind {
RADError::Close(_) => return Ok(()),
RADError::StreamClosed => return Ok(()),
_ => return Err(kind.into())
}
},
};
// Processing message
match external_msg {
C2SMessage::Token(_) => bail!("authentication passed, but the client sent the Token again"),
C2SMessage::Ping(func_id, echo, data) => {
let s2c_ping: Vec<u8> = S2CMessage::Ping(session.user.uuid, func_id, echo, data).into();
// Echo check
if echo {
ws.send(Message::Binary(s2c_ping.clone())).await?
}
// Sending to others
let _ = session.subs_tx.send(s2c_ping);
},
C2SMessage::Sub(uuid) => {
tracing::debug!("[WebSocket] {} subscribes to {}", session.user.username, uuid);
// Doesn't allow to subscribe to yourself
if session.user.uuid != uuid {
// Creates a channel to send pings to a subscriber if it can't find an existing one
let rx = match state.subscribes.get(&uuid) {
Some(tx) => tx.subscribe(),
None => {
let (tx, rx) = broadcast::channel(32);
state.subscribes.insert(uuid, tx);
rx
},
};
let handle = tokio::spawn(sub_worker(session.own_tx.clone(), rx)).abort_handle();
session.sub_workers_aborthandles.insert(uuid, handle);
}
},
C2SMessage::Unsub(uuid) => {
tracing::debug!("[WebSocket] {} unsubscribes from {}", session.user.username, uuid);
match session.sub_workers_aborthandles.get(&uuid) {
Some(handle) => handle.abort(),
None => tracing::warn!("[WebSocket] {} was not subscribed.", session.user.username),
};
},
}
},
internal_msg = session.own_rx.recv() => {
let internal_msg = internal_msg.ok_or(anyhow::anyhow!("Unexpected error! Session channel broken!"))?;
match internal_msg {
SessionMessage::Ping(msg) => {
ws.send(Message::Binary(msg)).await?
},
SessionMessage::Banned => {
let _ = ban_action(ws).await
.inspect_err(
|kind| tracing::warn!("[WebSocket] Didn't get the ban message due to {}", kind)
);
bail!("{} banned!", session.user.username)
},
}
}
}
}
}
async fn sub_worker(tx_main: mpsc::Sender<SessionMessage>, mut rx: broadcast::Receiver<Vec<u8>>) {
loop {
let msg = match rx.recv().await {
Ok(m) => m,
Err(kind) => {
tracing::error!("[Subscribes_Worker] Broadcast error! {}", kind);
return;
},
};
match tx_main.send(SessionMessage::Ping(msg)).await {
Ok(_) => (),
Err(kind) => {
tracing::error!("[Subscribes_Worker] Session error! {}", kind);
return;
},
}
}
}
async fn authenticate(socket: &mut WebSocket, state: &AppState) -> Result<Userinfo, AuthModeError> {
match socket.recv_and_decode().await {
Ok(msg) => {
match msg {
C2SMessage::Token(token) => {
let token = String::from_utf8(token.to_vec()).map_err(|_| AuthModeError::ConvertError)?;
match state.user_manager.get(&token) {
Some(user) => {
if socket.send(Message::Binary(S2CMessage::Auth.into())).await.is_err() {
Err(AuthModeError::SendError)
} else if !user.banned {
Ok(user.clone())
} else {
let _ = ban_action(socket).await
.inspect_err(
|kind| tracing::warn!("[WebSocket] Didn't get the ban message due to {}", kind)
);
Err(AuthModeError::Banned(user.username.clone()))
}
},
None => {
if socket.send(
Message::Close(Some(axum::extract::ws::CloseFrame { code: 4000, reason: "Re-auth".into() }))
).await.is_err() {
Err(AuthModeError::SendError)
} else {
Err(AuthModeError::AuthenticationFailure)
}
},
}
},
_ => {
Err(AuthModeError::UnauthorizedAction)
}
}
},
Err(err) => {
Err(AuthModeError::RecvError(err))
},
}
}
async fn ban_action(ws: &mut WebSocket) -> anyhow::Result<()> {
ws.send(Message::Binary(S2CMessage::Toast(2, "You're banned!".to_string(), None).into())).await?;
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
ws.send(Message::Close(Some(axum::extract::ws::CloseFrame { code: 4001, reason: "You're banned!".into() }))).await?;
Ok(())
}

View file

@ -0,0 +1,8 @@
// mod websocket;
mod handler;
mod processor;
mod types;
// pub use websocket::*;
pub use handler::initial;
pub use types::*;

View file

@ -0,0 +1,32 @@
use axum::extract::ws::{Message, WebSocket};
use super::{C2SMessage, RADError};
pub trait RecvAndDecode {
async fn recv_and_decode(&mut self) -> Result<C2SMessage, RADError>;
}
impl RecvAndDecode for WebSocket {
async fn recv_and_decode(&mut self) -> Result<C2SMessage, RADError> {
if let Some(msg) = self.recv().await {
match msg {
Ok(msg) => {
match msg {
Message::Close(frame) => Err(RADError::Close(frame.map(|f| format!("code: {}, reason: {}", f.code, f.reason)))),
_ => {
match C2SMessage::try_from(msg.clone().into_data().as_slice()) {
Ok(decoded) => Ok(decoded),
Err(e) => {
Err(RADError::DecodeError(e, hex::encode(msg.into_data())))
},
}
}
}
},
Err(e) => Err(RADError::WebSocketError(e)),
}
} else {
Err(RADError::StreamClosed)
}
}
}

View file

@ -5,27 +5,27 @@ use std::convert::{TryFrom, TryInto};
#[repr(u8)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum C2SMessage<'a> {
Token(&'a [u8]) = 0,
Ping(u32, bool, &'a [u8]) = 1,
pub enum C2SMessage {
Token(Vec<u8>) = 0,
Ping(u32, bool, Vec<u8>) = 1,
Sub(Uuid) = 2, // owo
Unsub(Uuid) = 3,
}
// 6 - 6
impl<'a> TryFrom<&'a [u8]> for C2SMessage<'a> {
impl TryFrom<&[u8]> for C2SMessage {
type Error = MessageLoadError;
fn try_from(buf: &'a [u8]) -> Result<Self, <Self as TryFrom<&'a [u8]>>::Error> {
fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
if buf.is_empty() {
Err(MessageLoadError::BadLength("C2SMessage", 1, false, 0))
} else {
match buf[0] {
0 => Ok(C2SMessage::Token(&buf[1..])),
0 => Ok(C2SMessage::Token(buf[1..].to_vec())),
1 => {
if buf.len() >= 6 {
Ok(C2SMessage::Ping(
u32::from_be_bytes((&buf[1..5]).try_into().unwrap()),
buf[5] != 0,
&buf[6..],
buf[6..].to_vec(),
))
} else {
Err(MessageLoadError::BadLength(
@ -73,10 +73,10 @@ impl<'a> TryFrom<&'a [u8]> for C2SMessage<'a> {
}
}
}
impl<'a> From<C2SMessage<'a>> for Box<[u8]> {
fn from(val: C2SMessage<'a>) -> Self {
impl From<C2SMessage> for Vec<u8> {
fn from(val: C2SMessage) -> Self {
use std::iter;
let a: Box<[u8]> = match val {
let a: Vec<u8> = match val {
C2SMessage::Token(t) => iter::once(0).chain(t.iter().copied()).collect(),
C2SMessage::Ping(p, s, d) => iter::once(1)
.chain(p.to_be_bytes())
@ -90,11 +90,11 @@ impl<'a> From<C2SMessage<'a>> for Box<[u8]> {
}
}
impl<'a> C2SMessage<'a> {
pub fn to_array(&self) -> Box<[u8]> {
<C2SMessage as Into<Box<[u8]>>>::into(self.clone())
}
pub fn to_vec(&self) -> Vec<u8> {
self.to_array().to_vec()
}
}
// impl<'a> C2SMessage<'a> {
// pub fn to_array(&self) -> Box<[u8]> {
// <C2SMessage as Into<Box<[u8]>>>::into(self.clone())
// }
// pub fn to_vec(&self) -> Vec<u8> {
// self.to_array().to_vec()
// }
// }

View file

@ -1,6 +1,8 @@
use std::fmt::*;
use std::ops::RangeInclusive;
use thiserror::Error;
#[derive(Debug)]
pub enum MessageLoadError {
BadEnum(&'static str, RangeInclusive<usize>, usize),
@ -23,6 +25,35 @@ impl Display for MessageLoadError {
}
}
}
#[derive(Error, Debug)]
pub enum RADError {
#[error("message decode error due: {0}, invalid data: {1}")]
DecodeError(MessageLoadError, String),
#[error("close, frame: {0:?}")]
Close(Option<String>),
#[error(transparent)]
WebSocketError(#[from] axum::Error),
#[error("stream closed")]
StreamClosed,
}
#[derive(Error, Debug)]
pub enum AuthModeError {
#[error("token recieve error due {0}")]
RecvError(RADError),
#[error("action attempt without authentication")]
UnauthorizedAction,
#[error("convert error, bytes into string")]
ConvertError,
#[error("can't send, websocket broken")]
SendError,
#[error("authentication failure, sending re-auth...")]
AuthenticationFailure,
#[error("{0} banned")]
Banned(String),
}
#[cfg(test)]
#[test]
fn message_load_error_display() {

View file

@ -0,0 +1,9 @@
mod c2s;
mod s2c;
mod errors;
mod session;
pub use session::*;
pub use errors::*;
pub use c2s::*;
pub use s2c::*;

View file

@ -5,17 +5,19 @@ use uuid::Uuid;
#[repr(u8)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum S2CMessage<'a> {
pub enum S2CMessage {
Auth = 0,
Ping(Uuid, u32, bool, &'a [u8]) = 1,
Ping(Uuid, u32, bool, Vec<u8>) = 1,
Event(Uuid) = 2, // Updates avatar for other players
Toast(u8, &'a str, Option<&'a str>) = 3,
Chat(&'a str) = 4,
Toast(u8, String, Option<String>) = 3,
Chat(String) = 4,
Notice(u8) = 5,
}
impl<'a> TryFrom<&'a [u8]> for S2CMessage<'a> {
impl TryFrom<&[u8]> for S2CMessage {
type Error = MessageLoadError;
fn try_from(buf: &'a [u8]) -> Result<Self, <Self as TryFrom<&'a [u8]>>::Error> {
fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
if buf.is_empty() {
Err(MessageLoadError::BadLength("S2CMessage", 1, false, 0))
} else {
@ -35,7 +37,7 @@ impl<'a> TryFrom<&'a [u8]> for S2CMessage<'a> {
Uuid::from_bytes((&buf[1..17]).try_into().unwrap()),
u32::from_be_bytes((&buf[17..21]).try_into().unwrap()),
buf[21] != 0,
&buf[22..],
buf[22..].to_vec(),
))
} else {
Err(BadLength("S2CMessage::Ping", 22, false, buf.len()))
@ -56,12 +58,13 @@ impl<'a> TryFrom<&'a [u8]> for S2CMessage<'a> {
}
}
}
impl<'a> From<S2CMessage<'a>> for Box<[u8]> {
fn from(val: S2CMessage<'a>) -> Self {
impl From<S2CMessage> for Vec<u8> {
fn from(val: S2CMessage) -> Self {
use std::iter::once;
use S2CMessage::*;
match val {
Auth => Box::new([0]),
Auth => vec![0],
Ping(u, i, s, d) => once(1)
.chain(u.into_bytes().iter().copied())
.chain(i.to_be_bytes().iter().copied())
@ -74,20 +77,20 @@ impl<'a> From<S2CMessage<'a>> for Box<[u8]> {
.chain(h.as_bytes().iter().copied())
.chain(
d.into_iter()
.flat_map(|s| once(0).chain(s.as_bytes().iter().copied())),
.flat_map(|s| once(0).chain(s.as_bytes().iter().copied()).collect::<Vec<_>>()), // FIXME: Try find other solution
)
.collect(),
Chat(c) => once(4).chain(c.as_bytes().iter().copied()).collect(),
Notice(t) => Box::new([5, t]),
Notice(t) => vec![5, t],
}
}
}
impl<'a> S2CMessage<'a> {
pub fn to_array(&self) -> Box<[u8]> {
<S2CMessage as Into<Box<[u8]>>>::into(self.clone())
}
pub fn to_vec(&self) -> Vec<u8> {
self.to_array().to_vec()
}
}
// impl<'a> S2CMessage<'a> {
// pub fn to_array(&self) -> Box<[u8]> {
// <S2CMessage as Into<Box<[u8]>>>::into(self.clone())
// }
// pub fn to_vec(&self) -> Vec<u8> {
// self.to_array().to_vec()
// }
// }

View file

@ -0,0 +1,15 @@
use dashmap::DashMap;
use tokio::{sync::{broadcast, mpsc}, task::AbortHandle};
pub struct WSSession {
pub user: crate::auth::Userinfo,
pub own_tx: mpsc::Sender<SessionMessage>,
pub own_rx: mpsc::Receiver<SessionMessage>,
pub subs_tx: broadcast::Sender<Vec<u8>>,
pub sub_workers_aborthandles: DashMap<uuid::Uuid, AbortHandle>,
}
pub enum SessionMessage {
Ping(Vec<u8>),
Banned,
}

View file

@ -28,7 +28,7 @@ pub(super) async fn raw(
Some(uuid) => {
// for only one
let tx = state.session.get(&uuid).ok_or_else(|| { warn!("unknown uuid"); crate::ApiError::NotFound })?;
tx.value().send(payload).await.map_err(|err| internal_and_log(err))?;
tx.value().send(crate::api::figura::SessionMessage::Ping(payload)).await.map_err(internal_and_log)?;
Ok("ok")
},
None => {
@ -53,8 +53,8 @@ pub(super) async fn sub_raw(
match query.uuid {
Some(uuid) => {
// for only one
let tx = state.broadcasts.get(&uuid).ok_or_else(|| { warn!("unknown uuid"); crate::ApiError::NotFound })?;
tx.value().send(payload).map_err(|err| internal_and_log(err))?;
let tx = state.subscribes.get(&uuid).ok_or_else(|| { warn!("unknown uuid"); crate::ApiError::NotFound })?;
tx.value().send(payload).map_err(internal_and_log)?;
Ok("ok")
},
None => {

View file

@ -29,7 +29,8 @@ pub(super) async fn ban(
info!("Trying ban user: {uuid}");
state.user_manager.ban(&Userinfo { uuid: uuid, banned: true, ..Default::default() });
if let Some(tx) = state.session.get(&uuid) {let _ = tx.send(crate::api::figura::SessionMessage::Banned).await;}
state.user_manager.ban(&Userinfo { uuid, banned: true, ..Default::default() });
Ok("ok")
}

View file

@ -83,8 +83,8 @@ async fn fetch_json(
trace!("{res:?}");
match res.status().as_u16() {
200 => {
let json = serde_json::from_str::<serde_json::Value>(&res.text().await?).with_context(|| format!("Cant deserialize"))?;
let uuid = get_id_json(&json).with_context(|| format!("Cant get UUID"))?;
let json = serde_json::from_str::<serde_json::Value>(&res.text().await?).with_context(|| "Cant deserialize".to_string())?;
let uuid = get_id_json(&json).with_context(|| "Cant get UUID".to_string())?;
Ok((uuid, auth_provider.clone()))
}
_ => Err(FetchError::WrongResponse(res.status().as_u16(), res.text().await)),
@ -131,7 +131,7 @@ pub async fn has_joined(
// Choosing what error return
// Returns if some internals errors occured
if errors.len() != 0 {
if !errors.is_empty() {
error!("Something wrong with your authentification providers!\nMisses: {misses:?}\nErrors: {errors:?}");
Err(anyhow::anyhow!("{:?}", errors))
@ -203,7 +203,7 @@ impl UManager {
pub fn insert_user(&self, uuid: Uuid, userinfo: Userinfo) {
// self.registered.insert(uuid, userinfo)
let usercopy = userinfo.clone();
self.registered.entry(uuid.clone())
self.registered.entry(uuid)
.and_modify(|exist| {
if !userinfo.username.is_empty() { exist.username = userinfo.username };
if !userinfo.auth_provider.is_empty() { exist.auth_provider = userinfo.auth_provider };

View file

@ -51,11 +51,7 @@ impl Default for AuthProvider {
impl AuthProvider {
pub fn is_empty(&self) -> bool {
if self.name == "Unknown".to_string() {
true
} else {
false
}
self.name == "Unknown"
}
}

View file

@ -1,22 +1,22 @@
// Environment
pub const LOGGER_ENV: &'static str = "RUST_LOG";
pub const CONFIG_ENV: &'static str = "RUST_CONFIG";
pub const LOGS_ENV: &'static str = "LOGS_FOLDER";
pub const ASSETS_ENV: &'static str = "ASSETS_FOLDER";
pub const AVATARS_ENV: &'static str = "AVATARS_FOLDER";
pub const LOGGER_ENV: &str = "RUST_LOG";
pub const CONFIG_ENV: &str = "RUST_CONFIG";
pub const LOGS_ENV: &str = "LOGS_FOLDER";
pub const ASSETS_ENV: &str = "ASSETS_FOLDER";
pub const AVATARS_ENV: &str = "AVATARS_FOLDER";
// Instance info
pub const SCULPTOR_VERSION: &'static str = env!("CARGO_PKG_VERSION");
pub const REPOSITORY: &'static str = "shiroyashik/sculptor";
pub const SCULPTOR_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const REPOSITORY: &str = "shiroyashik/sculptor";
// reqwest parameters
pub const USER_AGENT: &'static str = "reqwest";
pub const USER_AGENT: &str = "reqwest";
pub const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
// Figura update checker
pub const FIGURA_RELEASES_URL: &'static str = "https://api.github.com/repos/figuramc/figura/releases";
pub const FIGURA_DEFAULT_VERSION: &'static str = "0.1.4";
pub const FIGURA_RELEASES_URL: &str = "https://api.github.com/repos/figuramc/figura/releases";
pub const FIGURA_DEFAULT_VERSION: &str = "0.1.4";
// Figura Assets
pub const FIGURA_ASSETS_ZIP_URL: &'static str = "https://github.com/FiguraMC/Assets/archive/refs/heads/main.zip";
pub const FIGURA_ASSETS_COMMIT_URL: &'static str = "https://api.github.com/repos/FiguraMC/Assets/commits/main";
pub const FIGURA_ASSETS_ZIP_URL: &str = "https://github.com/FiguraMC/Assets/archive/refs/heads/main.zip";
pub const FIGURA_ASSETS_COMMIT_URL: &str = "https://api.github.com/repos/FiguraMC/Assets/commits/main";

View file

@ -1,3 +1,4 @@
#![allow(clippy::module_inception)]
use anyhow::Result;
use axum::{
extract::DefaultBodyLimit, routing::{delete, get, post, put}, Router
@ -6,9 +7,8 @@ use dashmap::DashMap;
use tracing_panic::panic_hook;
use tracing_subscriber::{fmt::{self, time::ChronoLocal}, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use std::{path::PathBuf, sync::Arc, env::var};
use tokio::{fs, sync::{broadcast, mpsc, RwLock}, time::Instant};
use tokio::{fs, sync::RwLock, time::Instant};
use tower_http::trace::TraceLayer;
use uuid::Uuid;
use lazy_static::lazy_static;
// Consts
@ -31,28 +31,12 @@ use auth::{UManager, check_auth};
// Config
mod state;
use state::Config;
use state::{Config, AppState};
// Utils
mod utils;
use utils::*;
#[derive(Debug, Clone)]
pub struct AppState {
/// Uptime
uptime: Instant,
/// User manager
user_manager: Arc<UManager>,
/// Send into WebSocket
session: Arc<DashMap<Uuid, mpsc::Sender<Vec<u8>>>>,
/// Ping broadcasts for WebSocket connections
broadcasts: Arc<DashMap<Uuid, broadcast::Sender<Vec<u8>>>>,
/// Current configuration
config: Arc<RwLock<state::Config>>,
/// Caching Figura Versions
figura_versions: Arc<RwLock<Option<FiguraVersions>>>,
}
lazy_static! {
pub static ref LOGGER_VAR: String = {
var(LOGGER_ENV).unwrap_or(String::from("info"))
@ -77,7 +61,7 @@ async fn main() -> Result<()> {
let _ = dotenvy::dotenv();
// 2. Set up logging
let file_appender = tracing_appender::rolling::never(&*LOGS_VAR, get_log_file(&*LOGS_VAR));
let file_appender = tracing_appender::rolling::never(&*LOGS_VAR, get_log_file(&LOGS_VAR));
let timer = ChronoLocal::new(String::from("%Y-%m-%dT%H:%M:%S%.3f%:z"));
let file_layer = fmt::layer()
@ -147,7 +131,7 @@ async fn app() -> Result<bool> {
// Config
let config = Arc::new(RwLock::new(Config::parse(CONFIG_VAR.clone().into())));
let listen = config.read().await.listen.clone();
let limit = get_limit_as_bytes(config.read().await.limitations.max_avatar_size.clone() as usize);
let limit = get_limit_as_bytes(config.read().await.limitations.max_avatar_size as usize);
if config.read().await.assets_updater_enabled {
// Force update assets if folder or hash file doesn't exists.
@ -179,15 +163,17 @@ async fn app() -> Result<bool> {
uptime: Instant::now(),
user_manager: Arc::new(UManager::new()),
session: Arc::new(DashMap::new()),
broadcasts: Arc::new(DashMap::new()),
subscribes: Arc::new(DashMap::new()),
figura_versions: Arc::new(RwLock::new(None)),
config,
};
// FIXME: FIXME: FIXME: ПЕРЕДЕЛАЙ ЭТО! НЕМЕДЛЕННО! ЕБУЧИЙ ПОЗОР :<
// Automatic update of configuration while the server is running
let config_update = Arc::clone(&state.config);
let user_manager = Arc::clone(&state.user_manager);
update_advanced_users(&config_update.read().await.advanced_users.clone(), &user_manager);
let umanager = Arc::clone(&state.user_manager);
let session = Arc::clone(&state.session);
update_advanced_users(&config_update.read().await.advanced_users.clone(), &umanager, &session).await;
tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
@ -197,14 +183,15 @@ async fn app() -> Result<bool> {
if new_config != *config {
tracing::info!("Server configuration modification detected!");
*config = new_config;
update_advanced_users(&config.advanced_users.clone(), &user_manager);
update_advanced_users(&config.advanced_users.clone(), &umanager, &session).await;
}
}
});
if state.config.read().await.mc_folder.exists() {
tokio::spawn(update_bans_from_minecraft(
state.config.read().await.mc_folder.clone(),
Arc::clone(&state.user_manager)
Arc::clone(&state.user_manager),
Arc::clone(&state.session)
));
}

View file

@ -62,11 +62,11 @@ pub struct BannedPlayer {
pub name: String,
}
impl Into<Userinfo> for BannedPlayer {
fn into(self) -> Userinfo {
impl From<BannedPlayer> for Userinfo {
fn from(val: BannedPlayer) -> Self {
Userinfo {
uuid: self.uuid,
username: self.name,
uuid: val.uuid,
username: val.name,
banned: true,
..Default::default()
}

View file

@ -1,4 +1,5 @@
mod config;
mod state;
pub use config::*;
pub use config::*;
pub use state::*;

View file

@ -0,0 +1,23 @@
use std::sync::Arc;
use dashmap::DashMap;
use tokio::{sync::*, time::Instant};
use uuid::Uuid;
use crate::{api::figura::SessionMessage, auth::UManager, FiguraVersions};
#[derive(Debug, Clone)]
pub struct AppState {
/// Uptime
pub uptime: Instant,
/// User manager
pub user_manager: Arc<UManager>,
/// Send into WebSocket
pub session: Arc<DashMap<Uuid, mpsc::Sender<SessionMessage>>>,
/// Send messages for subscribers
pub subscribes: Arc<DashMap<Uuid, broadcast::Sender<Vec<u8>>>>,
/// Current configuration
pub config: Arc<RwLock<super::Config>>,
/// Caching Figura Versions
pub figura_versions: Arc<RwLock<Option<FiguraVersions>>>,
}

View file

@ -30,14 +30,18 @@ pub fn _generate_hex_string(length: usize) -> String {
hex::encode(random_bytes)
}
pub fn update_advanced_users(value: &std::collections::HashMap<Uuid, AdvancedUsers>, umanager: &UManager) {
pub async fn update_advanced_users(
value: &std::collections::HashMap<Uuid, AdvancedUsers>,
umanager: &UManager,
sessions: &dashmap::DashMap<Uuid, tokio::sync::mpsc::Sender<crate::api::figura::SessionMessage>>
) {
let users: Vec<(Uuid, Userinfo)> = value
.iter()
.map( |(uuid, userdata)| {
(
uuid.clone(),
*uuid,
Userinfo {
uuid: uuid.clone(),
uuid: *uuid,
username: userdata.username.clone(),
banned: userdata.banned,
..Default::default()
@ -48,12 +52,17 @@ pub fn update_advanced_users(value: &std::collections::HashMap<Uuid, AdvancedUse
for (uuid, userinfo) in users {
umanager.insert_user(uuid, userinfo.clone());
if userinfo.banned {
umanager.ban(&userinfo)
umanager.ban(&userinfo);
if let Some(tx) = sessions.get(&uuid) {let _ = tx.send(crate::api::figura::SessionMessage::Banned).await;}
}
}
}
pub async fn update_bans_from_minecraft(folder: PathBuf, umanager: std::sync::Arc<UManager>) {
pub async fn update_bans_from_minecraft(
folder: PathBuf,
umanager: std::sync::Arc<UManager>,
sessions: std::sync::Arc<dashmap::DashMap<Uuid, tokio::sync::mpsc::Sender<crate::api::figura::SessionMessage>>>
) {
let path = folder.join("banned-players.json");
let mut file = tokio::fs::File::open(path.clone()).await.expect("Access denied or banned-players.json doesn't exists!");
let mut data = String::new();
@ -70,6 +79,7 @@ pub async fn update_bans_from_minecraft(folder: PathBuf, umanager: std::sync::Ar
for player in &old_bans {
umanager.ban(&player.clone().into());
if let Some(tx) = sessions.get(&player.uuid) {let _ = tx.send(crate::api::figura::SessionMessage::Banned).await;}
}
// old_bans
@ -97,6 +107,7 @@ pub async fn update_bans_from_minecraft(folder: PathBuf, umanager: std::sync::Ar
if !ban.is_empty() {
for player in ban {
umanager.ban(&player.clone().into());
if let Some(tx) = sessions.get(&player.uuid) {let _ = tx.send(crate::api::figura::SessionMessage::Banned).await;}
}
} else { ban_names = String::from("-")};
info!("List of changes:\n Banned: {ban_names}\n Unbanned: {unban_names}");

View file

@ -65,10 +65,8 @@ pub async fn get_figura_versions() -> anyhow::Result<FiguraVersions> {
if tag_ver > prerelease_ver {
prerelease_ver = tag_ver
}
} else {
if tag_ver > release_ver {
} else if tag_ver > release_ver {
release_ver = tag_ver
}
}
}
if release_ver > prerelease_ver {
@ -115,13 +113,11 @@ pub async fn is_assets_outdated(last_sha: &str) -> anyhow::Result<bool> {
if contents.lines().count() != 1 {
// Lines count in file abnormal
Ok(true)
} else if contents == last_sha {
Ok(false)
} else {
if contents == last_sha {
Ok(false)
} else {
// SHA in file mismatches with provided SHA
Ok(true)
}
// SHA in file mismatches with provided SHA
Ok(true)
}
},
Err(err) => if err.kind() == tokio::io::ErrorKind::NotFound {

View file

@ -1,7 +1,7 @@
mod utils;
mod auxiliary;
mod check_updates;
mod motd;
pub use utils::*;
pub use auxiliary::*;
pub use motd::*;
pub use check_updates::*;