Upgrade/Migrate to newer dependencies (check diff Cargo.toml)
Some checks failed
Push Dev / docker (push) Has been cancelled

+ Rename: api/v1 to api/sculptor
This commit is contained in:
Shiroyasha 2025-02-13 01:43:33 +03:00
parent 59ca04d5f8
commit c7c3bd881f
Signed by: shiroyashik
GPG key ID: E4953D3940D7860A
20 changed files with 470 additions and 390 deletions

View file

@ -12,8 +12,8 @@ use crate::{api::errors::internal_and_log, ApiError, ApiResult, AppState, ASSETS
pub fn router() -> Router<AppState> {
Router::new()
.route("/", get(versions))
.route("/:version", get(hashes))
.route("/:version/*key", get(download))
.route("/{version}", get(hashes))
.route("/{version}/{*path}", get(download))
}
async fn versions() -> ApiResult<Json<Value>> {

View file

@ -1,11 +1,12 @@
use anyhow::bail;
use axum::extract::{ws::{Message, WebSocket}, State};
use axum::{body::Bytes, extract::{ws::{Message, WebSocket}, State}};
use dashmap::DashMap;
use tokio::sync::{broadcast, mpsc};
use tracing::instrument;
use crate::{auth::Userinfo, AppState};
use super::{processor::*, AuthModeError, S2CMessage, C2SMessage, WSSession, SessionMessage, RADError};
use super::{AuthModeError, C2SMessage, RADError, RecvAndDecode, S2CMessage, SessionMessage, WSSession};
pub async fn initial(
ws: axum::extract::WebSocketUpgrade,
@ -42,9 +43,8 @@ async fn handle_socket(mut ws: WebSocket, state: AppState) {
};
// Starting main worker
match main_worker(&mut session, &mut ws, &state).await {
Ok(_) => (),
Err(kind) => tracing::error!("[WebSocket] Main worker halted due to {}.", kind),
if let Err(kind) = main_worker(&mut session, &mut ws, &state).await {
tracing::error!("[WebSocket] Main worker halted due to {}.", kind)
}
for (_, handle) in session.sub_workers_aborthandles {
@ -61,9 +61,10 @@ async fn handle_socket(mut ws: WebSocket, state: AppState) {
}
// Closing connection
if let Err(kind) = ws.close().await { tracing::trace!("[WebSocket] Closing fault: {}", kind) }
if let Err(kind) = ws.send(Message::Close(None)).await { tracing::trace!("[WebSocket] Closing fault: {}", kind) }
}
#[instrument(skip_all, fields(nickname = %session.user.nickname))]
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.nickname);
loop {
@ -90,7 +91,7 @@ async fn main_worker(session: &mut WSSession, ws: &mut WebSocket, state: &AppSta
// Echo check
if echo {
ws.send(Message::Binary(s2c_ping.clone())).await?
ws.send(Message::Binary(s2c_ping.clone().into())).await?
}
// Sending to others
let _ = session.subs_tx.send(s2c_ping);
@ -127,7 +128,7 @@ async fn main_worker(session: &mut WSSession, ws: &mut WebSocket, state: &AppSta
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?
ws.send(Message::Binary(msg.into())).await?
},
SessionMessage::Banned => {
let _ = ban_action(ws).await
@ -169,7 +170,7 @@ async fn authenticate(socket: &mut WebSocket, state: &AppState) -> Result<Userin
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() {
if socket.send(Message::Binary(Bytes::from(Into::<Vec<u8>>::into(S2CMessage::Auth)))).await.is_err() {
Err(AuthModeError::SendError)
} else if !user.banned {
Ok(user.clone())
@ -204,7 +205,7 @@ async fn authenticate(socket: &mut WebSocket, state: &AppState) -> Result<Userin
}
async fn ban_action(ws: &mut WebSocket) -> anyhow::Result<()> {
ws.send(Message::Binary(S2CMessage::Toast(2, "You're banned!".to_string(), None).into())).await?;
ws.send(Message::Binary(Into::<Vec<u8>>::into(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?;

View file

@ -1,6 +1,5 @@
// mod websocket;
mod handler;
mod processor;
mod types;
// pub use websocket::*;

View file

@ -1,32 +0,0 @@
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, faster_hex::hex_string(&msg.into_data())))
},
}
}
}
},
Err(e) => Err(RADError::WebSocketError(e)),
}
} else {
Err(RADError::StreamClosed)
}
}
}

View file

@ -6,4 +6,24 @@ mod session;
pub use session::*;
pub use errors::*;
pub use c2s::*;
pub use s2c::*;
pub use s2c::*;
use axum::extract::ws::{Message, WebSocket};
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> {
let msg = self.recv().await.ok_or(RADError::StreamClosed)??;
if let Message::Close(frame) = msg {
return Err(RADError::Close(frame.map(|f| format!("code: {}, reason: {}", f.code, f.reason))));
}
let data = msg.into_data();
C2SMessage::try_from(data.as_ref())
.map_err(|e| RADError::DecodeError(e, faster_hex::hex_string(&data)))
}
}

View file

@ -1,3 +1,3 @@
pub mod figura;
pub mod v1;
pub mod sculptor;
pub mod errors;

View file

@ -14,8 +14,8 @@ pub fn router(limit: usize) -> Router<AppState> {
.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))
.route("/avatar/:uuid", put(avatars::upload_avatar).layer(DefaultBodyLimit::max(limit)))
.route("/avatar/:uuid", delete(avatars::delete_avatar))
.route("/user/{uuid}/ban", post(users::ban))
.route("/user/{uuid}/unban", post(users::unban))
.route("/avatar/{uuid}", put(avatars::upload_avatar).layer(DefaultBodyLimit::max(limit)))
.route("/avatar/{uuid}", delete(avatars::delete_avatar))
}

View file

@ -2,11 +2,11 @@ use std::sync::Arc;
use anyhow::{anyhow, Context};
use axum::{
async_trait, extract::{FromRequestParts, State}, http::{request::Parts, StatusCode}
extract::{FromRequestParts, OptionalFromRequestParts, State}, http::{request::Parts, StatusCode}
};
use dashmap::DashMap;
use thiserror::Error;
use tracing::{debug, error, trace, warn};
use tracing::{debug, error, instrument, trace, warn};
use uuid::Uuid;
use crate::{ApiError, ApiResult, AppState, TIMEOUT, USER_AGENT};
@ -18,7 +18,7 @@ use super::types::*;
pub struct Token(pub String);
impl Token {
pub async fn check_auth(self, state: &AppState) -> ApiResult<()> {
pub async fn _check_auth(self, state: &AppState) -> ApiResult<()> {
if let Some(user) = state.user_manager.get(&self.0) {
if !user.banned {
Ok(())
@ -31,7 +31,6 @@ impl Token {
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Token
where
S: Send + Sync,
@ -50,6 +49,22 @@ where
}
}
}
impl<S> OptionalFromRequestParts<S> for Token
where
S: Send + Sync,
{
type Rejection = StatusCode; // Not required
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Option<Self>, Self::Rejection> {
let token = parts
.headers
.get("token")
.and_then(|value| value.to_str().ok());
trace!(token = ?token);
Ok(token.map(|t| Self(t.to_string())))
}
}
// End Extractor
// Work with external APIs
@ -259,16 +274,33 @@ impl UManager {
}
// End of User manager
#[axum::debug_handler]
#[instrument(skip_all)]
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?;
Ok("ok")
},
None => Err(ApiError::BadRequest),
match state.user_manager.get(&token.0) {
Some(user) => {
if user.banned {
debug!(nickname = user.nickname, status = "banned", "Token owner is banned");
Err(ApiError::Unauthorized)
} else {
debug!(nickname = user.nickname, status = "ok", "Token verified successfully");
Ok("ok")
}
}
None => {
debug!(token = token.0, status = "invalid", "Invalid token");
Err(ApiError::Unauthorized)
}
}
}
None => {
debug!(status = "not provided", "Token not provided");
Err(ApiError::BadRequest)
}
}
}

View file

@ -107,7 +107,7 @@ async fn main() -> Result<()> {
},
}
// 4. Starting an app() that starts to serve. If app() returns true, the sculptor will be restarted. for future
// 4. Starting an app() that starts to serve. If app() returns true, the sculptor will be restarted. TODO: for future
loop {
if !app().await? {
break;
@ -174,6 +174,7 @@ async fn app() -> Result<bool> {
Arc::clone(&state.session),
Arc::clone(&state.config)
));
// Blacklist auto update
if state.config.read().await.mc_folder.exists() {
tokio::spawn(update_bans_from_minecraft(
state.config.read().await.mc_folder.clone(),
@ -185,13 +186,13 @@ async fn app() -> Result<bool> {
let api = Router::new()
.nest("//auth", api_auth::router()) // => /api//auth ¯\_(ツ)_/¯
.nest("//assets", api_assets::router())
.nest("/v1", api::v1::router(limit))
.nest("/v1", api::sculptor::router(limit))
.route("/limits", get(api_info::limits))
.route("/version", get(api_info::version))
.route("/motd", get(api_info::motd))
.route("/equip", post(api_profile::equip_avatar))
.route("/:uuid", get(api_profile::user_info))
.route("/:uuid/avatar", get(api_profile::download_avatar))
.route("/{uuid}", get(api_profile::user_info))
.route("/{uuid}/avatar", get(api_profile::download_avatar))
.route("/avatar", put(api_profile::upload_avatar).layer(DefaultBodyLimit::max(limit)))
.route("/avatar", delete(api_profile::delete_avatar));
@ -205,9 +206,11 @@ async fn app() -> Result<bool> {
let listener = tokio::net::TcpListener::bind(listen).await?;
tracing::info!("Listening on {}", listener.local_addr()?);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
tracing::info!("Serve stopped.");
Ok(false)
}

View file

@ -3,7 +3,7 @@ use std::{fs::File, io::Read, path::{Path, PathBuf}, sync::Arc};
use notify::{Event, Watcher};
use tokio::{io::AsyncReadExt, sync::RwLock};
use base64::prelude::*;
use rand::{thread_rng, Rng};
use rand::{rng, Rng};
use ring::digest::{self, digest};
use uuid::Uuid;
use chrono::prelude::*;
@ -11,8 +11,8 @@ use chrono::prelude::*;
use crate::{auth::Userinfo, state::{BannedPlayer, Config}, UManager};
pub fn rand() -> [u8; 50] {
let mut rng = thread_rng();
let distr = rand::distributions::Uniform::new_inclusive(0, 255);
let mut rng = rng();
let distr = rand::distr::Uniform::new_inclusive(0, 255).expect("rand() failure.");
let mut nums: [u8; 50] = [0u8; 50];
for x in &mut nums {
*x = rng.sample(distr);