Dispatch read one byte of the code, so a code with a non-zero high byte was handled as its low byte. Message::get_message_code now reads the whole little-endian uint32 and handlers register by u32. PeerInit and PierceFirewall, the only frames with a one-byte code, get their own accessor for the listener's first frame.
310 lines
10 KiB
Rust
310 lines
10 KiB
Rust
use crate::{
|
|
actor::server_actor::ServerMessage,
|
|
message::{Message, MessageHandler},
|
|
types::{RoomUserStats, UserStatus},
|
|
};
|
|
use std::sync::mpsc::Sender;
|
|
|
|
pub struct JoinRoomHandler;
|
|
|
|
impl MessageHandler<ServerMessage> for JoinRoomHandler {
|
|
fn get_code(&self) -> u32 {
|
|
14
|
|
}
|
|
|
|
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
|
// JoinRoom (code 14): room name, the member usernames, then the
|
|
// parallel per-member stat vectors.
|
|
let room = message.read_string();
|
|
let users = read_strings(message);
|
|
let stats = parse_member_stats(message, &users);
|
|
let _ = sender.send(ServerMessage::RoomJoined {
|
|
room: room.clone(),
|
|
users,
|
|
});
|
|
if !stats.is_empty() {
|
|
let _ = sender.send(ServerMessage::RoomMemberStats { room, stats });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read a length-prefixed vector of strings, stopping early if the count
|
|
/// outruns the payload: a bogus (possibly hostile) count must not spin us
|
|
/// into an OOM allocation loop. A 4-byte floor is the smallest a
|
|
/// length-prefixed string can be.
|
|
fn read_strings(message: &mut Message) -> Vec<String> {
|
|
let count = message.read_int32();
|
|
let mut values = Vec::new();
|
|
for _ in 0..count {
|
|
if message.get_pointer() + 4 > message.get_size() {
|
|
break;
|
|
}
|
|
values.push(message.read_string());
|
|
}
|
|
values
|
|
}
|
|
|
|
/// Read a length-prefixed vector of 32-bit values, bounded the same way.
|
|
fn read_int32s(message: &mut Message) -> Vec<u32> {
|
|
let count = message.read_int32();
|
|
let mut values = Vec::new();
|
|
for _ in 0..count {
|
|
if message.get_pointer() + 4 > message.get_size() {
|
|
break;
|
|
}
|
|
values.push(message.read_int32());
|
|
}
|
|
values
|
|
}
|
|
|
|
/// Parse the per-member statistics that follow the membership list of a
|
|
/// `JoinRoom` (code 14) reply: statuses, then a record of share statistics per
|
|
/// member, then whether each member's upload slots are full, then country
|
|
/// codes.
|
|
///
|
|
/// Each vector is independently length-prefixed and a server may send fewer
|
|
/// entries than there are members (or, on an older server, omit the country
|
|
/// vector entirely), so every field is looked up positionally and defaults
|
|
/// when absent rather than shifting the whole roster.
|
|
#[must_use]
|
|
fn parse_member_stats(
|
|
message: &mut Message,
|
|
users: &[String],
|
|
) -> Vec<RoomUserStats> {
|
|
let statuses = read_int32s(message);
|
|
|
|
let stat_count = message.read_int32();
|
|
let mut shares = Vec::new();
|
|
for _ in 0..stat_count {
|
|
// A share record is 20 bytes: speed, an obsolete 64-bit upload
|
|
// number, files and folders.
|
|
if message.get_pointer() + 20 > message.get_size() {
|
|
break;
|
|
}
|
|
let average_speed = message.read_int32();
|
|
let _upload_number = message.read_int64(); // obsolete
|
|
let shared_files = message.read_int32();
|
|
let shared_folders = message.read_int32();
|
|
shares.push((average_speed, shared_files, shared_folders));
|
|
}
|
|
|
|
let slots_full = read_int32s(message);
|
|
let countries = read_strings(message);
|
|
|
|
users
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, username)| {
|
|
let (average_speed, shared_files, shared_folders) =
|
|
shares.get(i).copied().unwrap_or((0, 0, 0));
|
|
RoomUserStats {
|
|
username: username.clone(),
|
|
status: UserStatus::from_code(
|
|
statuses.get(i).copied().unwrap_or(0),
|
|
),
|
|
average_speed,
|
|
shared_files,
|
|
shared_folders,
|
|
slots_full: slots_full.get(i).copied().unwrap_or(0) != 0,
|
|
country: countries.get(i).cloned().filter(|c| !c.is_empty()),
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::message::framed;
|
|
|
|
#[test]
|
|
fn hostile_user_count_does_not_hang() {
|
|
// room="" then user_count=u32::MAX with no usernames: must return
|
|
// promptly instead of looping ~4 billion times.
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let mut message = framed(|m| {
|
|
m.write_string("").write_int32(u32::MAX);
|
|
});
|
|
|
|
JoinRoomHandler.handle(&mut message, tx);
|
|
match rx.try_recv() {
|
|
Ok(ServerMessage::RoomJoined { users, .. }) => {
|
|
assert!(users.is_empty());
|
|
}
|
|
other => panic!("unexpected: {other:?}"),
|
|
}
|
|
}
|
|
|
|
/// A full JoinRoom payload: room, members, then the four parallel stat
|
|
/// vectors the server sends for them.
|
|
fn join_payload() -> Message {
|
|
framed(|m| {
|
|
m.write_string("nicotine")
|
|
.write_int32(2)
|
|
.write_string("alice")
|
|
.write_string("bob")
|
|
// statuses
|
|
.write_int32(2)
|
|
.write_int32(2)
|
|
.write_int32(1)
|
|
// share records
|
|
.write_int32(2)
|
|
.write_int32(1024)
|
|
.write_int64(0)
|
|
.write_int32(4321)
|
|
.write_int32(77)
|
|
.write_int32(50)
|
|
.write_int64(0)
|
|
.write_int32(10)
|
|
.write_int32(2)
|
|
// slots full
|
|
.write_int32(2)
|
|
.write_int32(1)
|
|
.write_int32(0)
|
|
// countries
|
|
.write_int32(2)
|
|
.write_string("NL")
|
|
.write_string("");
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn member_stats_are_parsed_alongside_the_roster() {
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
JoinRoomHandler.handle(&mut join_payload(), tx);
|
|
|
|
assert!(matches!(
|
|
rx.try_recv(),
|
|
Ok(ServerMessage::RoomJoined { .. })
|
|
));
|
|
match rx.try_recv() {
|
|
Ok(ServerMessage::RoomMemberStats { room, stats }) => {
|
|
assert_eq!(room, "nicotine");
|
|
assert_eq!(
|
|
stats,
|
|
vec![
|
|
RoomUserStats {
|
|
username: "alice".to_string(),
|
|
status: UserStatus::Online,
|
|
average_speed: 1024,
|
|
shared_files: 4321,
|
|
shared_folders: 77,
|
|
slots_full: true,
|
|
country: Some("NL".to_string()),
|
|
},
|
|
RoomUserStats {
|
|
username: "bob".to_string(),
|
|
status: UserStatus::Away,
|
|
average_speed: 50,
|
|
shared_files: 10,
|
|
shared_folders: 2,
|
|
slots_full: false,
|
|
country: None,
|
|
},
|
|
]
|
|
);
|
|
}
|
|
other => panic!("unexpected: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_server_that_omits_the_stat_vectors_still_yields_a_roster() {
|
|
// Nothing after the membership list: every stat reads as a default
|
|
// rather than shifting the roster or dropping members.
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let mut message = framed(|m| {
|
|
m.write_string("jazz").write_int32(1).write_string("alice");
|
|
});
|
|
|
|
JoinRoomHandler.handle(&mut message, tx);
|
|
assert!(matches!(
|
|
rx.try_recv(),
|
|
Ok(ServerMessage::RoomJoined { .. })
|
|
));
|
|
match rx.try_recv() {
|
|
Ok(ServerMessage::RoomMemberStats { stats, .. }) => {
|
|
assert_eq!(stats.len(), 1);
|
|
assert_eq!(stats[0].username, "alice");
|
|
assert_eq!(stats[0].average_speed, 0);
|
|
assert_eq!(stats[0].country, None);
|
|
}
|
|
other => panic!("unexpected: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn short_stat_vectors_do_not_shift_the_roster() {
|
|
// Stats for the first member only: the second must still be reported,
|
|
// with defaults, rather than inheriting the first member's figures.
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let mut message = framed(|m| {
|
|
m.write_string("jazz")
|
|
.write_int32(2)
|
|
.write_string("alice")
|
|
.write_string("bob")
|
|
.write_int32(1)
|
|
.write_int32(2)
|
|
.write_int32(1)
|
|
.write_int32(1024)
|
|
.write_int64(0)
|
|
.write_int32(9)
|
|
.write_int32(1)
|
|
.write_int32(0)
|
|
.write_int32(0);
|
|
});
|
|
|
|
JoinRoomHandler.handle(&mut message, tx);
|
|
let _ = rx.try_recv();
|
|
match rx.try_recv() {
|
|
Ok(ServerMessage::RoomMemberStats { stats, .. }) => {
|
|
assert_eq!(stats.len(), 2);
|
|
assert_eq!(stats[0].shared_files, 9);
|
|
assert_eq!(stats[1].username, "bob");
|
|
assert_eq!(stats[1].shared_files, 0);
|
|
assert_eq!(stats[1].status, UserStatus::Offline);
|
|
}
|
|
other => panic!("unexpected: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_stat_counts_do_not_hang_or_overallocate() {
|
|
// Vectors claiming ~4 billion entries in a tiny frame must return
|
|
// promptly, bounded by the payload.
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let mut message = framed(|m| {
|
|
m.write_string("jazz")
|
|
.write_int32(1)
|
|
.write_string("alice")
|
|
.write_int32(u32::MAX);
|
|
});
|
|
|
|
JoinRoomHandler.handle(&mut message, tx);
|
|
assert!(matches!(
|
|
rx.try_recv(),
|
|
Ok(ServerMessage::RoomJoined { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn forwards_room_and_member_list() {
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let mut message = framed(|m| {
|
|
m.write_string("nicotine")
|
|
.write_int32(2)
|
|
.write_string("alice")
|
|
.write_string("bob");
|
|
});
|
|
|
|
JoinRoomHandler.handle(&mut message, tx);
|
|
match rx.try_recv() {
|
|
Ok(ServerMessage::RoomJoined { room, users }) => {
|
|
assert_eq!(room, "nicotine");
|
|
assert_eq!(users, vec!["alice", "bob"]);
|
|
}
|
|
other => panic!("unexpected: {other:?}"),
|
|
}
|
|
}
|
|
}
|