mirror of
https://github.com/shiroyashik/sculptor.git
synced 2025-12-06 13:01:12 +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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "dev" ]
|
branches:
|
||||||
|
- "**"
|
||||||
|
tags-ignore:
|
||||||
|
- '**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
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]
|
[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"] }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
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.
|
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
|
||||||

|

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

|

|
||||||
в
|
в
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>> {
|
||||||
|
|
|
||||||
|
|
@ -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?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// mod websocket;
|
// mod websocket;
|
||||||
mod handler;
|
mod handler;
|
||||||
mod processor;
|
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
// pub use websocket::*;
|
// 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 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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
pub mod figura;
|
pub mod figura;
|
||||||
pub mod v1;
|
pub mod sculptor;
|
||||||
pub mod errors;
|
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 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))
|
||||||
}
|
}
|
||||||
|
|
@ -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 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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() }
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
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 {
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue