diff --git a/cruciverb-core/src/puzzle.rs b/cruciverb-core/src/puzzle.rs index f80daca..a7e81a6 100644 --- a/cruciverb-core/src/puzzle.rs +++ b/cruciverb-core/src/puzzle.rs @@ -241,6 +241,16 @@ pub fn generate_themed_puzzle( ) -> Result { let start = Instant::now(); + // Bias the fill toward theme words when a theme is requested. + let theme_boost = theme_words + .filter(|w| !w.is_empty()) + .map(|tw| { + let mut d = dict.clone(); + d.boost_theme(tw); + d + }); + let solve_dict = theme_boost.as_ref().unwrap_or(dict); + let cores = std::thread::available_parallelism() .map(|n| n.get()) .unwrap_or(1) @@ -250,7 +260,7 @@ pub fn generate_themed_puzzle( if cores > 1 && config.width > 9 { return parallel_generate( config, - dict, + solve_dict, clue_db, difficulty, theme, @@ -260,7 +270,7 @@ pub fn generate_themed_puzzle( ); } - sequential_generate(config, dict, clue_db, difficulty, theme, theme_words, start) + sequential_generate(config, solve_dict, clue_db, difficulty, theme, theme_words, start) } struct SolvedGrid { diff --git a/cruciverb-server/src/main.rs b/cruciverb-server/src/main.rs index 01ae1a8..61a48ac 100644 --- a/cruciverb-server/src/main.rs +++ b/cruciverb-server/src/main.rs @@ -540,8 +540,8 @@ async fn main() { .allow_methods([axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::PUT, axum::http::Method::DELETE]) .allow_headers([axum::http::header::CONTENT_TYPE, axum::http::header::HeaderName::from_static("x-admin-session")]) } else { - tracing::warn!("CORS_ORIGIN not set - using permissive CORS (set CORS_ORIGIN for production)"); - CorsLayer::permissive() + tracing::warn!("CORS_ORIGIN not set - allowing same-origin requests only (set CORS_ORIGIN to allow cross-origin)"); + CorsLayer::new() }; let app = app diff --git a/cruciverb-server/src/rooms.rs b/cruciverb-server/src/rooms.rs index 169bbb1..b4d70e3 100644 --- a/cruciverb-server/src/rooms.rs +++ b/cruciverb-server/src/rooms.rs @@ -961,7 +961,7 @@ pub async fn create_room( RoomMode::Party => 20, }; - let (tx, _) = broadcast::channel(256); + let (tx, _) = broadcast::channel(1024); let grid = vec![vec![String::new(); width]; height]; let party_sub_str = req.get("party_sub").and_then(|v| v.as_str()).unwrap_or("wrong"); @@ -1374,6 +1374,31 @@ async fn handle_ws( break; } } + Err(broadcast::error::RecvError::Lagged(_)) => { + // A slow consumer fell behind: re-send full state so the + // client resyncs instead of being silently dropped or + // disconnected. + let rooms = state.rooms.read().await; + if let Some(room) = rooms.get(&code) { + let resync = serde_json::to_string(&WsMessage::FullState { + grid: room.grid.clone(), + players: room.players.values().map(|p| PlayerInfo { + id: p.id.clone(), + name: p.name.clone(), + color: p.color.clone(), + cursor: p.cursor, + direction: p.direction.clone(), + order: p.order, + }).collect(), + current_turn: room.current_player_id().map(|s| s.to_string()), + started: room.started, + your_id: token.clone(), + }).unwrap(); + if send_text(&mut socket, resync).await.is_err() { + break; + } + } + } Err(_) => break, } } diff --git a/cruciverb-server/src/security.rs b/cruciverb-server/src/security.rs index 776ee2f..dd877a0 100644 --- a/cruciverb-server/src/security.rs +++ b/cruciverb-server/src/security.rs @@ -113,7 +113,7 @@ pub fn is_internal_url(url: &str) -> bool { if host.contains('@') { return true; } // loopback - host == "localhost" + let blocked = host == "localhost" || host.starts_with("127.") // full 127.0.0.0/8 range || host == "::1" || host.starts_with("::ffff:127.") // IPv4-mapped IPv6 loopback @@ -146,7 +146,8 @@ pub fn is_internal_url(url: &str) -> bool { // cloud metadata endpoints || host == "169.254.169.254" || host == "metadata.google.internal" - || host == "100.100.100.200" // Alibaba cloud metadata + || host == "100.100.100.200"; // Alibaba cloud metadata + blocked || resolves_to_internal_ip(host) } fn is_172_private(host: &str) -> bool { @@ -163,6 +164,47 @@ fn is_cgnat(host: &str) -> bool { (64..=127).contains(&second) } +fn is_internal_ip(ip: &std::net::IpAddr) -> bool { + match ip { + std::net::IpAddr::V4(v4) => { + let o = v4.octets(); + o[0] == 0 + || o[0] == 10 + || o[0] == 127 + || (o[0] == 169 && o[1] == 254) + || (o[0] == 172 && (16..=31).contains(&o[1])) + || (o[0] == 192 && o[1] == 168) + || (o[0] == 100 && (64..=127).contains(&o[1])) + || (o[0] == 198 && (o[1] == 18 || o[1] == 19)) + || o[0] >= 224 + } + std::net::IpAddr::V6(v6) => { + v6.is_loopback() + || v6.is_unspecified() + || v6.is_multicast() + || (v6.segments()[0] & 0xfe00 == 0xfc00) + || (v6.segments()[0] & 0xffc0 == 0xfe80) + } + } +} + +fn resolves_to_internal_ip(host: &str) -> bool { + let host = host.strip_prefix('[').and_then(|h| h.strip_suffix(']')).unwrap_or(host); + if host.is_empty() || host.parse::().is_ok() { + return false; + } + use std::net::ToSocketAddrs; + let Ok(addrs) = (host, 0_u16).to_socket_addrs() else { + return false; + }; + for addr in addrs { + if is_internal_ip(&addr.ip()) { + return true; + } + } + false +} + pub fn sanitize_html(input: &str) -> String { input .replace('&', "&") diff --git a/cruciverb-server/src/webauthn.rs b/cruciverb-server/src/webauthn.rs index b097aef..425bf24 100644 --- a/cruciverb-server/src/webauthn.rs +++ b/cruciverb-server/src/webauthn.rs @@ -131,7 +131,7 @@ pub fn create_registration_challenge( authenticator_attachment: None, resident_key: "preferred".into(), require_resident_key: false, - user_verification: "preferred".into(), + user_verification: "required".into(), }, attestation: "none".into(), } @@ -204,6 +204,10 @@ pub fn verify_registration( if flags & 0x01 == 0 { return Err("user not present".into()); } + // UV flag (bit 2): user verification (PIN/biometric) is required + if flags & 0x04 == 0 { + return Err("user verification required".into()); + } if flags & 0x40 == 0 { return Err("no attested credential data".into()); } @@ -301,6 +305,9 @@ pub fn verify_authentication( if auth_data[32] & 0x01 == 0 { return Err("user not present".into()); } + if auth_data[32] & 0x04 == 0 { + return Err("user verification required".into()); + } let counter = u32::from_be_bytes([auth_data[33], auth_data[34], auth_data[35], auth_data[36]]);