Compare commits

...

3 commits

Author SHA1 Message Date
bac1203df8
Dev workflow update and enhance sculptor API:
Some checks failed
Push Dev / docker (push) Has been cancelled
+ remove unused types
+ added sending to all
+ enhance logging
2025-02-13 03:18:16 +03:00
c7c3bd881f
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
2025-02-13 01:43:33 +03:00
59ca04d5f8
Change Ely.By URI
Some checks failed
Push Dev / docker (push) Has been cancelled
2024-12-26 09:09:42 +03:00
23 changed files with 555 additions and 468 deletions

View file

@ -2,7 +2,10 @@ name: Push Dev
on:
push:
branches: [ "dev" ]
branches:
- "**"
tags-ignore:
- '**'
jobs:
docker:

646
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package]
name = "sculptor"
authors = ["Shiroyashik <shiroyashik@shsr.ru>"]
version = "0.4.0"
version = "0.4.1-dev"
edition = "2021"
publish = false
@ -32,14 +32,14 @@ walkdir = "2.5"
indexmap = { version = "2.6", features = ["serde"] }
zip = "2.2"
lazy_static = "1.5"
notify = "7.0"
notify = "8.0"
# Crypto
ring = "0.17"
rand = "0.8"
rand = "0.9"
# Web framework
axum = { version = "0.7", features = ["ws", "macros", "http2"] }
# Web
axum = { version = "0.8", features = ["ws", "macros", "http2"] }
tower-http = { version = "0.6", features = ["trace"] }
tokio = { version = "1.41", features = ["full"] }

View file

@ -1,4 +1,4 @@
## Don't touch this if you running under Docker container
## If running in a Docker container, leave this as default.
listen = "0.0.0.0:6665"
## Don't touch if you don't know what you're doing
@ -12,7 +12,7 @@ listen = "0.0.0.0:6665"
## If not set, default providers (Mojang, ElyBy) will be provided.
# authProviders = [
# { name = "Mojang", url = "https://sessionserver.mojang.com/session/minecraft/hasJoined" },
# { name = "ElyBy", url = "http://minecraft.ely.by/session/hasJoined" },
# { name = "ElyBy", url = "https://account.ely.by/api/minecraft/session/hasJoined" },
# ]
## Enabling Asset Updater.
@ -71,7 +71,7 @@ customText = """
[limitations]
maxAvatarSize = 100 # KB
maxAvatars = 10 # It doesn't look like Figura has any actions implemented with this?
# P.S. And it doesn't look like the current API allows anything like that...
## P.S. And it doesn't look like the current API allows anything like that...
[advancedUsers.66004548-4de5-49de-bade-9c3933d8eb97]
username = "Shiroyashik"

View file

@ -1,6 +1,6 @@
## Chef
# FROM clux/muslrust:stable AS chef
FROM rust:1.82.0-alpine3.20 AS chef
FROM rust:1.84-alpine3.21 AS chef
USER root
RUN apk add --no-cache musl-dev libressl-dev
RUN cargo install cargo-chef
@ -23,7 +23,7 @@ COPY src src
RUN cargo build --release --target x86_64-unknown-linux-musl --bin sculptor
## Runtime
FROM alpine:3.20.0 AS runtime
FROM alpine:3.21 AS runtime
WORKDIR /app
COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/sculptor /app/sculptor

View file

@ -35,7 +35,7 @@ To run it you will need a configured reverse proxy server.
Make sure that the reverse proxy you are using supports WebSocket and valid certificates are used for HTTPS connections.
> [!IMPORTANT]
> [!WARNING]
> NGINX requires additional configuration to work with websocket!
### Docker
@ -57,10 +57,10 @@ See the [pre-built archives](https://github.com/shiroyashik/sculptor/releases/la
A pre-installed Rust will be required for the build
```sh
# Clone the latest release
# Clone the pre-release
git clone https://github.com/shiroyashik/sculptor.git
# or a dev release
git clone --branch dev https://github.com/shiroyashik/sculptor.git
# or clone specific version
git clone --depth 1 --branch v0.4.0 https://github.com/shiroyashik/sculptor.git
# Enter the folder
cd sculptor
# Copy Sculptor configuration file
@ -73,6 +73,13 @@ cargo build --release
cargo run --release
```
#### Compiling from the `master` Branch
> [!IMPORTANT]
> Installing Sculptor directly from the `master` branch is **not recommended** for most users. This branch contains pre-release code that is actively being developed and may include broken or unstable features. Additionally, using the `master` branch could potentially cause issues with data migration when upgrading to future stable releases.
>
> If you still choose to use the `master` branch, please be aware that you may encounter bugs or unexpected behavior. Your feedback and bug reports are highly appreciated. However, for a more stable and reliable experience, we strongly advise using the **latest official release** instead.
## Contributing
![Ask me anything!](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg)
on
@ -92,10 +99,6 @@ If you are a Rust developer, you can modify the code yourself and request a Pull
Glad for any help from ideas to PRs. ❤
#### P.S.
The [“master”](https://github.com/shiroyashik/sculptor/tree/master) branch contains the source code of the latest release. [“dev”](https://github.com/shiroyashik/sculptor/tree/dev) branch is used for development.
## License
The Sculptor is licensed under [GPL-3.0](LICENSE)

View file

@ -33,7 +33,7 @@
Убедитесь, что используемый вами обратный прокси-сервер поддерживает WebSocket, а для HTTPS-соединений используются действительные сертификаты.
> [!IMPORTANT]
> [!WARNING]
> NGINX требует дополнительной настройки для работы с websocket!
### Docker
@ -55,10 +55,10 @@
Для сборки потребуется предустановленный Rust
```sh
# Клонируем последний релиз
# Клонируем пре-релиз
git clone https://github.com/shiroyashik/sculptor.git
# или из dev ветки
git clone --branch dev https://github.com/shiroyashik/sculptor.git
# или из выбранного тега
git clone --depth 1 --branch v0.4.0 https://github.com/shiroyashik/sculptor.git
# Переходим в репу
cd sculptor
# Меняем имя конфиг файлу
@ -71,6 +71,13 @@ cargo build --release
cargo run --release
```
#### Сборка из `master` ветки
> [!IMPORTANT]
> Сборка Sculptor непосредственно из ветки `master` **не рекомендуется** для большинства пользователей. Эта ветка содержит предрелизный код, который активно разрабатывается и может содержать неработающие или нестабильные функции. Кроме того, использование ветки `master` может привести к проблемам с миграцией данных при обновлении до будущих стабильных релизов.
>
> Если вы все же решили использовать ветку `master`, пожалуйста, имейте в виду, что вы можете столкнуться с ошибками или некорректным поведением. Тем не менее ваши сообщения об ошибках высоко ценятся. Однако для более стабильной и надежной работы настоятельно рекомендую использовать **последний официальный релиз**.
## Вклад в развитие
![Спроси меня о чём угодно!](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg)
в
@ -90,10 +97,6 @@ cargo run --release
Буду рад любой вашей помощи! ❤
#### Постскриптум
Ветка [“master”](https://github.com/shiroyashik/sculptor/tree/master) содержит код последнего релиза. А [“dev”](https://github.com/shiroyashik/sculptor/tree/dev) ветка дря разработки.
## License
The Sculptor is licensed under [GPL-3.0](LICENSE)

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

@ -7,3 +7,23 @@ pub use session::*;
pub use errors::*;
pub use c2s::*;
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

@ -0,0 +1,80 @@
use std::collections::HashMap;
use axum::extract::{Query, State};
use tracing::instrument;
use uuid::Uuid;
use crate::{api::errors::{error_and_log, internal_and_log}, auth::Token, ApiResult, AppState};
/*
FIXME: need to refactor
*/
pub(super) async fn verify(
Token(token): Token,
State(state): State<AppState>,
) -> ApiResult<&'static str> {
state.config.read().await.clone()
.verify_token(&token)?;
Ok("ok")
}
#[instrument(skip(token, state, body))]
pub(super) async fn raw(
Token(token): Token,
Query(query): Query<HashMap<String, String>>,
State(state): State<AppState>,
body: String,
) -> ApiResult<&'static str> {
tracing::trace!(body = body);
state.config.read().await.clone().verify_token(&token)?;
let mut payload = vec![0; body.len() / 2];
faster_hex::hex_decode(body.as_bytes(), &mut payload).map_err(|err| { tracing::warn!("not raw data"); error_and_log(err, crate::ApiError::NotAcceptable) })?;
if query.contains_key("uuid") == query.contains_key("all") {
tracing::warn!("invalid query params");
return Err(crate::ApiError::BadRequest);
}
if let Some(uuid) = query.get("uuid") {
// for one
let uuid = Uuid::parse_str(uuid).map_err(|err| { tracing::warn!("invalid uuid"); error_and_log(err, crate::ApiError::BadRequest) })?;
let tx = state.session.get(&uuid).ok_or_else(|| { tracing::warn!("unknown uuid"); crate::ApiError::NotFound })?;
tx.value().send(crate::api::figura::SessionMessage::Ping(payload)).await.map_err(internal_and_log)?;
Ok("ok")
} else if query.contains_key("all") {
// for all
for tx in state.session.iter() {
if let Err(e) = tx.value().send(crate::api::figura::SessionMessage::Ping(payload.clone())).await {
tracing::debug!(error = ?e , "error while sending to session");
}
};
Ok("ok")
} else {
tracing::error!("unreachable code!");
Err(crate::ApiError::Internal)
}
}
#[instrument(skip(token, state, body))]
pub(super) async fn sub_raw(
Token(token): Token,
Query(query): Query<HashMap<String, String>>,
State(state): State<AppState>,
body: String,
) -> ApiResult<&'static str> {
tracing::trace!(body = body);
state.config.read().await.clone().verify_token(&token)?;
let mut payload = vec![0; body.len() / 2];
faster_hex::hex_decode(body.as_bytes(), &mut payload).map_err(|err| { tracing::warn!("not raw data"); error_and_log(err, crate::ApiError::NotAcceptable) })?;
if let Some(uuid) = query.get("uuid") {
let uuid = Uuid::parse_str(uuid).map_err(|err| { tracing::warn!("invalid uuid"); error_and_log(err, crate::ApiError::BadRequest) })?;
let tx = state.subscribes.get(&uuid).ok_or_else(|| { tracing::warn!("unknown uuid"); crate::ApiError::NotFound })?;
tx.value().send(payload).map_err(internal_and_log)?;
Ok("ok")
} else {
tracing::warn!("uuid doesnt defined");
Err(crate::ApiError::NotFound)
}
}

View file

@ -3,7 +3,6 @@ use crate::AppState;
mod http2ws;
mod users;
mod types;
mod avatars;
pub fn router(limit: usize) -> Router<AppState> {
@ -14,8 +13,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

@ -1,67 +0,0 @@
use axum::extract::{Query, State};
use tracing::{debug, trace, warn};
use crate::{api::errors::{error_and_log, internal_and_log}, auth::Token, ApiResult, AppState};
use super::types::UserUuid;
pub(super) async fn verify(
Token(token): Token,
State(state): State<AppState>,
) -> ApiResult<&'static str> {
state.config.read().await.clone()
.verify_token(&token)?;
Ok("ok")
}
pub(super) async fn raw(
Token(token): Token,
Query(query): Query<UserUuid>,
State(state): State<AppState>,
body: String,
) -> ApiResult<&'static str> {
trace!(body = body);
state.config.read().await.clone().verify_token(&token)?;
let mut payload = vec![0; body.len() / 2];
faster_hex::hex_decode(body.as_bytes(), &mut payload).map_err(|err| { warn!("not raw data"); error_and_log(err, crate::ApiError::NotAcceptable) })?;
debug!("{:?}", payload);
match query.uuid {
Some(uuid) => {
// for only one
let tx = state.session.get(&uuid).ok_or_else(|| { warn!("unknown uuid"); crate::ApiError::NotFound })?;
tx.value().send(crate::api::figura::SessionMessage::Ping(payload)).await.map_err(internal_and_log)?;
Ok("ok")
},
None => {
// for all
warn!("uuid doesnt defined");
Err(crate::ApiError::NotFound)
},
}
}
pub(super) async fn sub_raw(
Token(token): Token,
Query(query): Query<UserUuid>,
State(state): State<AppState>,
body: String,
) -> ApiResult<&'static str> {
trace!(body = body);
state.config.read().await.clone().verify_token(&token)?;
let mut payload = vec![0; body.len() / 2];
faster_hex::hex_decode(body.as_bytes(), &mut payload).map_err(|err| { warn!("not raw data"); error_and_log(err, crate::ApiError::NotAcceptable) })?;
debug!("{:?}", payload);
match query.uuid {
Some(uuid) => {
// for only one
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 => {
warn!("uuid doesnt defined");
Err(crate::ApiError::NotFound)
},
}
}

View file

@ -1,7 +0,0 @@
use serde::Deserialize;
use uuid::Uuid;
#[derive(Deserialize)]
pub(super) struct UserUuid {
pub uuid: Option<Uuid>,
}

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

@ -61,6 +61,6 @@ pub struct AuthProviders(pub Vec<AuthProvider>);
pub fn default_authproviders() -> AuthProviders {
AuthProviders(vec![
AuthProvider { name: "Mojang".to_string(), url: "https://sessionserver.mojang.com/session/minecraft/hasJoined".to_string() },
AuthProvider { name: "ElyBy".to_string(), url: "http://minecraft.ely.by/session/hasJoined".to_string() }
AuthProvider { name: "ElyBy".to_string(), url: "https://account.ely.by/api/minecraft/session/hasJoined".to_string() }
])
}

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);