Skip to content

Commit b49ef79

Browse files
authored
Add configurable CORS support for /api/rpc endpoint (#486)
- Add tower-http with cors feature as a regular dependency - Configure CORS origins via CORS_ALLOWED_ORIGINS environment variable - Support comma-separated list of origins or "*" for any origin - Default to common localhost origins for development - Fix clippy warning about uninlined format args - Document new environment variable in README This enables cross-origin requests to /api/rpc when WEBSOCKET_HOST is configured to a different origin, while maintaining security by requiring explicit origin configuration in production. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> :house: Remote-Dev: homespace
1 parent 4fa93eb commit b49ef79

File tree

4 files changed

+54
-3
lines changed

4 files changed

+54
-3
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ listens on 127.0.0.1:3030, and should only be exposed to an external network
1616
behind a proxy that supports both HTTP and WebSocket protocols (only tested
1717
with `nginx`).
1818

19+
## Environment Variables
20+
21+
- `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`).
22+
1923
# Development
2024

2125
```

backend/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ publish = false
88
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
99
[features]
1010
default = []
11-
dynamic = ["slog-term", "tower-http"]
11+
dynamic = ["slog-term"]
1212

1313
[dependencies]
1414
anyhow = "1.0"
@@ -39,7 +39,7 @@ tokio = { version = "1.28", features = [
3939
"sync",
4040
"io-util",
4141
] }
42-
tower-http = { version = "0.4", features = ["fs"], optional = true }
42+
tower-http = { version = "0.4", features = ["fs", "cors"] }
4343
zstd = "0.12"
4444

4545
[dev-dependencies]

backend/src/main.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ use axum::{
2525
extract::Path,
2626
response::Response,
2727
};
28+
use http::{HeaderValue, Method};
29+
use tower_http::cors::{AllowOrigin, CorsLayer};
2830
#[cfg(feature = "dynamic")]
2931
use tower_http::services::ServeDir;
3032

@@ -146,7 +148,52 @@ async fn main() -> Result<(), anyhow::Error> {
146148
)
147149
.route("/*path", get(serve_static_routes));
148150

151+
// Configure CORS based on environment variables
152+
// CORS_ALLOWED_ORIGINS: comma-separated list of allowed origins (e.g., "http://localhost:3000,https://example.com")
153+
// Set to "*" to allow any origin (not recommended for production)
154+
// If not set, defaults to allowing localhost origins in development
155+
let cors = {
156+
let allowed_origins = std::env::var("CORS_ALLOWED_ORIGINS")
157+
.unwrap_or_else(|_| {
158+
// Default to common development origins if not specified
159+
"http://localhost:3000,http://localhost:3030,http://127.0.0.1:3000,http://127.0.0.1:3030".to_string()
160+
});
161+
162+
if allowed_origins.trim() == "*" {
163+
// Allow any origin (use with caution)
164+
info!(
165+
ROOT_LOGGER,
166+
"CORS configured to allow ANY origin - not recommended for production"
167+
);
168+
CorsLayer::new()
169+
.allow_origin(tower_http::cors::Any)
170+
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
171+
.allow_headers(tower_http::cors::Any)
172+
} else {
173+
let origins: Vec<HeaderValue> = allowed_origins
174+
.split(',')
175+
.filter_map(|origin| origin.trim().parse::<HeaderValue>().ok())
176+
.collect();
177+
178+
if origins.is_empty() {
179+
// If no valid origins, fall back to same-origin only
180+
info!(
181+
ROOT_LOGGER,
182+
"No valid CORS origins configured, using same-origin policy"
183+
);
184+
CorsLayer::new()
185+
} else {
186+
info!(ROOT_LOGGER, "CORS origins configured: {:?}", origins);
187+
CorsLayer::new()
188+
.allow_origin(AllowOrigin::list(origins))
189+
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
190+
.allow_headers(tower_http::cors::Any)
191+
}
192+
}
193+
};
194+
149195
let app = app
196+
.layer(cors)
150197
.layer(Extension(backend_storage))
151198
.layer(Extension(stats));
152199

mechanics/src/hands.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ impl<'de> Deserialize<'de> for Hands {
5050
if let serde_json::Value::Object(obj) = helper.hands {
5151
for (key, value) in obj {
5252
let player_id = PlayerID::from_str(&key)
53-
.map_err(|_| de::Error::custom(format!("invalid PlayerID: {}", key)))?;
53+
.map_err(|_| de::Error::custom(format!("invalid PlayerID: {key}")))?;
5454
let card_map: HashMap<Card, usize> =
5555
serde_json::from_value(value).map_err(de::Error::custom)?;
5656
hands_map.insert(player_id, card_map);

0 commit comments

Comments
 (0)