diff --git a/src/config.rs b/src/config.rs index d095f6b..1492454 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,6 +22,7 @@ pub struct RuntimeConfig { pub menu_message: String, pub dashboard: DashboardConfig, pub tunnel: TunnelConfig, + pub api: APIConfig, } /// Environment variable key to load the config from @@ -78,6 +79,7 @@ pub struct Config { pub logging: LevelFilter, pub retriever: RetrieverConfig, pub tunnel: TunnelConfig, + pub api: APIConfig, } impl Default for Config { @@ -92,7 +94,8 @@ impl Default for Config { galaxy_at_war: Default::default(), logging: LevelFilter::Info, retriever: Default::default(), - tunnel: Default::default() + tunnel: Default::default(), + api: Default::default() } } } @@ -133,6 +136,24 @@ pub enum QosServerConfig { }, } +#[derive(Deserialize)] +#[serde(default)] +pub struct APIConfig { + /// Allow games data to be requested from the API without auth + pub public_games: bool, + /// Hide players from API response when no auth is provided + pub public_games_hide_players: bool, +} + +impl Default for APIConfig { + fn default() -> Self { + Self { + public_games: false, + public_games_hide_players: true, + } + } +} + #[derive(Deserialize)] #[serde(default)] pub struct GalaxyAtWarConfig { diff --git a/src/main.rs b/src/main.rs index c0a8b13..eb76d3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,6 +45,7 @@ async fn main() { dashboard: config.dashboard, qos: config.qos, tunnel: config.tunnel, + api: config.api, }; debug!("QoS server: {:?}", &runtime_config.qos); diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs index 7511bcd..6468b69 100644 --- a/src/middleware/auth.rs +++ b/src/middleware/auth.rs @@ -13,9 +13,36 @@ use axum::{ }; use futures_util::future::BoxFuture; use sea_orm::DatabaseConnection; +use std::future::Future; use std::sync::Arc; use thiserror::Error; +pub struct MaybeAuth(pub Option); + +impl FromRequestParts for MaybeAuth { + type Rejection = TokenError; + + fn from_request_parts<'a, 'b, 'c>( + parts: &'a mut axum::http::request::Parts, + state: &'b S, + ) -> BoxFuture<'c, Result> + where + 'a: 'c, + 'b: 'c, + Self: 'c, + { + let auth: std::pin::Pin> + Send>> = + Auth::from_request_parts(parts, state); + Box::pin(async move { + match auth.await { + Ok(Auth(value)) => Ok(MaybeAuth(Some(value))), + Err(TokenError::MissingToken) => Ok(MaybeAuth(None)), + Err(err) => Err(err), + } + }) + } +} + pub struct Auth(pub Player); pub struct AdminAuth(pub Player); @@ -47,7 +74,6 @@ const TOKEN_HEADER: &str = "X-Token"; impl FromRequestParts for Auth { type Rejection = TokenError; - fn from_request_parts<'a, 'b, 'c>( parts: &'a mut axum::http::request::Parts, _state: &'b S, diff --git a/src/routes/games.rs b/src/routes/games.rs index 3e4a535..ebeb62a 100644 --- a/src/routes/games.rs +++ b/src/routes/games.rs @@ -1,6 +1,7 @@ use crate::{ + config::RuntimeConfig, database::entities::players::PlayerRole, - middleware::auth::Auth, + middleware::auth::MaybeAuth, services::game::{manager::GameManager, GameSnapshot}, utils::types::GameID, }; @@ -20,6 +21,8 @@ pub enum GamesError { /// The requested game could not be found (For specific game lookup) #[error("Game not found")] NotFound, + #[error("Missing required access")] + NoPermission, } /// The query structure for a players query @@ -55,17 +58,25 @@ pub struct GamesResponse { /// Player networking information is included for requesting /// players with admin level or greater access. pub async fn get_games( - Auth(auth): Auth, + MaybeAuth(auth): MaybeAuth, Query(GamesRequest { offset, count }): Query, Extension(game_manager): Extension>, + Extension(config): Extension>, ) -> Result, GamesError> { + if let (None, false) = (&auth, config.api.public_games) { + return Err(GamesError::NoPermission); + } + let count: usize = count.unwrap_or(20) as usize; let offset: usize = offset * count; - let include_net = auth.role >= PlayerRole::Admin; + let include_net = auth + .as_ref() + .is_some_and(|player| player.role >= PlayerRole::Admin); + let include_players = auth.is_some() || !config.api.public_games_hide_players; // Retrieve the game snapshots let (games, more) = game_manager - .create_snapshot(offset, count, include_net) + .create_snapshot(offset, count, include_net, include_players) .await; // Get the total number of games @@ -86,16 +97,26 @@ pub async fn get_games( /// Player networking information is included for requesting /// players with admin level or greater access. pub async fn get_game( - Auth(auth): Auth, + MaybeAuth(auth): MaybeAuth, Path(game_id): Path, Extension(game_manager): Extension>, + Extension(config): Extension>, ) -> Result, GamesError> { + if let (None, false) = (&auth, config.api.public_games) { + return Err(GamesError::NoPermission); + } + + let include_net = auth + .as_ref() + .is_some_and(|player| player.role >= PlayerRole::Admin); + let include_players = auth.is_some() || !config.api.public_games_hide_players; + let game = game_manager .get_game(game_id) .await .ok_or(GamesError::NotFound)?; let game = &*game.read().await; - let snapshot = game.snapshot(auth.role >= PlayerRole::Admin); + let snapshot = game.snapshot(include_net, include_players); Ok(Json(snapshot)) } @@ -105,6 +126,7 @@ impl IntoResponse for GamesError { fn into_response(self) -> Response { let status_code = match &self { Self::NotFound => StatusCode::NOT_FOUND, + Self::NoPermission => StatusCode::FORBIDDEN, }; (status_code, self.to_string()).into_response() diff --git a/src/routes/gaw.rs b/src/routes/gaw.rs index 9c4f237..7363ecc 100644 --- a/src/routes/gaw.rs +++ b/src/routes/gaw.rs @@ -1,5 +1,5 @@ //! Routes for the Galaxy At War API used by the Mass Effect 3 client in order -//! to retrieve and increase the Galxay At War values for a player. +//! to retrieve and increase the Galaxy At War values for a player. //! //! This API is not documented as it is not intended to be used by anyone //! other than the Mass Effect 3 client itself. @@ -90,7 +90,7 @@ pub async fn shared_token_login(Query(AuthQuery { auth }): Query) -> /// GET /galaxyatwar/getRatings/:id /// /// Route for retrieving the galaxy at war ratings for the player -/// with the provied ID +/// with the provided ID /// /// `id` The hex encoded ID of the player pub async fn get_ratings( diff --git a/src/services/game/manager.rs b/src/services/game/manager.rs index bd5ed0b..e06c817 100644 --- a/src/services/game/manager.rs +++ b/src/services/game/manager.rs @@ -83,6 +83,7 @@ impl GameManager { offset: usize, count: usize, include_net: bool, + include_players: bool, ) -> (Vec, bool) { // Create the futures using the handle action before passing // them to a future to be awaited @@ -111,7 +112,7 @@ impl GameManager { .for_each(|game| { join_set.spawn(async move { let game = &*game.read().await; - game.snapshot(include_net) + game.snapshot(include_net, include_players) }); }); diff --git a/src/services/game/mod.rs b/src/services/game/mod.rs index 108d71e..96e649e 100644 --- a/src/services/game/mod.rs +++ b/src/services/game/mod.rs @@ -68,7 +68,7 @@ pub struct GameSnapshot { /// The game attributes pub attributes: AttrMap, /// Snapshots of the game players - pub players: Box<[GamePlayerSnapshot]>, + pub players: Option>, } /// Attributes map type @@ -423,13 +423,17 @@ impl Game { GameJoinableState::Joinable } - pub fn snapshot(&self, include_net: bool) -> GameSnapshot { - let players = self - .players - .iter() - .map(|value| value.snapshot(include_net)) - .collect(); - + pub fn snapshot(&self, include_net: bool, include_players: bool) -> GameSnapshot { + let players = if include_players { + let players = self + .players + .iter() + .map(|value| value.snapshot(include_net)) + .collect(); + Some(players) + } else { + None + }; GameSnapshot { id: self.id, state: self.state,