diff --git a/README.md b/README.md index 6847dfc..0901d9c 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ listens on 127.0.0.1:3030, and should only be exposed to an external network behind a proxy that supports both HTTP and WebSocket protocols (only tested with `nginx`). +## Environment Variables + +- `CORS_ALLOWED_ORIGINS`: Comma-separated list of allowed origins for CORS requests to the `/api/rpc` endpoint (e.g., `"https://example.com,https://app.example.com"`). Set to `"*"` to allow any origin (not recommended for production). If not set, defaults to allowing common localhost origins for development (`http://localhost:3000,http://localhost:3030,http://127.0.0.1:3000,http://127.0.0.1:3030`). + # Development ``` diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 0b6bdda..89ec11b 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -8,7 +8,7 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] default = [] -dynamic = ["slog-term", "tower-http"] +dynamic = ["slog-term"] [dependencies] anyhow = "1.0" @@ -39,7 +39,7 @@ tokio = { version = "1.28", features = [ "sync", "io-util", ] } -tower-http = { version = "0.4", features = ["fs"], optional = true } +tower-http = { version = "0.4", features = ["fs", "cors"] } zstd = "0.12" [dev-dependencies] diff --git a/backend/src/main.rs b/backend/src/main.rs index 8edc1e1..65ddb5f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -25,6 +25,8 @@ use axum::{ extract::Path, response::Response, }; +use http::{HeaderValue, Method}; +use tower_http::cors::{AllowOrigin, CorsLayer}; #[cfg(feature = "dynamic")] use tower_http::services::ServeDir; @@ -146,7 +148,52 @@ async fn main() -> Result<(), anyhow::Error> { ) .route("/*path", get(serve_static_routes)); + // Configure CORS based on environment variables + // CORS_ALLOWED_ORIGINS: comma-separated list of allowed origins (e.g., "http://localhost:3000,https://example.com") + // Set to "*" to allow any origin (not recommended for production) + // If not set, defaults to allowing localhost origins in development + let cors = { + let allowed_origins = std::env::var("CORS_ALLOWED_ORIGINS") + .unwrap_or_else(|_| { + // Default to common development origins if not specified + "http://localhost:3000,http://localhost:3030,http://127.0.0.1:3000,http://127.0.0.1:3030".to_string() + }); + + if allowed_origins.trim() == "*" { + // Allow any origin (use with caution) + info!( + ROOT_LOGGER, + "CORS configured to allow ANY origin - not recommended for production" + ); + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) + .allow_headers(tower_http::cors::Any) + } else { + let origins: Vec = allowed_origins + .split(',') + .filter_map(|origin| origin.trim().parse::().ok()) + .collect(); + + if origins.is_empty() { + // If no valid origins, fall back to same-origin only + info!( + ROOT_LOGGER, + "No valid CORS origins configured, using same-origin policy" + ); + CorsLayer::new() + } else { + info!(ROOT_LOGGER, "CORS origins configured: {:?}", origins); + CorsLayer::new() + .allow_origin(AllowOrigin::list(origins)) + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) + .allow_headers(tower_http::cors::Any) + } + } + }; + let app = app + .layer(cors) .layer(Extension(backend_storage)) .layer(Extension(stats)); diff --git a/mechanics/src/hands.rs b/mechanics/src/hands.rs index e545646..e113268 100644 --- a/mechanics/src/hands.rs +++ b/mechanics/src/hands.rs @@ -50,7 +50,7 @@ impl<'de> Deserialize<'de> for Hands { if let serde_json::Value::Object(obj) = helper.hands { for (key, value) in obj { let player_id = PlayerID::from_str(&key) - .map_err(|_| de::Error::custom(format!("invalid PlayerID: {}", key)))?; + .map_err(|_| de::Error::custom(format!("invalid PlayerID: {key}")))?; let card_map: HashMap = serde_json::from_value(value).map_err(de::Error::custom)?; hands_map.insert(player_id, card_map);