Code refactored + new API

Added ability to send messages to a websocket session via HTTP API
This commit is contained in:
Shiroyasha 2024-06-17 23:05:25 +03:00
parent ca6990c5b1
commit 81ab2ba724
Signed by: shiroyashik
GPG key ID: E4953D3940D7860A
11 changed files with 172 additions and 74 deletions

View file

@ -1,7 +1,11 @@
listen = "0.0.0.0:6665" # Don't touch this if you running under Docker container
## Don't touch this if you running under Docker container
listen = "0.0.0.0:6665"
# Message of The Day
# It will be displayed to every player in the Figura menu who is connected to your server
## Don't touch if you don't know what you're doing
# token = "<random symbols>"
## Message of The Day
## It will be displayed to every player in the Figura menu who is connected to your server
motd = """
[
{
@ -41,7 +45,7 @@ motd = """
"""
[limitations]
maxAvatarSize = 100000
maxAvatarSize = 100000 # 100 KB
maxAvatars = 10
[advancedUsers.66004548-4de5-49de-bade-9c3933d8eb97]
@ -50,11 +54,11 @@ authSystem = "elyby"
special = [0,0,0,1,0,0] # 6
pride = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] # 25
# With advancedUsers you can set additional parameters
## With advancedUsers you can set additional parameters
# [advancedUsers.your uuid here]
# username = "Your_username_here"
# authSystem = "mojang" # can be: mojang, elyby, internal (cant be authenticated)
# special = [0,1,0,0,0,0] # and set badges what you want! :D
# pride = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
# you can create an unlimited number of "advancedUsers" for any users.
## you can create an unlimited number of "advancedUsers" for any users.

View file

@ -7,11 +7,27 @@ use toml::Table;
#[serde(rename_all = "camelCase")]
pub struct Config {
pub listen: String,
pub token: Option<String>,
pub motd: String,
pub limitations: Limitations,
pub advanced_users: Table,
}
impl Config {
pub fn verify_token(&self, suspicious: &str) -> bool {
match &self.token {
Some(t) => {
if t == suspicious {
true
} else {
false
}
},
None => false
}
}
}
#[derive(Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Limitations {

View file

@ -4,7 +4,7 @@ use axum::{
};
use dashmap::DashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, Mutex};
use tokio::sync::{broadcast, mpsc, Mutex};
use tower_http::trace::TraceLayer;
use tracing::info;
use uuid::Uuid;
@ -35,12 +35,10 @@ use config::Config;
#[derive(Debug, Clone)]
pub struct AppState {
/// Users with incomplete authentication
//pending: Arc<DashMap<String, String>>, // <SHA1 serverId, USERNAME>
/// Authenticated users
//authenticated: Arc<Authenticated>, // <SHA1 serverId, Userinfo> NOTE: In the future, try it in a separate LockRw branch
/// 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
@ -73,6 +71,7 @@ async fn main() -> Result<()> {
// State
let state = AppState {
user_manager: Arc::new(UManager::new()),
session: Arc::new(DashMap::new()),
broadcasts: Arc::new(DashMap::new()),
config: config,
};
@ -97,6 +96,7 @@ async fn main() -> Result<()> {
let api = Router::new()
.nest("//auth", api_auth::router())
.nest("/v1", ws::http_router())
.route("/limits", get(api_info::limits))
.route("/version", get(api_info::version))
.route("/motd", get(api_info::motd))

View file

@ -136,7 +136,7 @@ pub async fn equip_avatar(Token(token): Token, State(state): State<AppState>) ->
.send(S2CMessage::Event(uuid).to_vec())
.is_err()
{
warn!("[WebSocket] Failed to send Event! Maybe there is no one to send")
debug!("[WebSocket] Failed to send Event! Maybe there is no one to send")
// TODO: Put into Handler
};
"ok".to_string()

83
src/ws/http.rs Normal file
View file

@ -0,0 +1,83 @@
use axum::{
extract::{Query, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
Router
};
use serde::Deserialize;
use tracing::debug;
use uuid::Uuid;
use crate::{auth::Token, AppState};
pub fn router() -> Router<AppState> {
Router::new()
.route("/verify", get(verify))
// .route("/ping", post(ping))
// .route("/event", post(event))
// .route("/toast", post(toast))
// .route("/chat", post(chat))
// .route("/notice", post(notice))
.route("/raw", post(raw))
}
#[derive(Deserialize)]
struct UserUuid {
uuid: Option<Uuid>,
}
async fn verify(
Token(token): Token,
State(state): State<AppState>,
) -> Response {
match token {
Some(t) => {
if !state.config.lock().await.verify_token(&t) {
return (StatusCode::UNAUTHORIZED, "wrong token".to_string()).into_response()
}
},
None => return (StatusCode::UNAUTHORIZED, "unauthorized".to_string()).into_response(),
}
(StatusCode::OK, "ok".to_string()).into_response()
}
async fn raw(
Token(token): Token,
Query(query): Query<UserUuid>,
State(state): State<AppState>,
body: String,
) -> Response {
debug!(body = body);
match token {
Some(t) => {
if !state.config.lock().await.verify_token(&t) {
return (StatusCode::UNAUTHORIZED, "wrong token".to_string()).into_response()
}
},
None => return (StatusCode::UNAUTHORIZED, "unauthorized".to_string()).into_response(),
}
let payload = match hex::decode(body) {
Ok(v) => v,
Err(_) => return (StatusCode::NOT_ACCEPTABLE, "not raw data".to_string()).into_response(),
};
debug!("{:?}", payload);
match query.uuid {
Some(uuid) => {
// for only one
let tx = match state.session.get(&uuid) {
Some(d) => d,
None => return (StatusCode::NOT_FOUND, "unknown uuid".to_string()).into_response(),
};
match tx.value().send(payload).await {
Ok(_) => return (StatusCode::OK, "ok".to_string()).into_response(),
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "cant send".to_string()).into_response(),
};
},
None => {
// for all
return (StatusCode::NOT_FOUND, "uuid doesnt defined".to_string()).into_response();
},
}
}

View file

@ -1,9 +1,8 @@
mod c2s;
mod errors;
mod handler;
mod s2c;
mod types;
mod websocket;
mod http;
pub use c2s::C2SMessage;
pub use errors::MessageLoadError;
pub use handler::handler;
pub use s2c::S2CMessage;
pub use types::C2SMessage;
pub use types::S2CMessage;
pub use websocket::handler;
pub use http::router as http_router;

7
src/ws/types/mod.rs Normal file
View file

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

View file

@ -24,9 +24,6 @@ pub async fn handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Res
ws.on_upgrade(|socket| handle_socket(socket, state))
}
#[derive(Debug, Clone)]
struct WSOwner(Option<WSUser>);
#[derive(Debug, Clone)]
struct WSUser {
username: String,
@ -34,9 +31,13 @@ struct WSUser {
uuid: Uuid,
}
impl WSOwner {
trait ExtWSUser {
fn name(&self) -> String;
}
impl ExtWSUser for Option<WSUser> {
fn name(&self) -> String {
if let Some(user) = &self.0 {
if let Some(user) = self {
format!(" ({})", user.username)
} else {
String::new()
@ -46,31 +47,24 @@ impl WSOwner {
async fn handle_socket(mut socket: WebSocket, state: AppState) {
debug!("[WebSocket] New unknown connection!");
let mut owner = WSOwner(None);
let cutoff: DashMap<Uuid, Arc<Notify>> = DashMap::new();
let (mtx, mut mrx) = mpsc::channel(64);
let mut bctx: Option<broadcast::Sender<Vec<u8>>> = None;
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());
if let Some(u) = owner.0 {
state.broadcasts.remove(&u.uuid);
state.user_manager.remove(&u.uuid);
}
return;
break;
}
msg
} else {
debug!("[WebSocket{}] Receive error! Connection terminated!", owner.name());
if let Some(u) = owner.0 {
state.broadcasts.remove(&u.uuid);
state.user_manager.remove(&u.uuid);
}
return;
break;
};
// Next is the code for processing msg
let msg_vec = msg.clone().into_data();
@ -80,11 +74,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
Ok(data) => data,
Err(e) => {
error!("[WebSocket{}] This message is not from Figura! {e:?}", owner.name());
if let Some(u) = owner.0 {
state.broadcasts.remove(&u.uuid);
state.user_manager.remove(&u.uuid);
}
return;
break;
},
};
@ -97,7 +87,8 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
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.0 = Some(WSUser { username: t.username.clone(), token, uuid: t.uuid });
owner = Some(WSUser { username: t.username.clone(), token, 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) => {
@ -113,53 +104,52 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
None => {
warn!("[WebSocket] Authenticaton error! Connection terminated!");
debug!("[WebSocket] Tried to log in with {token}"); // Tried to log in with token: {token}
if let Some(u) = owner.0 {
state.broadcasts.remove(&u.uuid);
state.user_manager.remove(&u.uuid);
}
return; // TODO: Define the trip code
break;
},
};
},
C2SMessage::Ping(_, _, _) => {
debug!("[WebSocket{}] C2S : Ping", owner.name());
let data = into_s2c_ping(msg_vec, owner.0.clone().unwrap().uuid);
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
debug!("[WebSocket{}] C2S : Sub", owner.name());
// Rejecting Sub to itself
if uuid == owner.0.clone().unwrap().uuid {
// Ignoring self Sub
if uuid == owner.clone().unwrap().uuid {
continue;
};
let rx = match state.broadcasts.get(&uuid) {
Some(rx) => rx.to_owned().subscribe(),
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);
state.broadcasts.insert(uuid, tx);
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());
tokio::spawn(subscribe(mtx.clone(), rx, shutdown.clone()));
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) => {
debug!("[WebSocket{}] C2S : Unsub", owner.name());
// Rejecting UnSub to itself
if uuid == owner.0.clone().unwrap().uuid {
// Ignoring self Unsub
if uuid == owner.clone().unwrap().uuid {
continue;
};
let shutdown = cutoff.remove(&uuid).unwrap().1;
shutdown.notify_one();
let shutdown = cutoff.remove(&uuid).unwrap().1; // Getting <Notify> from list // FIXME: UNWRAP PANIC! NONE VALUE
shutdown.notify_one(); // Shutdown <subscribe> function
continue;
},
}
@ -168,11 +158,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
debug!("[WebSocket{}] Answering: {msg:?}", owner.name());
if socket.send(msg).await.is_err() {
warn!("[WebSocket{}] Send error! Connection terminated!", owner.name());
if let Some(u) = owner.0 {
state.broadcasts.remove(&u.uuid);
state.user_manager.remove(&u.uuid);
}
return;
break;
}
}
msg = mrx.recv() => {
@ -182,16 +168,19 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
}
Err(_) => {
warn!("[WebSocketSubscriber{}] Send error! Connection terminated!", owner.name());
if let Some(u) = owner.0 {
state.broadcasts.remove(&u.uuid);
state.user_manager.remove(&u.uuid);
}
return;
break;
}
}
}
}
}
// Closing connection
if let Some(u) = owner {
state.session.remove(&u.uuid);
// state.broadcasts.remove(&u.uuid); // NOTE: Create broadcasts manager ??
state.user_manager.remove(&u.uuid);
}
}
async fn subscribe(