Limitations management and real-time MOTD update added.

- deleted unnecessary comments
- korewaChino artefacts removed
This commit is contained in:
Shiroyasha 2024-06-08 13:34:35 +03:00
parent c53c10cb0a
commit cfea3e6e71
Signed by: shiroyashik
GPG key ID: E4953D3940D7860A
12 changed files with 79 additions and 84 deletions

View file

@ -3,8 +3,8 @@ name: Create and publish a Docker image
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: tags:
- master - v*.*.*
permissions: permissions:
packages: write packages: write

View file

@ -1 +1,7 @@
Many thanks to PoolloverNathan (https://github.com/PoolloverNathan) and Martinz64 (https://github.com/Martinz64) for their work on which Sculptor was developed. Many thanks to:
PoolloverNathan (https://github.com/PoolloverNathan)
Martinz64 (https://github.com/Martinz64)
for their work on which Sculptor was developed.
Contributors:
korewaChino (https://github.com/korewaChino)

2
Cargo.lock generated
View file

@ -1134,7 +1134,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "sculptor" name = "sculptor"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"anyhow-http", "anyhow-http",

View file

@ -40,6 +40,10 @@ motd = """
] ]
""" """
[limitations]
maxAvatarSize = 100000
maxAvatars = 10
# Shiroyashik # Shiroyashik
[advancedUsers.66004548-4de5-49de-bade-9c3933d8eb97] [advancedUsers.66004548-4de5-49de-bade-9c3933d8eb97]
special = [0,0,0,1,0,0] # 6 special = [0,0,0,1,0,0] # 6

View file

@ -6,8 +6,6 @@ Implements Ping transmission functionality via Websocket and full avatar upload
And also a distinctive feature is the possibility of player identification through the third-party authorization system [Ely.By](https://ely.by/) And also a distinctive feature is the possibility of player identification through the third-party authorization system [Ely.By](https://ely.by/)
> This is a personal fork of Sculptor, made to be a workaround for a private server.
### Running with Docker ### Running with Docker
You will need an already configured Docker with Traefik (you can use any reverse proxy) You will need an already configured Docker with Traefik (you can use any reverse proxy)
@ -28,8 +26,13 @@ To do this, you will need to reverse proxy port 6665 to your domain with SSL
3. Set up your reverse proxy server 3. Set up your reverse proxy server
4. `cargo run` 4. `cargo run`
### TODO: ### Public server
- [ ] Realization of storing profiles in the database
- [ ] Frontend for moderation I'm keeping the public server running at the moment!
- [ ] Autonomous working without reverse proxy server You can use it if running your own Sculptor instance is difficult for you.
- [ ] and many other...
> figura.shsr.ru
For reasons beyond my control, the server is not available in some countries.
[Check server availability](https://figura.shsr.ru/health)

View file

@ -2,17 +2,22 @@ name: sculptor
services: services:
sculptor: sculptor:
build: . # build: .
image: ghcr.io/korewachino/sculptor:latest image: ghcr.io/shiroyashik/sculptor:latest
container_name: sculptor container_name: sculptor
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./Config.toml:/app/Config.toml:ro - ./Config.toml:/app/Config.toml:ro
- ./avatars:/app/avatars - ./avatars:/app/avatars
## Recommended for use with reverse proxy. ## Recommended for use with reverse proxy.
# networks:
# - traefik
# labels: # labels:
# - traefik.enable=true # - traefik.enable=true
# - traefik.http.routers.sculptor.rule=Host(`mc.example.com`) # - traefik.http.routers.sculptor.rule=Host(`mc.example.com`)
# - traefik.http.routers.sculptor.entrypoints=websecure, web # - traefik.http.routers.sculptor.entrypoints=websecure, web
# - traefik.http.routers.sculptor.tls=true # - traefik.http.routers.sculptor.tls=true
# - traefik.http.routers.sculptor.tls.certresolver=production # - traefik.http.routers.sculptor.tls.certresolver=production
# networks:
# traefik:
# external: true

View file

@ -56,7 +56,6 @@ async fn verify(
if let Some((uuid, auth_system)) = has_joined(&server_id, &username).await.unwrap() { if let Some((uuid, auth_system)) = has_joined(&server_id, &username).await.unwrap() {
info!("[Authorization] {username} logged in using {auth_system:?}"); info!("[Authorization] {username} logged in using {auth_system:?}");
let authenticated = state.authenticated; let authenticated = state.authenticated;
// let link = state.authenticated_link.lock().await; // // Реализация поиска пользователя в HashMap по UUID
authenticated.insert( authenticated.insert(
uuid, uuid,
server_id.clone(), server_id.clone(),
@ -66,7 +65,6 @@ async fn verify(
auth_system, auth_system,
}, },
); );
// link.insert(uuid, crate::AuthenticatedLink(server_id.clone())); // Реализация поиска пользователя в HashMap по UUID
server_id.to_string() server_id.to_string()
} else { } else {
String::from("failed to verify") String::from("failed to verify")
@ -135,7 +133,6 @@ impl ToString for AuthSystem {
} }
/// Get UUID from JSON response /// Get UUID from JSON response
// Written to be reusable so we don't have to specify the same complex code twice
#[inline] #[inline]
fn get_id_json(json: &serde_json::Value) -> anyhow::Result<Uuid> { fn get_id_json(json: &serde_json::Value) -> anyhow::Result<Uuid> {
trace!("json: {json:#?}"); // For debugging, we'll get to this later! trace!("json: {json:#?}"); // For debugging, we'll get to this later!
@ -143,8 +140,6 @@ fn get_id_json(json: &serde_json::Value) -> anyhow::Result<Uuid> {
Ok(uuid) Ok(uuid)
} }
// Considering dropping ely.by support here, I don't really want to deal with it
#[inline] #[inline]
async fn fetch_json( async fn fetch_json(
url: &str, url: &str,
@ -181,39 +176,6 @@ pub async fn has_joined(
server_id: &str, server_id: &str,
username: &str, username: &str,
) -> anyhow::Result<Option<(Uuid, AuthSystem)>> { ) -> anyhow::Result<Option<(Uuid, AuthSystem)>> {
// let client = reqwest::Client::new();
// tokio::select! {
// Ok(Some(res)) = async {
// let res = client.clone().get(
// format!("http://minecraft.ely.by/session/hasJoined?serverId={server_id}&username={username}")).send().await?;
// debug!("{res:?}");
// match res.status().as_u16() {
// 200 => {
// let json = serde_json::from_str::<serde_json::Value>(&res.text().await?)?;
// let uuid = get_id_json(&json)?;
// Ok(Some((uuid, AuthSystem::ElyBy)))
// },
// 401 => Ok(None),
// _ => Err(anyhow::anyhow!("Unknown code: {}", res.status().as_u16()))
// }
// } => {Ok(Some(res))}
// Ok(Some(res)) = async {
// let res = client.clone().get(
// format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?serverId={server_id}&username={username}")).send().await?;
// debug!("{res:?}");
// match res.status().as_u16() {
// 200 => {
// let json = serde_json::from_str::<serde_json::Value>(&res.text().await?)?;
// let uuid = get_id_json(&json)?;
// Ok(Some((uuid, AuthSystem::Mojang)))
// },
// 204 => Ok(None),
// _ => Err(anyhow::anyhow!("Unknown code: {}", res.status().as_u16()))
// }
// } => {Ok(Some(res))}
// else => {Err(anyhow!("Something went wrong in external apis request process"))}
// }
tokio::select! { tokio::select! {
Ok(Some(res)) = fetch_json("http://minecraft.ely.by/session/hasJoined", server_id, username) => {Ok(Some(res))}, Ok(Some(res)) = fetch_json("http://minecraft.ely.by/session/hasJoined", server_id, username) => {Ok(Some(res))},
Ok(Some(res)) = fetch_json("https://sessionserver.mojang.com/session/minecraft/hasJoined", server_id, username) => {Ok(Some(res))}, Ok(Some(res)) = fetch_json("https://sessionserver.mojang.com/session/minecraft/hasJoined", server_id, username) => {Ok(Some(res))},

View file

@ -3,14 +3,22 @@ use std::{io::Read, path::PathBuf};
use serde::Deserialize; use serde::Deserialize;
use toml::Table; use toml::Table;
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Config { pub struct Config {
pub listen: String, pub listen: String,
pub motd: String, pub motd: String,
pub limitations: Limitations,
pub advanced_users: Table, pub advanced_users: Table,
} }
#[derive(Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Limitations {
pub max_avatar_size: u64,
pub max_avatars: u64,
}
impl Config { impl Config {
pub fn parse(path: PathBuf) -> Self { pub fn parse(path: PathBuf) -> Self {
let mut file = std::fs::File::open(path).expect("Access denied or file doesn't exists!"); let mut file = std::fs::File::open(path).expect("Access denied or file doesn't exists!");

View file

@ -1,6 +1,14 @@
use axum::Json; use axum::{extract::State, Json};
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::AppState;
/// Assert health of the server
/// If times out, the server is considered dead, so we can return basically anything
pub async fn health_check() -> String {
"ok".to_string()
}
pub async fn version() -> Json<Value> { pub async fn version() -> Json<Value> {
Json(json!({ Json(json!({
"release": "0.1.4", "release": "0.1.4",
@ -8,7 +16,12 @@ pub async fn version() -> Json<Value> {
})) }))
} }
pub async fn limits() -> Json<Value> { pub async fn motd(State(state): State<AppState>) -> String {
state.config.lock().await.motd.clone()
}
pub async fn limits(State(state): State<AppState>) -> Json<Value> {
let state = &state.config.lock().await.limitations;
Json(json!({ Json(json!({
"rate": { "rate": {
"pingSize": 1024, "pingSize": 1024,
@ -18,8 +31,8 @@ pub async fn limits() -> Json<Value> {
"upload": 1 "upload": 1
}, },
"limits": { "limits": {
"maxAvatarSize": 100000, "maxAvatarSize": state.max_avatar_size,
"maxAvatars": 10, "maxAvatars": state.max_avatars,
"allowedBadges": { "allowedBadges": {
"special": [0,0,0,0,0,0], "special": [0,0,0,0,0,0],
"pride": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] "pride": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

View file

@ -90,18 +90,10 @@ pub struct AppState {
authenticated: Arc<Authenticated>, // <SHA1 serverId, Userinfo> NOTE: In the future, try it in a separate LockRw branch authenticated: Arc<Authenticated>, // <SHA1 serverId, Userinfo> NOTE: In the future, try it in a separate LockRw branch
// Ping broadcasts for WebSocket connections // Ping broadcasts for WebSocket connections
broadcasts: Arc<DashMap<Uuid, broadcast::Sender<Vec<u8>>>>, broadcasts: Arc<DashMap<Uuid, broadcast::Sender<Vec<u8>>>>,
// Advanced configured users // Current configuration
advanced_users: Arc<Mutex<toml::Table>>, config: Arc<Mutex<config::Config>>,
} }
/// Assert health of the server
/// If times out, the server is considered dead, so we can return basically anything
async fn health_check() -> String {
// tokio::time::sleep(std::time::Duration::from_secs(5)).await;
"ok".to_string()
}
const LOGGER_ENV: &str = "RUST_LOG"; const LOGGER_ENV: &str = "RUST_LOG";
#[tokio::main] #[tokio::main]
@ -118,29 +110,30 @@ async fn main() -> Result<()> {
let config_file = std::env::var("CONFIG_PATH").unwrap_or_else(|_| "Config.toml".into()); let config_file = std::env::var("CONFIG_PATH").unwrap_or_else(|_| "Config.toml".into());
info!("The Sculptor MMSI edition v{}", env!("CARGO_PKG_VERSION")); info!("The Sculptor v{}", env!("CARGO_PKG_VERSION"));
// Config // Config
let config = config::Config::parse(config_file.clone().into()); let config = Arc::new(Mutex::new(config::Config::parse(config_file.clone().into())));
let listen = config.listen.as_str(); let listen = config.lock().await.listen.clone();
// State // State
let state = AppState { let state = AppState {
pending: Arc::new(DashMap::new()), pending: Arc::new(DashMap::new()),
authenticated: Arc::new(Authenticated::new()), authenticated: Arc::new(Authenticated::new()),
broadcasts: Arc::new(DashMap::new()), broadcasts: Arc::new(DashMap::new()),
advanced_users: Arc::new(Mutex::new(config.advanced_users)), config: config,
}; };
// Automatic update of advanced_users while the server is running // Automatic update of configuration while the server is running
let advanced_users = state.advanced_users.clone(); let config_update = state.config.clone();
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
tokio::time::sleep(std::time::Duration::from_secs(10)).await; tokio::time::sleep(std::time::Duration::from_secs(10)).await;
let new_config = config::Config::parse(config_file.clone().into()).advanced_users; let new_config = config::Config::parse(config_file.clone().into());
let mut config = advanced_users.lock().await; let mut config = config_update.lock().await;
if new_config != *config { if new_config != *config {
info!("Server configuration modification detected!");
*config = new_config; *config = new_config;
} }
} }
@ -148,20 +141,20 @@ async fn main() -> Result<()> {
let api = Router::new() let api = Router::new()
.nest("//auth", api_auth::router()) .nest("//auth", api_auth::router())
.route("/limits", get(api_info::limits)) // TODO: .route("/limits", get(api_info::limits))
.route("/version", get(api_info::version)) .route("/version", get(api_info::version))
.route("/motd", get(|| async { config.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)) .route("/avatar", put(api_profile::upload_avatar))
.route("/avatar", delete(api_profile::delete_avatar)); // delete Avatar .route("/avatar", delete(api_profile::delete_avatar));
let app = Router::new() let app = Router::new()
.nest("/api", api) .nest("/api", api)
.route("/api/", get(api_auth::status)) .route("/api/", get(api_auth::status))
.route("/ws", get(handler)) .route("/ws", get(handler))
.route("/health", get(health_check)) .route("/health", get(api_info::health_check))
.route_layer(from_extractor::<api_auth::Token>()) .route_layer(from_extractor::<api_auth::Token>())
.with_state(state) .with_state(state)
.layer(TraceLayer::new_for_http().on_request(())); .layer(TraceLayer::new_for_http().on_request(()));

View file

@ -23,7 +23,7 @@ use crate::{
#[debug_handler] #[debug_handler]
pub async fn user_info( pub async fn user_info(
Path(uuid): Path<Uuid>, Path(uuid): Path<Uuid>,
State(state): State<AppState>, // FIXME: Variable doesn't using! State(state): State<AppState>,
) -> Json<Value> { ) -> Json<Value> {
tracing::info!("Receiving profile information for {}", uuid); tracing::info!("Receiving profile information for {}", uuid);
@ -47,10 +47,10 @@ pub async fn user_info(
}, },
"version": "0.1.4+1.20.1", "version": "0.1.4+1.20.1",
"banned": false, "banned": false,
"authSystem": auth_system // add Trust "authSystem": auth_system
}); });
if let Some(settings) = state.advanced_users.lock().await.get(&formatted_uuid) { if let Some(settings) = state.config.lock().await.advanced_users.get(&formatted_uuid) {
let pride = get_correct_array(settings.get("pride").unwrap()); let pride = get_correct_array(settings.get("pride").unwrap());
let special = get_correct_array(settings.get("special").unwrap()); let special = get_correct_array(settings.get("special").unwrap());
let badges = user_info_response let badges = user_info_response
@ -141,7 +141,7 @@ pub async fn equip_avatar(Token(token): Token, State(state): State<AppState>) ->
.is_err() .is_err()
{ {
warn!("[WebSocket] Failed to send Event! Maybe there is no one to send") warn!("[WebSocket] Failed to send Event! Maybe there is no one to send")
// FIXME: Засунуть в Handler // TODO: Put into Handler
}; };
"ok".to_string() "ok".to_string()
} }

View file

@ -5,7 +5,7 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng};
use ring::digest::{self, digest}; use ring::digest::{self, digest};
use uuid::Uuid; use uuid::Uuid;
// Кор функции // Core functions
pub fn rand() -> [u8; 50] { pub fn rand() -> [u8; 50] {
let mut rng = thread_rng(); let mut rng = thread_rng();
let distr = rand::distributions::Uniform::new_inclusive(0, 255); let distr = rand::distributions::Uniform::new_inclusive(0, 255);
@ -15,6 +15,7 @@ pub fn rand() -> [u8; 50] {
} }
nums nums
} }
//? What is this guy doing //? What is this guy doing
#[tracing::instrument] #[tracing::instrument]
pub fn bytes_into_string(code: &[u8]) -> String { pub fn bytes_into_string(code: &[u8]) -> String {
@ -27,7 +28,7 @@ pub fn bytes_into_string(code: &[u8]) -> String {
// String::from_utf8_lossy(code).to_string() // Tried this, causes corrupted string // String::from_utf8_lossy(code).to_string() // Tried this, causes corrupted string
} }
// Конец кор функций // End of Core functions
pub fn _generate_hex_string(length: usize) -> String { pub fn _generate_hex_string(length: usize) -> String {
// FIXME: Variable doesn't using! // FIXME: Variable doesn't using!