mirror of
https://github.com/shiroyashik/sculptor.git
synced 2025-12-06 13:01:12 +03:00
🎉WebSocket refactored!
This commit is contained in:
parent
7a4f3dc7a5
commit
4c0871e26c
30 changed files with 650 additions and 587 deletions
|
|
@ -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())));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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()
|
||||
}
|
||||
212
src/api/figura/websocket/handler.rs
Normal file
212
src/api/figura/websocket/handler.rs
Normal 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(())
|
||||
}
|
||||
8
src/api/figura/websocket/mod.rs
Normal file
8
src/api/figura/websocket/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// mod websocket;
|
||||
mod handler;
|
||||
mod processor;
|
||||
mod types;
|
||||
|
||||
// pub use websocket::*;
|
||||
pub use handler::initial;
|
||||
pub use types::*;
|
||||
32
src/api/figura/websocket/processor.rs
Normal file
32
src/api/figura/websocket/processor.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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() {
|
||||
9
src/api/figura/websocket/types/mod.rs
Normal file
9
src/api/figura/websocket/types/mod.rs
Normal 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::*;
|
||||
|
|
@ -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()
|
||||
// }
|
||||
// }
|
||||
15
src/api/figura/websocket/types/session.rs
Normal file
15
src/api/figura/websocket/types/session.rs
Normal 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,
|
||||
}
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
39
src/main.rs
39
src/main.rs
|
|
@ -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)
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
mod config;
|
||||
mod state;
|
||||
|
||||
pub use config::*;
|
||||
pub use config::*;
|
||||
pub use state::*;
|
||||
|
|
@ -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>>>,
|
||||
}
|
||||
|
|
@ -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}");
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
Loading…
Add table
Add a link
Reference in a new issue