Release v0.3.1

See release body for more information
This commit is contained in:
Shiroyasha 2024-09-12 08:00:22 +03:00
parent 9e3bca85cd
commit d5433101f1
12 changed files with 194 additions and 134 deletions

View file

@ -1,7 +1,7 @@
use axum::{debug_handler, extract::{Query, State}, response::{IntoResponse, Response}, routing::get, Router};
use reqwest::StatusCode;
use ring::digest::{self, digest};
use tracing::info;
use tracing::{error, info};
use crate::{auth::{has_joined, Userinfo}, utils::rand, AppState};
use super::types::auth::*;
@ -51,17 +51,23 @@ async fn verify(
return (StatusCode::BAD_REQUEST, "You're banned!".to_string()).into_response();
}
info!("[Authentication] {username} logged in using {}", auth_provider.name);
umanager.insert(
let userinfo = Userinfo {
username,
uuid,
server_id.clone(),
Userinfo {
username,
uuid,
token: Some(server_id.clone()),
auth_provider,
..Default::default()
},
);
token: Some(server_id.clone()),
auth_provider,
..Default::default()
};
match umanager.insert(uuid, server_id.clone(), userinfo.clone()) {
Ok(_) => {},
Err(_) => {
umanager.remove(&uuid);
if umanager.insert(uuid, server_id.clone(), userinfo).is_err() {
error!("Old token error after attempting to remove it! Unexpected behavior!");
return (StatusCode::BAD_REQUEST, "second session detected".to_string()).into_response();
};
}
}
(StatusCode::OK, server_id.to_string()).into_response()
} else {
info!("[Authentication] failed to verify {username}");

View file

@ -1,14 +1,11 @@
use std::sync::Arc;
use axum::{
body::Bytes, extract::{Path, State}, Json
};
use dashmap::DashMap;
use tracing::debug;
use serde_json::{json, Value};
use tokio::{
fs,
io::{self, AsyncReadExt, BufWriter}, sync::broadcast::Sender,
io::{self, AsyncReadExt, BufWriter},
};
use uuid::Uuid;
@ -115,7 +112,7 @@ pub async fn upload_avatar(
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;
send_event(&state.broadcasts, &uuid);
send_event(&state, &uuid).await;
Ok("ok")
}
@ -128,18 +125,27 @@ pub async fn delete_avatar(Token(token): Token, State(state): State<AppState>) -
);
let avatar_file = format!("avatars/{}.moon", user_info.uuid);
fs::remove_file(avatar_file).await.map_err(|err| internal_and_log(err))?;
send_event(&state.broadcasts, &user_info.uuid);
send_event(&state, &user_info.uuid).await;
}
// let avatar_file = format!("avatars/{}.moon",user_info.uuid);
Ok("ok".to_string())
}
pub fn send_event(broadcasts: &Arc<DashMap<Uuid, Sender<Vec<u8>>>>, uuid: &Uuid) {
if let Some(broadcast) = broadcasts.get(&uuid) {
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() {
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() {
debug!("[WebSocket] Failed to send Event! WS doesn't connected? UUID: {uuid}")
};
} else {
debug!("[WebSocket] Failed to send Event! Can't find UUID: {uuid}")
};
}

View file

@ -91,7 +91,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
match newmsg {
C2SMessage::Token(token) => {
debug!("[WebSocket{}] C2S : Token", owner.name());
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) => {
@ -119,7 +119,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
};
},
C2SMessage::Ping(_, _, _) => {
debug!("[WebSocket{}] C2S : Ping", owner.name());
trace!("[WebSocket{}] C2S : Ping", owner.name());
let data = into_s2c_ping(msg_vec, owner.clone().unwrap().uuid);
match bctx.clone().unwrap().send(data) {
Ok(_) => (),
@ -129,7 +129,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
},
// Subscribing
C2SMessage::Sub(uuid) => { // TODO: Eliminate the possibility of using SUB without authentication
debug!("[WebSocket{}] C2S : Sub", owner.name());
trace!("[WebSocket{}] C2S : Sub", owner.name());
// Ignoring self Sub
if uuid == owner.clone().unwrap().uuid {
continue;
@ -152,7 +152,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
},
// Unsubscribing
C2SMessage::Unsub(uuid) => {
debug!("[WebSocket{}] C2S : Unsub", owner.name());
trace!("[WebSocket{}] C2S : Unsub", owner.name());
// Ignoring self Unsub
if uuid == owner.clone().unwrap().uuid {
continue;
@ -186,11 +186,13 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
}
// 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(

View file

@ -23,7 +23,7 @@ pub async fn upload_avatar(
let avatar_file = format!("avatars/{}.moon", &uuid);
let mut file = BufWriter::new(fs::File::create(&avatar_file).await.unwrap());
io::copy(&mut request_data.as_ref(), &mut file).await.unwrap();
send_event(&state.broadcasts, &uuid);
send_event(&state, &uuid).await;
Ok("ok")
}
@ -48,7 +48,7 @@ pub async fn delete_avatar(
return Err(crate::ApiError::NotFound)
}
};
send_event(&state.broadcasts, &uuid);
send_event(&state, &uuid).await;
Ok("ok")
}

View file

@ -11,6 +11,8 @@ pub fn router() -> Router<AppState> {
.route("/verify", get(http2ws::verify))
.route("/raw", post(http2ws::raw))
.route("/sub/raw", post(http2ws::sub_raw))
.route("/user/list", get(users::list))
.route("/user/sessions", get(users::list_sessions))
.route("/user/create", post(users::create_user))
.route("/user/:uuid/ban", post(users::ban))
.route("/user/:uuid/unban", post(users::unban))

View file

@ -5,7 +5,7 @@ use axum::{
use tracing::{debug, info};
use uuid::Uuid;
use crate::{auth::{Token, Userinfo}, ApiResult, AppState};
use crate::{api::errors::internal_and_log, auth::{Token, Userinfo}, ApiResult, AppState};
pub(super) async fn create_user(
Token(token): Token,
@ -44,4 +44,22 @@ pub(super) async fn unban(
state.user_manager.unban(&uuid);
Ok("ok")
}
pub(super) async fn list(
Token(token): Token,
State(state): State<AppState>,
) -> ApiResult<String> {
state.config.read().await.clone().verify_token(&token)?;
serde_json::to_string_pretty(&state.user_manager.get_all_registered()).map_err(|err| { internal_and_log(err) })
}
pub(super) async fn list_sessions(
Token(token): Token,
State(state): State<AppState>,
) -> ApiResult<String> {
state.config.read().await.clone().verify_token(&token)?;
serde_json::to_string_pretty(&state.user_manager.get_all_authenticated()).map_err(|err| { internal_and_log(err) })
}

View file

@ -1,11 +1,12 @@
use std::sync::Arc;
use anyhow::anyhow;
use anyhow::{anyhow, Context};
use axum::{
async_trait, extract::{FromRequestParts, State}, http::{request::Parts, StatusCode}
};
use dashmap::DashMap;
use tracing::{debug, error, trace};
use thiserror::Error;
use tracing::{debug, error, trace, warn};
use uuid::Uuid;
use crate::{ApiError, ApiResult, AppState, TIMEOUT, USER_AGENT};
@ -49,18 +50,28 @@ where
// Work with external APIs
/// Get UUID from JSON response
#[inline]
fn get_id_json(json: &serde_json::Value) -> anyhow::Result<Uuid> {
fn get_id_json(json: &serde_json::Value) -> Result<Uuid, uuid::Error> {
trace!("json: {json:#?}"); // For debugging, we'll get to this later!
let uuid = Uuid::parse_str(json.get("id").unwrap().as_str().unwrap())?;
Ok(uuid)
}
#[derive(Debug, Error)]
enum FetchError {
#[error("invalid response code (expected 200), found {0}.\n Response: {1:#?}")]
WrongResponse(u16, Result<String, reqwest::Error>),
#[error(transparent)]
SendError(#[from] reqwest::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
async fn fetch_json(
auth_provider: &AuthProvider,
server_id: &str,
username: &str,
) -> anyhow::Result<anyhow::Result<(Uuid, AuthProvider)>> {
) -> Result<(Uuid, AuthProvider), FetchError> {
let client = reqwest::Client::builder().timeout(TIMEOUT).user_agent(USER_AGENT).build().unwrap();
let url = auth_provider.url.clone();
@ -72,11 +83,11 @@ async fn fetch_json(
trace!("{res:?}");
match res.status().as_u16() {
200 => {
let json = serde_json::from_str::<serde_json::Value>(&res.text().await?)?;
let uuid = get_id_json(&json)?;
Ok(Ok((uuid, auth_provider.clone())))
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"))?;
Ok((uuid, auth_provider.clone()))
}
_ => Ok(Err(anyhow!("notOK: {} data: {:?}", res.status().as_u16(), res.text().await))),
_ => Err(FetchError::WrongResponse(res.status().as_u16(), res.text().await)),
}
}
@ -100,14 +111,15 @@ pub async fn has_joined(
let mut prov_count: usize = authproviders.len();
while prov_count > 0 {
if let Some(fetch_res) = rx.recv().await {
if let Ok(user_res) = fetch_res {
if let Ok(data) = user_res {
return Ok(Some(data))
} else {
misses.push(user_res.unwrap_err());
}
} else {
errors.push(fetch_res.unwrap_err());
match fetch_res {
Ok(data) => return Ok(Some(data)),
Err(err) => {
match err {
FetchError::WrongResponse(code, data) => misses.push((code, data)),
FetchError::SendError(err) => errors.push(err.to_string()),
FetchError::Other(err) => errors.push(err.to_string()),
}
},
}
} else {
error!("Unexpected behavior!");
@ -134,7 +146,7 @@ async fn fetch_and_send(
provider: AuthProvider,
server_id: String,
username: String,
tx: tokio::sync::mpsc::Sender<anyhow::Result<anyhow::Result<(Uuid, AuthProvider)>>>
tx: tokio::sync::mpsc::Sender<Result<(Uuid, AuthProvider), FetchError>>
) {
let _ = tx.send(fetch_json(&provider, &server_id, &username).await)
.await.map_err( |err| trace!("fetch_and_send error [note: ok res returned and mpsc clossed]: {err:?}"));
@ -159,15 +171,34 @@ impl UManager {
authenticated: Arc::new(DashMap::new()),
}
}
pub fn get_all_registered(&self) -> DashMap<Uuid, Userinfo> {
self.registered.as_ref().clone()
}
pub fn get_all_authenticated(&self) -> DashMap<String, Uuid> {
self.authenticated.as_ref().clone()
}
pub fn pending_insert(&self, server_id: String, username: String) {
self.pending.insert(server_id, username);
}
pub fn pending_remove(&self, server_id: &str) -> Option<(String, String)> {
self.pending.remove(server_id)
}
pub fn insert(&self, uuid: Uuid, token: String, userinfo: Userinfo) {
pub fn insert(&self, uuid: Uuid, token: String, userinfo: Userinfo) -> Result<(), ()> {
// Check for the presence of an active session.
if let Some(userinfo) = self.registered.get(&uuid) {
if let Some(token) = &userinfo.token {
if self.authenticated.contains_key(token) {
warn!("Rejected attempt to create a second session for the same user!");
return Err(())
}
debug!("`{}` already have token in registered profile (old token already removed from 'authenticated')", userinfo.username);
}
}
// Adding a user
self.authenticated.insert(token, uuid);
self.insert_user(uuid, userinfo);
Ok(())
}
pub fn insert_user(&self, uuid: Uuid, userinfo: Userinfo) {
// self.registered.insert(uuid, userinfo)
@ -228,7 +259,7 @@ pub async fn check_auth(
token: Option<Token>,
State(state): State<AppState>,
) -> ApiResult<&'static str> {
debug!("Checking auth actuality...");
match token {
Some(token) => {
token.check_auth(&state).await?;

View file

@ -1,8 +1,8 @@
use chrono::Utc;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Userinfo {
pub uuid: Uuid,
@ -33,7 +33,7 @@ impl Default for Userinfo {
// new part
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthProvider {
pub name: String,

View file

@ -140,9 +140,8 @@ async fn main() -> Result<()> {
}
let api = Router::new()
.nest("//auth", api_auth::router())
.nest("//auth", api_auth::router()) // => /api//auth ¯\_(ツ)_/¯
.nest("/v1", api::v1::router())
.route("/", get(check_auth))
.route("/limits", get(api_info::limits))
.route("/version", get(api_info::version))
.route("/motd", get(api_info::motd))
@ -154,10 +153,11 @@ async fn main() -> Result<()> {
let app = Router::new()
.nest("/api", api)
.route("/api/", get(check_auth))
.route("/ws", get(ws))
.route("/health", get(|| async { "ok" }))
.with_state(state)
.layer(TraceLayer::new_for_http().on_request(()));
.layer(TraceLayer::new_for_http().on_request(()))
.route("/health", get(|| async { "ok" }));
let listener = tokio::net::TcpListener::bind(listen).await?;
info!("Listening on {}", listener.local_addr()?);