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: on:
push: push:
branches: [ "dev" ] branches:
- "**"
tags-ignore:
- '**'
jobs: jobs:
docker: docker:

646
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package] [package]
name = "sculptor" name = "sculptor"
authors = ["Shiroyashik <shiroyashik@shsr.ru>"] authors = ["Shiroyashik <shiroyashik@shsr.ru>"]
version = "0.4.0" version = "0.4.1-dev"
edition = "2021" edition = "2021"
publish = false publish = false
@ -32,14 +32,14 @@ walkdir = "2.5"
indexmap = { version = "2.6", features = ["serde"] } indexmap = { version = "2.6", features = ["serde"] }
zip = "2.2" zip = "2.2"
lazy_static = "1.5" lazy_static = "1.5"
notify = "7.0" notify = "8.0"
# Crypto # Crypto
ring = "0.17" ring = "0.17"
rand = "0.8" rand = "0.9"
# Web framework # Web
axum = { version = "0.7", features = ["ws", "macros", "http2"] } axum = { version = "0.8", features = ["ws", "macros", "http2"] }
tower-http = { version = "0.6", features = ["trace"] } tower-http = { version = "0.6", features = ["trace"] }
tokio = { version = "1.41", features = ["full"] } 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" listen = "0.0.0.0:6665"
## Don't touch if you don't know what you're doing ## 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. ## If not set, default providers (Mojang, ElyBy) will be provided.
# authProviders = [ # authProviders = [
# { name = "Mojang", url = "https://sessionserver.mojang.com/session/minecraft/hasJoined" }, # { 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. ## Enabling Asset Updater.
@ -71,7 +71,7 @@ customText = """
[limitations] [limitations]
maxAvatarSize = 100 # KB maxAvatarSize = 100 # KB
maxAvatars = 10 # It doesn't look like Figura has any actions implemented with this? 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] [advancedUsers.66004548-4de5-49de-bade-9c3933d8eb97]
username = "Shiroyashik" username = "Shiroyashik"

View file

@ -1,6 +1,6 @@
## Chef ## Chef
# FROM clux/muslrust:stable AS 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 USER root
RUN apk add --no-cache musl-dev libressl-dev RUN apk add --no-cache musl-dev libressl-dev
RUN cargo install cargo-chef RUN cargo install cargo-chef
@ -23,7 +23,7 @@ COPY src src
RUN cargo build --release --target x86_64-unknown-linux-musl --bin sculptor RUN cargo build --release --target x86_64-unknown-linux-musl --bin sculptor
## Runtime ## Runtime
FROM alpine:3.20.0 AS runtime FROM alpine:3.21 AS runtime
WORKDIR /app WORKDIR /app
COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/sculptor /app/sculptor 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. 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! > NGINX requires additional configuration to work with websocket!
### Docker ### 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 A pre-installed Rust will be required for the build
```sh ```sh
# Clone the latest release # Clone the pre-release
git clone https://github.com/shiroyashik/sculptor.git git clone https://github.com/shiroyashik/sculptor.git
# or a dev release # or clone specific version
git clone --branch dev https://github.com/shiroyashik/sculptor.git git clone --depth 1 --branch v0.4.0 https://github.com/shiroyashik/sculptor.git
# Enter the folder # Enter the folder
cd sculptor cd sculptor
# Copy Sculptor configuration file # Copy Sculptor configuration file
@ -73,6 +73,13 @@ cargo build --release
cargo run --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 ## Contributing
![Ask me anything!](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg) ![Ask me anything!](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg)
on 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. ❤ 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 ## License
The Sculptor is licensed under [GPL-3.0](LICENSE) The Sculptor is licensed under [GPL-3.0](LICENSE)

View file

@ -33,7 +33,7 @@
Убедитесь, что используемый вами обратный прокси-сервер поддерживает WebSocket, а для HTTPS-соединений используются действительные сертификаты. Убедитесь, что используемый вами обратный прокси-сервер поддерживает WebSocket, а для HTTPS-соединений используются действительные сертификаты.
> [!IMPORTANT] > [!WARNING]
> NGINX требует дополнительной настройки для работы с websocket! > NGINX требует дополнительной настройки для работы с websocket!
### Docker ### Docker
@ -55,10 +55,10 @@
Для сборки потребуется предустановленный Rust Для сборки потребуется предустановленный Rust
```sh ```sh
# Клонируем последний релиз # Клонируем пре-релиз
git clone https://github.com/shiroyashik/sculptor.git 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 cd sculptor
# Меняем имя конфиг файлу # Меняем имя конфиг файлу
@ -71,6 +71,13 @@ cargo build --release
cargo run --release cargo run --release
``` ```
#### Сборка из `master` ветки
> [!IMPORTANT]
> Сборка Sculptor непосредственно из ветки `master` **не рекомендуется** для большинства пользователей. Эта ветка содержит предрелизный код, который активно разрабатывается и может содержать неработающие или нестабильные функции. Кроме того, использование ветки `master` может привести к проблемам с миграцией данных при обновлении до будущих стабильных релизов.
>
> Если вы все же решили использовать ветку `master`, пожалуйста, имейте в виду, что вы можете столкнуться с ошибками или некорректным поведением. Тем не менее ваши сообщения об ошибках высоко ценятся. Однако для более стабильной и надежной работы настоятельно рекомендую использовать **последний официальный релиз**.
## Вклад в развитие ## Вклад в развитие
![Спроси меня о чём угодно!](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg) ![Спроси меня о чём угодно!](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 ## License
The Sculptor is licensed under [GPL-3.0](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> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(versions)) .route("/", get(versions))
.route("/:version", get(hashes)) .route("/{version}", get(hashes))
.route("/:version/*key", get(download)) .route("/{version}/{*path}", get(download))
} }
async fn versions() -> ApiResult<Json<Value>> { async fn versions() -> ApiResult<Json<Value>> {

View file

@ -1,11 +1,12 @@
use anyhow::bail; use anyhow::bail;
use axum::extract::{ws::{Message, WebSocket}, State}; use axum::{body::Bytes, extract::{ws::{Message, WebSocket}, State}};
use dashmap::DashMap; use dashmap::DashMap;
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
use tracing::instrument;
use crate::{auth::Userinfo, AppState}; 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( pub async fn initial(
ws: axum::extract::WebSocketUpgrade, ws: axum::extract::WebSocketUpgrade,
@ -42,9 +43,8 @@ async fn handle_socket(mut ws: WebSocket, state: AppState) {
}; };
// Starting main worker // Starting main worker
match main_worker(&mut session, &mut ws, &state).await { if let Err(kind) = main_worker(&mut session, &mut ws, &state).await {
Ok(_) => (), tracing::error!("[WebSocket] Main worker halted due to {}.", kind)
Err(kind) => tracing::error!("[WebSocket] Main worker halted due to {}.", kind),
} }
for (_, handle) in session.sub_workers_aborthandles { for (_, handle) in session.sub_workers_aborthandles {
@ -61,9 +61,10 @@ async fn handle_socket(mut ws: WebSocket, state: AppState) {
} }
// Closing connection // 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<()> { 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); tracing::debug!("WebSocket control for {} is transferred to the main worker", session.user.nickname);
loop { loop {
@ -90,7 +91,7 @@ async fn main_worker(session: &mut WSSession, ws: &mut WebSocket, state: &AppSta
// Echo check // Echo check
if echo { if echo {
ws.send(Message::Binary(s2c_ping.clone())).await? ws.send(Message::Binary(s2c_ping.clone().into())).await?
} }
// Sending to others // Sending to others
let _ = session.subs_tx.send(s2c_ping); 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!"))?; let internal_msg = internal_msg.ok_or(anyhow::anyhow!("Unexpected error! Session channel broken!"))?;
match internal_msg { match internal_msg {
SessionMessage::Ping(msg) => { SessionMessage::Ping(msg) => {
ws.send(Message::Binary(msg)).await? ws.send(Message::Binary(msg.into())).await?
}, },
SessionMessage::Banned => { SessionMessage::Banned => {
let _ = ban_action(ws).await 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)?; let token = String::from_utf8(token.to_vec()).map_err(|_| AuthModeError::ConvertError)?;
match state.user_manager.get(&token) { match state.user_manager.get(&token) {
Some(user) => { 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) Err(AuthModeError::SendError)
} else if !user.banned { } else if !user.banned {
Ok(user.clone()) 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<()> { 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; 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?; 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 websocket;
mod handler; mod handler;
mod processor;
mod types; mod types;
// pub use websocket::*; // 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 errors::*;
pub use c2s::*; 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 figura;
pub mod v1; pub mod sculptor;
pub mod errors; 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 http2ws;
mod users; mod users;
mod types;
mod avatars; mod avatars;
pub fn router(limit: usize) -> Router<AppState> { 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/list", get(users::list))
.route("/user/sessions", get(users::list_sessions)) .route("/user/sessions", get(users::list_sessions))
.route("/user/create", post(users::create_user)) .route("/user/create", post(users::create_user))
.route("/user/:uuid/ban", post(users::ban)) .route("/user/{uuid}/ban", post(users::ban))
.route("/user/:uuid/unban", post(users::unban)) .route("/user/{uuid}/unban", post(users::unban))
.route("/avatar/:uuid", put(avatars::upload_avatar).layer(DefaultBodyLimit::max(limit))) .route("/avatar/{uuid}", put(avatars::upload_avatar).layer(DefaultBodyLimit::max(limit)))
.route("/avatar/:uuid", delete(avatars::delete_avatar)) .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 anyhow::{anyhow, Context};
use axum::{ use axum::{
async_trait, extract::{FromRequestParts, State}, http::{request::Parts, StatusCode} extract::{FromRequestParts, OptionalFromRequestParts, State}, http::{request::Parts, StatusCode}
}; };
use dashmap::DashMap; use dashmap::DashMap;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, instrument, trace, warn};
use uuid::Uuid; use uuid::Uuid;
use crate::{ApiError, ApiResult, AppState, TIMEOUT, USER_AGENT}; use crate::{ApiError, ApiResult, AppState, TIMEOUT, USER_AGENT};
@ -18,7 +18,7 @@ use super::types::*;
pub struct Token(pub String); pub struct Token(pub String);
impl Token { 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 let Some(user) = state.user_manager.get(&self.0) {
if !user.banned { if !user.banned {
Ok(()) Ok(())
@ -31,7 +31,6 @@ impl Token {
} }
} }
#[async_trait]
impl<S> FromRequestParts<S> for Token impl<S> FromRequestParts<S> for Token
where where
S: Send + Sync, 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 // End Extractor
// Work with external APIs // Work with external APIs
@ -259,16 +274,33 @@ impl UManager {
} }
// End of User manager // End of User manager
#[axum::debug_handler]
#[instrument(skip_all)]
pub async fn check_auth( pub async fn check_auth(
token: Option<Token>, token: Option<Token>,
State(state): State<AppState>, State(state): State<AppState>,
) -> ApiResult<&'static str> { ) -> ApiResult<&'static str> {
debug!("Checking auth actuality...");
match token { match token {
Some(token) => { Some(token) => {
token.check_auth(&state).await?; 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") Ok("ok")
}, }
None => Err(ApiError::BadRequest), }
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 { pub fn default_authproviders() -> AuthProviders {
AuthProviders(vec![ AuthProviders(vec![
AuthProvider { name: "Mojang".to_string(), url: "https://sessionserver.mojang.com/session/minecraft/hasJoined".to_string() }, 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 { loop {
if !app().await? { if !app().await? {
break; break;
@ -174,6 +174,7 @@ async fn app() -> Result<bool> {
Arc::clone(&state.session), Arc::clone(&state.session),
Arc::clone(&state.config) Arc::clone(&state.config)
)); ));
// Blacklist auto update
if state.config.read().await.mc_folder.exists() { if state.config.read().await.mc_folder.exists() {
tokio::spawn(update_bans_from_minecraft( tokio::spawn(update_bans_from_minecraft(
state.config.read().await.mc_folder.clone(), state.config.read().await.mc_folder.clone(),
@ -185,13 +186,13 @@ async fn app() -> Result<bool> {
let api = Router::new() let api = Router::new()
.nest("//auth", api_auth::router()) // => /api//auth ¯\_(ツ)_/¯ .nest("//auth", api_auth::router()) // => /api//auth ¯\_(ツ)_/¯
.nest("//assets", api_assets::router()) .nest("//assets", api_assets::router())
.nest("/v1", api::v1::router(limit)) .nest("/v1", api::sculptor::router(limit))
.route("/limits", get(api_info::limits)) .route("/limits", get(api_info::limits))
.route("/version", get(api_info::version)) .route("/version", get(api_info::version))
.route("/motd", get(api_info::motd)) .route("/motd", get(api_info::motd))
.route("/equip", post(api_profile::equip_avatar)) .route("/equip", post(api_profile::equip_avatar))
.route("/:uuid", get(api_profile::user_info)) .route("/{uuid}", get(api_profile::user_info))
.route("/:uuid/avatar", get(api_profile::download_avatar)) .route("/{uuid}/avatar", get(api_profile::download_avatar))
.route("/avatar", put(api_profile::upload_avatar).layer(DefaultBodyLimit::max(limit))) .route("/avatar", put(api_profile::upload_avatar).layer(DefaultBodyLimit::max(limit)))
.route("/avatar", delete(api_profile::delete_avatar)); .route("/avatar", delete(api_profile::delete_avatar));
@ -205,9 +206,11 @@ async fn app() -> Result<bool> {
let listener = tokio::net::TcpListener::bind(listen).await?; let listener = tokio::net::TcpListener::bind(listen).await?;
tracing::info!("Listening on {}", listener.local_addr()?); tracing::info!("Listening on {}", listener.local_addr()?);
axum::serve(listener, app) axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal()) .with_graceful_shutdown(shutdown_signal())
.await?; .await?;
tracing::info!("Serve stopped."); tracing::info!("Serve stopped.");
Ok(false) Ok(false)
} }

View file

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