mirror of
https://github.com/shiroyashik/sculptor.git
synced 2025-12-06 04:51:13 +03:00
Compare commits
3 commits
41efdc1981
...
bac1203df8
| Author | SHA1 | Date | |
|---|---|---|---|
| bac1203df8 | |||
| c7c3bd881f | |||
| 59ca04d5f8 |
23 changed files with 555 additions and 468 deletions
5
.github/workflows/dev-release.yml
vendored
5
.github/workflows/dev-release.yml
vendored
|
|
@ -2,7 +2,10 @@ name: Push Dev
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ "dev" ]
|
||||
branches:
|
||||
- "**"
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
|
|
|
|||
646
Cargo.lock
generated
646
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
|
@ -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"] }
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
19
README.md
19
README.md
|
|
@ -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
|
||||

|
||||
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)
|
||||
|
|
|
|||
19
README.ru.md
19
README.ru.md
|
|
@ -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`, пожалуйста, имейте в виду, что вы можете столкнуться с ошибками или некорректным поведением. Тем не менее ваши сообщения об ошибках высоко ценятся. Однако для более стабильной и надежной работы настоятельно рекомендую использовать **последний официальный релиз**.
|
||||
|
||||
## Вклад в развитие
|
||||

|
||||
в
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// mod websocket;
|
||||
mod handler;
|
||||
mod processor;
|
||||
mod types;
|
||||
|
||||
// pub use websocket::*;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
pub mod figura;
|
||||
pub mod v1;
|
||||
pub mod sculptor;
|
||||
pub mod errors;
|
||||
80
src/api/sculptor/http2ws.rs
Normal file
80
src/api/sculptor/http2ws.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct UserUuid {
|
||||
pub uuid: Option<Uuid>,
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
])
|
||||
}
|
||||
11
src/main.rs
11
src/main.rs
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue