harden network and auth guards, boost themed fill
This commit is contained in:
@@ -241,6 +241,16 @@ pub fn generate_themed_puzzle(
|
|||||||
) -> Result<Puzzle> {
|
) -> Result<Puzzle> {
|
||||||
let start = Instant::now();
|
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()
|
let cores = std::thread::available_parallelism()
|
||||||
.map(|n| n.get())
|
.map(|n| n.get())
|
||||||
.unwrap_or(1)
|
.unwrap_or(1)
|
||||||
@@ -250,7 +260,7 @@ pub fn generate_themed_puzzle(
|
|||||||
if cores > 1 && config.width > 9 {
|
if cores > 1 && config.width > 9 {
|
||||||
return parallel_generate(
|
return parallel_generate(
|
||||||
config,
|
config,
|
||||||
dict,
|
solve_dict,
|
||||||
clue_db,
|
clue_db,
|
||||||
difficulty,
|
difficulty,
|
||||||
theme,
|
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 {
|
struct SolvedGrid {
|
||||||
|
|||||||
@@ -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_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")])
|
.allow_headers([axum::http::header::CONTENT_TYPE, axum::http::header::HeaderName::from_static("x-admin-session")])
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("CORS_ORIGIN not set - using permissive CORS (set CORS_ORIGIN for production)");
|
tracing::warn!("CORS_ORIGIN not set - allowing same-origin requests only (set CORS_ORIGIN to allow cross-origin)");
|
||||||
CorsLayer::permissive()
|
CorsLayer::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = app
|
let app = app
|
||||||
|
|||||||
@@ -961,7 +961,7 @@ pub async fn create_room(
|
|||||||
RoomMode::Party => 20,
|
RoomMode::Party => 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (tx, _) = broadcast::channel(256);
|
let (tx, _) = broadcast::channel(1024);
|
||||||
let grid = vec![vec![String::new(); width]; height];
|
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");
|
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;
|
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,
|
Err(_) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ pub fn is_internal_url(url: &str) -> bool {
|
|||||||
if host.contains('@') { return true; }
|
if host.contains('@') { return true; }
|
||||||
|
|
||||||
// loopback
|
// loopback
|
||||||
host == "localhost"
|
let blocked = host == "localhost"
|
||||||
|| host.starts_with("127.") // full 127.0.0.0/8 range
|
|| host.starts_with("127.") // full 127.0.0.0/8 range
|
||||||
|| host == "::1"
|
|| host == "::1"
|
||||||
|| host.starts_with("::ffff:127.") // IPv4-mapped IPv6 loopback
|
|| host.starts_with("::ffff:127.") // IPv4-mapped IPv6 loopback
|
||||||
@@ -146,7 +146,8 @@ pub fn is_internal_url(url: &str) -> bool {
|
|||||||
// cloud metadata endpoints
|
// cloud metadata endpoints
|
||||||
|| host == "169.254.169.254"
|
|| host == "169.254.169.254"
|
||||||
|| host == "metadata.google.internal"
|
|| 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 {
|
fn is_172_private(host: &str) -> bool {
|
||||||
@@ -163,6 +164,47 @@ fn is_cgnat(host: &str) -> bool {
|
|||||||
(64..=127).contains(&second)
|
(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::<std::net::IpAddr>().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 {
|
pub fn sanitize_html(input: &str) -> String {
|
||||||
input
|
input
|
||||||
.replace('&', "&")
|
.replace('&', "&")
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ pub fn create_registration_challenge(
|
|||||||
authenticator_attachment: None,
|
authenticator_attachment: None,
|
||||||
resident_key: "preferred".into(),
|
resident_key: "preferred".into(),
|
||||||
require_resident_key: false,
|
require_resident_key: false,
|
||||||
user_verification: "preferred".into(),
|
user_verification: "required".into(),
|
||||||
},
|
},
|
||||||
attestation: "none".into(),
|
attestation: "none".into(),
|
||||||
}
|
}
|
||||||
@@ -204,6 +204,10 @@ pub fn verify_registration(
|
|||||||
if flags & 0x01 == 0 {
|
if flags & 0x01 == 0 {
|
||||||
return Err("user not present".into());
|
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 {
|
if flags & 0x40 == 0 {
|
||||||
return Err("no attested credential data".into());
|
return Err("no attested credential data".into());
|
||||||
}
|
}
|
||||||
@@ -301,6 +305,9 @@ pub fn verify_authentication(
|
|||||||
if auth_data[32] & 0x01 == 0 {
|
if auth_data[32] & 0x01 == 0 {
|
||||||
return Err("user not present".into());
|
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]]);
|
let counter = u32::from_be_bytes([auth_data[33], auth_data[34], auth_data[35], auth_data[36]]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user