diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d7b5a57..0064e95 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,8 +3,8 @@ name: Create and publish a Docker image on: workflow_dispatch: push: - branches: - - master + tags: + - v*.*.* permissions: packages: write diff --git a/CREDITS b/CREDITS index 0792949..40b70f1 100644 --- a/CREDITS +++ b/CREDITS @@ -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) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 90b71fa..e99e948 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1134,7 +1134,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sculptor" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "anyhow-http", diff --git a/Config.example.toml b/Config.example.toml index f452d55..ce64829 100644 --- a/Config.example.toml +++ b/Config.example.toml @@ -40,6 +40,10 @@ motd = """ ] """ +[limitations] +maxAvatarSize = 100000 +maxAvatars = 10 + # Shiroyashik [advancedUsers.66004548-4de5-49de-bade-9c3933d8eb97] special = [0,0,0,1,0,0] # 6 diff --git a/README.md b/README.md index 6a05faf..7187b3d 100644 --- a/README.md +++ b/README.md @@ -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/) -> This is a personal fork of Sculptor, made to be a workaround for a private server. - ### Running with Docker 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 4. `cargo run` -### TODO: -- [ ] Realization of storing profiles in the database -- [ ] Frontend for moderation -- [ ] Autonomous working without reverse proxy server -- [ ] and many other... \ No newline at end of file +### Public server + +I'm keeping the public server running at the moment! +You can use it if running your own Sculptor instance is difficult for you. + +> figura.shsr.ru + +For reasons beyond my control, the server is not available in some countries. + +[Check server availability](https://figura.shsr.ru/health) \ No newline at end of file diff --git a/docker-compose.example.yml b/docker-compose.example.yml index c28069e..eef4778 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -2,17 +2,22 @@ name: sculptor services: sculptor: - build: . - image: ghcr.io/korewachino/sculptor:latest + # build: . + image: ghcr.io/shiroyashik/sculptor:latest container_name: sculptor restart: unless-stopped volumes: - ./Config.toml:/app/Config.toml:ro - ./avatars:/app/avatars ## Recommended for use with reverse proxy. + # networks: + # - traefik # labels: # - traefik.enable=true # - traefik.http.routers.sculptor.rule=Host(`mc.example.com`) # - traefik.http.routers.sculptor.entrypoints=websecure, web # - traefik.http.routers.sculptor.tls=true # - traefik.http.routers.sculptor.tls.certresolver=production +# networks: +# traefik: +# external: true diff --git a/src/auth.rs b/src/auth.rs index 637ac5c..f0ccecd 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -56,7 +56,6 @@ async fn verify( if let Some((uuid, auth_system)) = has_joined(&server_id, &username).await.unwrap() { info!("[Authorization] {username} logged in using {auth_system:?}"); let authenticated = state.authenticated; - // let link = state.authenticated_link.lock().await; // // Реализация поиска пользователя в HashMap по UUID authenticated.insert( uuid, server_id.clone(), @@ -66,7 +65,6 @@ async fn verify( auth_system, }, ); - // link.insert(uuid, crate::AuthenticatedLink(server_id.clone())); // Реализация поиска пользователя в HashMap по UUID server_id.to_string() } else { String::from("failed to verify") @@ -135,7 +133,6 @@ impl ToString for AuthSystem { } /// Get UUID from JSON response -// Written to be reusable so we don't have to specify the same complex code twice #[inline] fn get_id_json(json: &serde_json::Value) -> anyhow::Result { 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 { Ok(uuid) } -// Considering dropping ely.by support here, I don't really want to deal with it - #[inline] async fn fetch_json( url: &str, @@ -181,39 +176,6 @@ pub async fn has_joined( server_id: &str, username: &str, ) -> anyhow::Result> { - // 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::(&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::(&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! { 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))}, diff --git a/src/config.rs b/src/config.rs index 784f449..9d29054 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,14 +3,22 @@ use std::{io::Read, path::PathBuf}; use serde::Deserialize; use toml::Table; -#[derive(Deserialize, Clone, Debug)] +#[derive(Deserialize, Clone, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Config { pub listen: String, pub motd: String, + pub limitations: Limitations, 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 { pub fn parse(path: PathBuf) -> Self { let mut file = std::fs::File::open(path).expect("Access denied or file doesn't exists!"); diff --git a/src/info.rs b/src/info.rs index 33d650d..d76bfdb 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,6 +1,14 @@ -use axum::Json; +use axum::{extract::State, Json}; 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 { Json(json!({ "release": "0.1.4", @@ -8,7 +16,12 @@ pub async fn version() -> Json { })) } -pub async fn limits() -> Json { +pub async fn motd(State(state): State) -> String { + state.config.lock().await.motd.clone() +} + +pub async fn limits(State(state): State) -> Json { + let state = &state.config.lock().await.limitations; Json(json!({ "rate": { "pingSize": 1024, @@ -18,8 +31,8 @@ pub async fn limits() -> Json { "upload": 1 }, "limits": { - "maxAvatarSize": 100000, - "maxAvatars": 10, + "maxAvatarSize": state.max_avatar_size, + "maxAvatars": state.max_avatars, "allowedBadges": { "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] diff --git a/src/main.rs b/src/main.rs index ab903cc..61cf871 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,18 +90,10 @@ pub struct AppState { authenticated: Arc, // NOTE: In the future, try it in a separate LockRw branch // Ping broadcasts for WebSocket connections broadcasts: Arc>>>, - // Advanced configured users - advanced_users: Arc>, + // Current configuration + config: Arc>, } -/// 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"; #[tokio::main] @@ -118,29 +110,30 @@ async fn main() -> Result<()> { 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 - let config = config::Config::parse(config_file.clone().into()); - let listen = config.listen.as_str(); + let config = Arc::new(Mutex::new(config::Config::parse(config_file.clone().into()))); + let listen = config.lock().await.listen.clone(); // State let state = AppState { pending: Arc::new(DashMap::new()), authenticated: Arc::new(Authenticated::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 - let advanced_users = state.advanced_users.clone(); + // Automatic update of configuration while the server is running + let config_update = state.config.clone(); tokio::spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_secs(10)).await; - let new_config = config::Config::parse(config_file.clone().into()).advanced_users; - let mut config = advanced_users.lock().await; + let new_config = config::Config::parse(config_file.clone().into()); + let mut config = config_update.lock().await; if new_config != *config { + info!("Server configuration modification detected!"); *config = new_config; } } @@ -148,20 +141,20 @@ async fn main() -> Result<()> { let api = Router::new() .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("/motd", get(|| async { config.motd })) + .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("/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() .nest("/api", api) .route("/api/", get(api_auth::status)) .route("/ws", get(handler)) - .route("/health", get(health_check)) + .route("/health", get(api_info::health_check)) .route_layer(from_extractor::()) .with_state(state) .layer(TraceLayer::new_for_http().on_request(())); diff --git a/src/profile.rs b/src/profile.rs index 8f5dc68..d9cabf4 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -23,7 +23,7 @@ use crate::{ #[debug_handler] pub async fn user_info( Path(uuid): Path, - State(state): State, // FIXME: Variable doesn't using! + State(state): State, ) -> Json { tracing::info!("Receiving profile information for {}", uuid); @@ -47,10 +47,10 @@ pub async fn user_info( }, "version": "0.1.4+1.20.1", "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 special = get_correct_array(settings.get("special").unwrap()); let badges = user_info_response @@ -141,7 +141,7 @@ pub async fn equip_avatar(Token(token): Token, State(state): State) -> .is_err() { warn!("[WebSocket] Failed to send Event! Maybe there is no one to send") - // FIXME: Засунуть в Handler + // TODO: Put into Handler }; "ok".to_string() } diff --git a/src/utils.rs b/src/utils.rs index 1e1b4ca..0b7a834 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,7 +5,7 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng}; use ring::digest::{self, digest}; use uuid::Uuid; -// Кор функции +// Core functions pub fn rand() -> [u8; 50] { let mut rng = thread_rng(); let distr = rand::distributions::Uniform::new_inclusive(0, 255); @@ -15,6 +15,7 @@ pub fn rand() -> [u8; 50] { } nums } + //? What is this guy doing #[tracing::instrument] 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 } -// Конец кор функций +// End of Core functions pub fn _generate_hex_string(length: usize) -> String { // FIXME: Variable doesn't using!