From 936d4259ece6a302804db7fd2d65ff2017867409 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 15 Jul 2026 02:08:12 +0000 Subject: [PATCH 1/3] fix(security): reject legacy short access keys on RTMP and stats auth paths HTTP stream creation already enforces 32+ character publish/play/stats keys, but RTMP authorization and public stats lookups still accepted shorter values present in upgraded SQLite databases. Reject sub-minimum keys at lookup time so grandfathered weak keys cannot be brute-forced over RTMP or the rate-limited /stats endpoints. Co-authored-by: Alexander Wagner --- src/db.rs | 75 ++++++++++++++++++++++++++------- src/http.rs | 16 +------ src/keygen.rs | 39 +++++++++++++++++ src/rtmp_bridge.rs | 101 +++++++++++++++++++++++---------------------- 4 files changed, 152 insertions(+), 79 deletions(-) diff --git a/src/db.rs b/src/db.rs index ff5abf2..6cab17c 100644 --- a/src/db.rs +++ b/src/db.rs @@ -452,6 +452,9 @@ impl Db { crate::log_error!("stream_find_by: rejected disallowed column '{column}'"); return DbLookup::Failed; } + if !crate::keygen::is_valid_access_key(key) { + return DbLookup::Missing; + } let conn = self.conn.lock(); map_optional(conn.query_row( &format!( @@ -472,6 +475,9 @@ impl Db { /// belongs to a disabled/pending-delete stream, so the RTMP auth-failure /// rate limiter only counts the former as a credential mismatch. pub fn stream_find_by_publish_key_any(&self, key: &str) -> DbLookup { + if !crate::keygen::is_valid_access_key(key) { + return DbLookup::Missing; + } let conn = self.conn.lock(); map_optional(conn.query_row( &format!( @@ -655,6 +661,9 @@ impl Db { } pub fn viewer_find_by_play_key(&self, key: &str) -> DbLookup { + if !crate::keygen::is_valid_access_key(key) { + return DbLookup::Missing; + } let conn = self.conn.lock(); map_optional(conn.query_row( &format!( @@ -1125,9 +1134,9 @@ mod tests { id: id.to_string(), name: format!("{id} name"), app: "live".to_string(), - publish_key: pub_key.to_string(), - play_key: play_key.to_string(), - stats_key: stats_key.to_string(), + publish_key: crate::keygen::test_pad_access_key(pub_key), + play_key: crate::keygen::test_pad_access_key(play_key), + stats_key: crate::keygen::test_pad_access_key(stats_key), enabled: true, created_at: now_ts(), } @@ -1177,18 +1186,18 @@ mod tests { assert_eq!(got.name, "stream1 name"); assert_eq!( - match db.stream_find_by_publish_key("pub_key_123") { + match db.stream_find_by_publish_key(&s.publish_key) { DbLookup::Ok(s) => s.id, _ => panic!("publish key lookup failed"), }, "stream1" ); assert!(matches!( - db.viewer_find_by_play_key("pl_key_456"), + db.viewer_find_by_play_key(&s.play_key), DbLookup::Ok(_) )); assert!(matches!( - db.stream_find_by_stats_key("st_key_789"), + db.stream_find_by_stats_key(&s.stats_key), DbLookup::Ok(_) )); assert!(matches!( @@ -1202,13 +1211,13 @@ mod tests { #[test] fn publishers_players_and_stats() { let db = Db::open(":memory:").unwrap(); - db.stream_add(&sample_stream( + let s = sample_stream( "stream1", "pub_key_123", "pl_key_456", "st_key_789", - )) - .unwrap(); + ); + db.stream_add(&s).unwrap(); let mut p = Publisher { id: "pub1".to_string(), @@ -1231,7 +1240,7 @@ mod tests { assert!(db.publisher_try_acquire(&p)); assert_eq!(db.publisher_list(Some("stream1")).len(), 1); - let DbLookup::Ok(viewer) = db.viewer_find_by_play_key("pl_key_456") else { + let DbLookup::Ok(viewer) = db.viewer_find_by_play_key(&s.play_key) else { panic!("viewer not found"); }; let player = Player { @@ -1380,7 +1389,10 @@ mod tests { }); // Simulate on_close for pub1: find by publish_key -> stream_id -> list. - let DbLookup::Ok(found) = db.stream_find_by_publish_key("pub_key_1") else { + let DbLookup::Ok(stream1) = db.stream_get("stream1") else { + panic!("stream not found"); + }; + let DbLookup::Ok(found) = db.stream_find_by_publish_key(&stream1.publish_key) else { panic!("publish key not found"); }; let mut pubs = db.publisher_list(Some(&found.id)); @@ -1401,14 +1413,14 @@ mod tests { #[test] fn player_try_acquire_enforces_per_key_connection_cap() { let db = Db::open(":memory:").unwrap(); - db.stream_add(&sample_stream( + let s = sample_stream( "stream1", "pub_key_123", "pl_key_456", "st_key_789", - )) - .unwrap(); - let DbLookup::Ok(viewer) = db.viewer_find_by_play_key("pl_key_456") else { + ); + db.stream_add(&s).unwrap(); + let DbLookup::Ok(viewer) = db.viewer_find_by_play_key(&s.play_key) else { panic!("viewer not found"); }; @@ -1434,4 +1446,37 @@ mod tests { }; assert!(!db.player_try_acquire(&overflow)); } + + #[test] + fn legacy_short_access_keys_are_rejected_at_lookup() { + let db = Db::open(":memory:").unwrap(); + let s = Stream { + id: "legacy".to_string(), + name: "Legacy".to_string(), + app: "live".to_string(), + publish_key: "a".to_string(), + play_key: "b".to_string(), + stats_key: "c".to_string(), + enabled: true, + created_at: now_ts(), + }; + assert!(db.stream_add(&s).is_ok()); + + assert!(matches!( + db.stream_find_by_publish_key("a"), + DbLookup::Missing + )); + assert!(matches!( + db.stream_find_by_publish_key_any("a"), + DbLookup::Missing + )); + assert!(matches!( + db.viewer_find_by_play_key("b"), + DbLookup::Missing + )); + assert!(matches!( + db.stream_find_by_stats_key("c"), + DbLookup::Missing + )); + } } diff --git a/src/http.rs b/src/http.rs index 1a59b8b..abcffcd 100644 --- a/src/http.rs +++ b/src/http.rs @@ -206,21 +206,9 @@ fn is_valid_stream_key_part(value: &str) -> bool { && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) } -/// Minimum length for operator-supplied publish/play/stats keys. Shorter custom -/// keys are trivially brute-forced over the unrate-limited RTMP auth path. -const MIN_ACCESS_KEY_LEN: usize = 32; - /// Publish/play/stats keys: safe ASCII, no slashes, minimum entropy via length. fn is_valid_access_key(value: &str) -> bool { - if value.len() < MIN_ACCESS_KEY_LEN || value.len() > 63 { - return false; - } - let mut chars = value.chars(); - let Some(first) = chars.next() else { - return false; - }; - first.is_ascii_alphanumeric() - && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) + crate::keygen::is_valid_access_key(value) } fn trim_optional_string(value: Option) -> Option { @@ -249,7 +237,7 @@ fn resolve_or_generate_access_key( } } -const ACCESS_KEY_VALIDATION_MSG: &str = "Key must be 32-63 characters, start with a letter or number, and use only letters, numbers, dots, underscores, or hyphens"; +const ACCESS_KEY_VALIDATION_MSG: &str = crate::keygen::ACCESS_KEY_VALIDATION_MSG; fn access_keys_must_be_unique(keys: &[&str]) -> bool { let mut seen = HashSet::with_capacity(keys.len()); diff --git a/src/keygen.rs b/src/keygen.rs index ef87a32..4bb8cc5 100644 --- a/src/keygen.rs +++ b/src/keygen.rs @@ -18,6 +18,38 @@ pub const PREFIX_STATS_KEY: &str = "sts_"; /// Prefix for configured viewer-slot row ids (panel-managed play access). pub const PREFIX_VIEWER_ID: &str = "vi_"; +/// Minimum length for publish/play/stats keys used at runtime. Shorter keys stored +/// in legacy databases are rejected on RTMP and public stats paths. +pub const MIN_ACCESS_KEY_LEN: usize = 32; + +/// Publish/play/stats keys: safe ASCII, no slashes, minimum entropy via length. +pub fn is_valid_access_key(value: &str) -> bool { + if value.len() < MIN_ACCESS_KEY_LEN || value.len() > 63 { + return false; + } + let mut chars = value.chars(); + let Some(first) = chars.next() else { + return false; + }; + first.is_ascii_alphanumeric() + && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) +} + +pub const ACCESS_KEY_VALIDATION_MSG: &str = + "Key must be 32-63 characters, start with a letter or number, and use only letters, numbers, dots, underscores, or hyphens"; + +#[cfg(test)] +pub fn test_pad_access_key(value: &str) -> String { + if is_valid_access_key(value) { + return value.to_string(); + } + let mut padded = value.to_string(); + while padded.len() < MIN_ACCESS_KEY_LEN { + padded.push('x'); + } + padded.chars().take(63).collect() +} + fn keygen_with_entropy(prefix: &str, entropy_bytes: usize) -> Result { let mut rnd = vec![0u8; entropy_bytes]; SysRng @@ -65,4 +97,11 @@ mod tests { assert_eq!(token.len(), API_TOKEN_ENTROPY_BYTES * 2); assert!(token.chars().all(|c| c.is_ascii_hexdigit())); } + + #[test] + fn access_key_validation_enforces_minimum_length() { + assert!(is_valid_access_key("live.main_1_with_sufficient_length_ok")); + assert!(!is_valid_access_key("too_short")); + assert!(!is_valid_access_key("a")); + } } diff --git a/src/rtmp_bridge.rs b/src/rtmp_bridge.rs index 76a7925..89bac5b 100644 --- a/src/rtmp_bridge.rs +++ b/src/rtmp_bridge.rs @@ -894,17 +894,18 @@ mod tests { id: id.to_string(), name: format!("{id} name"), app: "live".to_string(), - publish_key: pub_key.to_string(), - play_key: play_key.to_string(), - stats_key: format!("st_{id}"), + publish_key: crate::keygen::test_pad_access_key(pub_key), + play_key: crate::keygen::test_pad_access_key(play_key), + stats_key: crate::keygen::test_pad_access_key(&format!("stats_{id}")), enabled: true, created_at: crate::db::now_ts(), } } - fn add_stream_with_player(db: &Db, id: &str, pub_key: &str, play_key: &str) { - db.stream_add(&sample_stream(id, pub_key, play_key)) - .unwrap(); + fn add_stream_with_player(db: &Db, id: &str, pub_key: &str, play_key: &str) -> crate::db::Stream { + let s = sample_stream(id, pub_key, play_key); + db.stream_add(&s).unwrap(); + s } #[test] @@ -1057,23 +1058,23 @@ mod tests { #[test] fn publish_and_play_reject_stream_marked_for_deletion() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let deleted = Arc::new(Mutex::new(HashSet::new())); deleted.lock().insert("s1".to_string()); let bridge = DbRtmpBridge::new(db, deleted); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k").is_err()); + assert!(bridge.authorize_publish(1, "live", &s.publish_key).is_err()); bridge.on_close(1); bridge.on_connect(2, "127.0.0.1:1000"); - assert!(bridge.authorize_play(2, "live", "pl_k").is_err()); + assert!(bridge.authorize_play(2, "live", &s.play_key).is_err()); } #[test] fn publish_with_valid_key_for_disabled_stream_does_not_burn_auth_budget() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); assert!(db.stream_disable("s1").unwrap()); let bridge = test_bridge(db); let ip = "203.0.113.8:1935"; @@ -1084,7 +1085,7 @@ mod tests { // a brute-force guess would. for conn in 0..(RTMP_AUTH_MAX_FAILURES as u64 + 2) { bridge.on_connect(conn, ip); - assert!(bridge.authorize_publish(conn, "live", "pub_k").is_err()); + assert!(bridge.authorize_publish(conn, "live", &s.publish_key).is_err()); bridge.on_close(conn); } @@ -1102,33 +1103,33 @@ mod tests { #[test] fn publish_rejects_key_for_wrong_app() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "other", "pub_k").is_err()); + assert!(bridge.authorize_publish(1, "other", &s.publish_key).is_err()); assert_eq!(db.publisher_list(Some("s1")).len(), 0); } #[test] fn play_rejects_key_for_wrong_app() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_play(1, "other", "pl_k").is_err()); + assert!(bridge.authorize_play(1, "other", &s.play_key).is_err()); assert_eq!(db.player_list(Some("s1")).len(), 0); } #[test] fn publish_then_close_deactivates_publisher() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s.publish_key).is_ok()); assert_eq!(db.publisher_list(Some("s1")).len(), 1); bridge.on_close(1); @@ -1138,14 +1139,14 @@ mod tests { #[test] fn close_only_affects_its_own_connection() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); - add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); + let s1 = add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); + let s2 = add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); bridge.on_connect(2, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k1").is_ok()); - assert!(bridge.authorize_publish(2, "live", "pub_k2").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s1.publish_key).is_ok()); + assert!(bridge.authorize_publish(2, "live", &s2.publish_key).is_ok()); bridge.on_close(1); assert_eq!(db.publisher_list(Some("s1")).len(), 0); @@ -1155,24 +1156,24 @@ mod tests { #[test] fn authorize_publish_rejects_second_publisher() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s.publish_key).is_ok()); bridge.on_connect(2, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(2, "live", "pub_k").is_err()); + assert!(bridge.authorize_publish(2, "live", &s.publish_key).is_err()); } #[test] fn on_connect_preserves_prior_authorize_publish_state() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); // publish callback can run during poll() before the poll-loop on_connect. - assert!(bridge.authorize_publish(1, "live", "pub_k").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s.publish_key).is_ok()); bridge.on_connect(1, "127.0.0.1:1000"); assert!(bridge.has_publisher(1)); @@ -1192,13 +1193,13 @@ mod tests { #[test] fn authorize_publish_switching_streams_deactivates_prior_publisher() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); - add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); + let s1 = add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); + let s2 = add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k1").is_ok()); - assert!(bridge.authorize_publish(1, "live", "pub_k2").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s1.publish_key).is_ok()); + assert!(bridge.authorize_publish(1, "live", &s2.publish_key).is_ok()); assert_eq!(db.publisher_list(Some("s1")).len(), 0); assert_eq!(db.publisher_list(Some("s2")).len(), 1); @@ -1207,16 +1208,16 @@ mod tests { #[test] fn authorize_publish_failed_switch_keeps_prior_publisher_active() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); - add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); + let s1 = add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); + let s2 = add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); bridge.on_connect(2, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k1").is_ok()); - assert!(bridge.authorize_publish(2, "live", "pub_k2").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s1.publish_key).is_ok()); + assert!(bridge.authorize_publish(2, "live", &s2.publish_key).is_ok()); - assert!(bridge.authorize_publish(1, "live", "pub_k2").is_err()); + assert!(bridge.authorize_publish(1, "live", &s2.publish_key).is_err()); assert!(bridge.has_publisher(1)); assert_eq!(db.publisher_list(Some("s1")).len(), 1); @@ -1226,13 +1227,13 @@ mod tests { #[test] fn on_play_switching_streams_deactivates_prior_player() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); - add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); + let s1 = add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); + let s2 = add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_play(1, "live", "pl_k1").is_ok()); - assert!(bridge.authorize_play(1, "live", "pl_k2").is_ok()); + assert!(bridge.authorize_play(1, "live", &s1.play_key).is_ok()); + assert!(bridge.authorize_play(1, "live", &s2.play_key).is_ok()); assert_eq!(db.player_list(Some("s1")).len(), 0); assert_eq!(db.player_list(Some("s2")).len(), 1); @@ -1244,17 +1245,17 @@ mod tests { #[test] fn player_replacement_stats_reset_is_not_consumed_by_publisher_stats() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); - add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); + let s1 = add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); + let s2 = add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k1").is_ok()); - assert!(bridge.authorize_play(1, "live", "pl_k1").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s1.publish_key).is_ok()); + assert!(bridge.authorize_play(1, "live", &s1.play_key).is_ok()); bridge.update_publisher_stats(1, 1_000, "avc1", "mp4a", PublisherStreamMetadata::default()); bridge.update_player_stats(1, 2_000); - assert!(bridge.authorize_play(1, "live", "pl_k2").is_ok()); + assert!(bridge.authorize_play(1, "live", &s2.play_key).is_ok()); bridge.update_publisher_stats(1, 1_500, "avc1", "mp4a", PublisherStreamMetadata::default()); { @@ -1273,11 +1274,11 @@ mod tests { #[test] fn update_player_stats_persists_bytes_out() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_play(1, "live", "pl_k").is_ok()); + assert!(bridge.authorize_play(1, "live", &s.play_key).is_ok()); bridge.update_player_stats(1, 4096); let players = db.player_list(Some("s1")); @@ -1288,19 +1289,19 @@ mod tests { #[test] fn play_rejects_when_connection_cap_reached() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); - let DbLookup::Ok(viewer) = db.viewer_find_by_play_key("pl_k") else { + let DbLookup::Ok(viewer) = db.viewer_find_by_play_key(&s.play_key) else { panic!("viewer not found"); }; for conn in 1..=crate::db::MAX_CONNECTIONS_PER_PLAY_KEY as u64 { bridge.on_connect(conn, "127.0.0.1:1000"); - assert!(bridge.authorize_play(conn, "live", "pl_k").is_ok()); + assert!(bridge.authorize_play(conn, "live", &s.play_key).is_ok()); } bridge.on_connect(99, "127.0.0.1:1000"); - assert!(bridge.authorize_play(99, "live", "pl_k").is_err()); + assert!(bridge.authorize_play(99, "live", &s.play_key).is_err()); assert_eq!( db.player_list(Some(&viewer.stream_id)) .iter() From b56502d4c6544c28bc13d34e6f5f19f5bed394e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:08:39 +0000 Subject: [PATCH 2/3] Update Cargo.lock --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc7c851..a1ed983 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1609,9 +1609,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", From 72763145707b3c6241e6f50183c88f15dc7933f5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 15 Jul 2026 05:01:49 +0000 Subject: [PATCH 3/3] style: apply rustfmt after security key validation changes cargo fmt --check failed on PR #84 due to formatting drift in db.rs, keygen.rs, and rtmp_bridge.rs introduced by the legacy short-key guard. Co-authored-by: Alexander Wagner --- src/db.rs | 19 +++---------------- src/keygen.rs | 3 +-- src/rtmp_bridge.rs | 25 +++++++++++++++++++++---- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/db.rs b/src/db.rs index 6cab17c..ea9dd9c 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1211,12 +1211,7 @@ mod tests { #[test] fn publishers_players_and_stats() { let db = Db::open(":memory:").unwrap(); - let s = sample_stream( - "stream1", - "pub_key_123", - "pl_key_456", - "st_key_789", - ); + let s = sample_stream("stream1", "pub_key_123", "pl_key_456", "st_key_789"); db.stream_add(&s).unwrap(); let mut p = Publisher { @@ -1413,12 +1408,7 @@ mod tests { #[test] fn player_try_acquire_enforces_per_key_connection_cap() { let db = Db::open(":memory:").unwrap(); - let s = sample_stream( - "stream1", - "pub_key_123", - "pl_key_456", - "st_key_789", - ); + let s = sample_stream("stream1", "pub_key_123", "pl_key_456", "st_key_789"); db.stream_add(&s).unwrap(); let DbLookup::Ok(viewer) = db.viewer_find_by_play_key(&s.play_key) else { panic!("viewer not found"); @@ -1470,10 +1460,7 @@ mod tests { db.stream_find_by_publish_key_any("a"), DbLookup::Missing )); - assert!(matches!( - db.viewer_find_by_play_key("b"), - DbLookup::Missing - )); + assert!(matches!(db.viewer_find_by_play_key("b"), DbLookup::Missing)); assert!(matches!( db.stream_find_by_stats_key("c"), DbLookup::Missing diff --git a/src/keygen.rs b/src/keygen.rs index 4bb8cc5..e871cd9 100644 --- a/src/keygen.rs +++ b/src/keygen.rs @@ -35,8 +35,7 @@ pub fn is_valid_access_key(value: &str) -> bool { && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) } -pub const ACCESS_KEY_VALIDATION_MSG: &str = - "Key must be 32-63 characters, start with a letter or number, and use only letters, numbers, dots, underscores, or hyphens"; +pub const ACCESS_KEY_VALIDATION_MSG: &str = "Key must be 32-63 characters, start with a letter or number, and use only letters, numbers, dots, underscores, or hyphens"; #[cfg(test)] pub fn test_pad_access_key(value: &str) -> String { diff --git a/src/rtmp_bridge.rs b/src/rtmp_bridge.rs index 89bac5b..c4c409b 100644 --- a/src/rtmp_bridge.rs +++ b/src/rtmp_bridge.rs @@ -902,7 +902,12 @@ mod tests { } } - fn add_stream_with_player(db: &Db, id: &str, pub_key: &str, play_key: &str) -> crate::db::Stream { + fn add_stream_with_player( + db: &Db, + id: &str, + pub_key: &str, + play_key: &str, + ) -> crate::db::Stream { let s = sample_stream(id, pub_key, play_key); db.stream_add(&s).unwrap(); s @@ -1085,7 +1090,11 @@ mod tests { // a brute-force guess would. for conn in 0..(RTMP_AUTH_MAX_FAILURES as u64 + 2) { bridge.on_connect(conn, ip); - assert!(bridge.authorize_publish(conn, "live", &s.publish_key).is_err()); + assert!( + bridge + .authorize_publish(conn, "live", &s.publish_key) + .is_err() + ); bridge.on_close(conn); } @@ -1107,7 +1116,11 @@ mod tests { let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "other", &s.publish_key).is_err()); + assert!( + bridge + .authorize_publish(1, "other", &s.publish_key) + .is_err() + ); assert_eq!(db.publisher_list(Some("s1")).len(), 0); } @@ -1217,7 +1230,11 @@ mod tests { assert!(bridge.authorize_publish(1, "live", &s1.publish_key).is_ok()); assert!(bridge.authorize_publish(2, "live", &s2.publish_key).is_ok()); - assert!(bridge.authorize_publish(1, "live", &s2.publish_key).is_err()); + assert!( + bridge + .authorize_publish(1, "live", &s2.publish_key) + .is_err() + ); assert!(bridge.has_publisher(1)); assert_eq!(db.publisher_list(Some("s1")).len(), 1);