feat: 初始化项目基础架构和核心功能
- 添加项目基础结构:Cargo.toml、.gitignore、设备UID和密钥文件 - 实现前端Vue3项目结构:路由、登录页面、设备管理页面 - 添加核心协议定义(crates/protocol):设备状态、资产、USB事件等 - 实现客户端监控模块:系统状态收集、资产收集 - 实现服务端基础API和插件系统 - 添加数据库迁移脚本:设备管理、资产跟踪、告警系统等 - 实现前端设备状态展示和基本交互 - 添加使用时长统计和水印功能插件
This commit is contained in:
12
crates/protocol/Cargo.toml
Normal file
12
crates/protocol/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "csm-protocol"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
108
crates/protocol/src/device.rs
Normal file
108
crates/protocol/src/device.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Real-time device status report
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DeviceStatus {
|
||||
pub device_uid: String,
|
||||
pub cpu_usage: f64,
|
||||
pub memory_usage: f64,
|
||||
pub memory_total_mb: u64,
|
||||
pub disk_usage: f64,
|
||||
pub disk_total_mb: u64,
|
||||
pub network_rx_rate: u64,
|
||||
pub network_tx_rate: u64,
|
||||
pub running_procs: u32,
|
||||
pub top_processes: Vec<ProcessInfo>,
|
||||
}
|
||||
|
||||
/// Top process information
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ProcessInfo {
|
||||
pub name: String,
|
||||
pub pid: u32,
|
||||
pub cpu_usage: f64,
|
||||
pub memory_mb: u64,
|
||||
}
|
||||
|
||||
/// Hardware asset information
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct HardwareAsset {
|
||||
pub device_uid: String,
|
||||
pub cpu_model: String,
|
||||
pub cpu_cores: u32,
|
||||
pub memory_total_mb: u64,
|
||||
pub disk_model: String,
|
||||
pub disk_total_mb: u64,
|
||||
pub gpu_model: Option<String>,
|
||||
pub motherboard: Option<String>,
|
||||
pub serial_number: Option<String>,
|
||||
}
|
||||
|
||||
/// Software asset information
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SoftwareAsset {
|
||||
pub device_uid: String,
|
||||
pub name: String,
|
||||
pub version: Option<String>,
|
||||
pub publisher: Option<String>,
|
||||
pub install_date: Option<String>,
|
||||
pub install_path: Option<String>,
|
||||
}
|
||||
|
||||
/// USB device event
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UsbEvent {
|
||||
pub device_uid: String,
|
||||
pub event_type: UsbEventType,
|
||||
pub vendor_id: Option<String>,
|
||||
pub product_id: Option<String>,
|
||||
pub serial: Option<String>,
|
||||
pub device_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UsbEventType {
|
||||
Inserted,
|
||||
Removed,
|
||||
Blocked,
|
||||
}
|
||||
|
||||
/// Asset change event
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AssetChange {
|
||||
pub device_uid: String,
|
||||
pub change_type: AssetChangeType,
|
||||
pub change_detail: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssetChangeType {
|
||||
Hardware,
|
||||
SoftwareAdded,
|
||||
SoftwareRemoved,
|
||||
}
|
||||
|
||||
/// USB policy (Server → Client)
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UsbPolicy {
|
||||
pub policy_id: i64,
|
||||
pub policy_type: UsbPolicyType,
|
||||
pub allowed_devices: Vec<UsbDevicePattern>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsbPolicyType {
|
||||
AllBlock,
|
||||
Whitelist,
|
||||
Blacklist,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UsbDevicePattern {
|
||||
pub vendor_id: Option<String>,
|
||||
pub product_id: Option<String>,
|
||||
pub serial: Option<String>,
|
||||
}
|
||||
27
crates/protocol/src/lib.rs
Normal file
27
crates/protocol/src/lib.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
pub mod message;
|
||||
pub mod device;
|
||||
|
||||
// Re-export constants from message module
|
||||
pub use message::{MAGIC, PROTOCOL_VERSION, FRAME_HEADER_SIZE, MAX_PAYLOAD_SIZE};
|
||||
|
||||
// Core frame & message types
|
||||
pub use message::{
|
||||
Frame, FrameError, MessageType,
|
||||
RegisterRequest, RegisterResponse, ClientConfig,
|
||||
HeartbeatPayload, TaskExecutePayload, ConfigUpdateType,
|
||||
};
|
||||
|
||||
// Device status & asset types
|
||||
pub use device::{
|
||||
DeviceStatus, ProcessInfo, HardwareAsset, SoftwareAsset,
|
||||
UsbEvent, UsbEventType, AssetChange, AssetChangeType,
|
||||
UsbPolicy, UsbPolicyType, UsbDevicePattern,
|
||||
};
|
||||
|
||||
// Plugin message payloads
|
||||
pub use message::{
|
||||
WebAccessLogEntry, UsageDailyReport, AppUsageEntry,
|
||||
SoftwareViolationReport, UsbFileOpEntry,
|
||||
WatermarkConfigPayload, PluginControlPayload,
|
||||
UsbPolicyPayload, UsbDeviceRule,
|
||||
};
|
||||
417
crates/protocol/src/message.rs
Normal file
417
crates/protocol/src/message.rs
Normal file
@@ -0,0 +1,417 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Protocol magic bytes: "CSM\0"
|
||||
pub const MAGIC: [u8; 4] = [0x43, 0x53, 0x4D, 0x00];
|
||||
|
||||
/// Current protocol version
|
||||
pub const PROTOCOL_VERSION: u8 = 0x01;
|
||||
|
||||
/// Frame header size: magic(4) + version(1) + type(1) + length(4)
|
||||
pub const FRAME_HEADER_SIZE: usize = 10;
|
||||
|
||||
/// Maximum payload size: 4 MB — prevents memory exhaustion from malicious frames
|
||||
pub const MAX_PAYLOAD_SIZE: usize = 4 * 1024 * 1024;
|
||||
|
||||
/// Binary message types for client-server communication
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MessageType {
|
||||
// Client → Server (Core)
|
||||
Heartbeat = 0x01,
|
||||
Register = 0x02,
|
||||
StatusReport = 0x03,
|
||||
AssetReport = 0x04,
|
||||
AssetChange = 0x05,
|
||||
UsbEvent = 0x06,
|
||||
AlertAck = 0x07,
|
||||
|
||||
// Server → Client (Core)
|
||||
RegisterResponse = 0x08,
|
||||
PolicyUpdate = 0x10,
|
||||
ConfigUpdate = 0x11,
|
||||
TaskExecute = 0x12,
|
||||
|
||||
// Plugin: Web Filter (上网拦截)
|
||||
WebFilterRuleUpdate = 0x20,
|
||||
WebAccessLog = 0x21,
|
||||
|
||||
// Plugin: Usage Timer (时长记录)
|
||||
UsageReport = 0x30,
|
||||
AppUsageReport = 0x31,
|
||||
|
||||
// Plugin: Software Blocker (软件禁止安装)
|
||||
SoftwareBlacklist = 0x40,
|
||||
SoftwareViolation = 0x41,
|
||||
|
||||
// Plugin: Popup Blocker (弹窗拦截)
|
||||
PopupRules = 0x50,
|
||||
|
||||
// Plugin: USB File Audit (U盘文件操作记录)
|
||||
UsbFileOp = 0x60,
|
||||
|
||||
// Plugin: Screen Watermark (水印管理)
|
||||
WatermarkConfig = 0x70,
|
||||
|
||||
// Plugin: USB Policy (U盘管控策略)
|
||||
UsbPolicyUpdate = 0x71,
|
||||
|
||||
// Plugin control
|
||||
PluginEnable = 0x80,
|
||||
PluginDisable = 0x81,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for MessageType {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x01 => Ok(Self::Heartbeat),
|
||||
0x02 => Ok(Self::Register),
|
||||
0x03 => Ok(Self::StatusReport),
|
||||
0x04 => Ok(Self::AssetReport),
|
||||
0x05 => Ok(Self::AssetChange),
|
||||
0x06 => Ok(Self::UsbEvent),
|
||||
0x07 => Ok(Self::AlertAck),
|
||||
0x08 => Ok(Self::RegisterResponse),
|
||||
0x10 => Ok(Self::PolicyUpdate),
|
||||
0x11 => Ok(Self::ConfigUpdate),
|
||||
0x12 => Ok(Self::TaskExecute),
|
||||
0x20 => Ok(Self::WebFilterRuleUpdate),
|
||||
0x21 => Ok(Self::WebAccessLog),
|
||||
0x30 => Ok(Self::UsageReport),
|
||||
0x31 => Ok(Self::AppUsageReport),
|
||||
0x40 => Ok(Self::SoftwareBlacklist),
|
||||
0x41 => Ok(Self::SoftwareViolation),
|
||||
0x50 => Ok(Self::PopupRules),
|
||||
0x60 => Ok(Self::UsbFileOp),
|
||||
0x70 => Ok(Self::WatermarkConfig),
|
||||
0x71 => Ok(Self::UsbPolicyUpdate),
|
||||
0x80 => Ok(Self::PluginEnable),
|
||||
0x81 => Ok(Self::PluginDisable),
|
||||
_ => Err(format!("Unknown message type: 0x{:02X}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wire-format frame for transmission over TCP
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Frame {
|
||||
pub version: u8,
|
||||
pub msg_type: MessageType,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// Create a new frame with the current protocol version
|
||||
pub fn new(msg_type: MessageType, payload: Vec<u8>) -> Self {
|
||||
Self {
|
||||
version: PROTOCOL_VERSION,
|
||||
msg_type,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new frame with JSON-serialized payload
|
||||
pub fn new_json<T: Serialize>(msg_type: MessageType, data: &T) -> anyhow::Result<Self> {
|
||||
let payload = serde_json::to_vec(data)?;
|
||||
Ok(Self::new(msg_type, payload))
|
||||
}
|
||||
|
||||
/// Encode frame to bytes for transmission
|
||||
/// Format: MAGIC(4) + VERSION(1) + TYPE(1) + LENGTH(4) + PAYLOAD(var)
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(FRAME_HEADER_SIZE + self.payload.len());
|
||||
buf.extend_from_slice(&MAGIC);
|
||||
buf.push(self.version);
|
||||
buf.push(self.msg_type as u8);
|
||||
buf.extend_from_slice(&(self.payload.len() as u32).to_be_bytes());
|
||||
buf.extend_from_slice(&self.payload);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Decode frame from bytes. Returns Ok(Some(frame)) when a complete frame is available.
|
||||
pub fn decode(data: &[u8]) -> Result<Option<Frame>, FrameError> {
|
||||
if data.len() < FRAME_HEADER_SIZE {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if data[0..4] != MAGIC {
|
||||
return Err(FrameError::InvalidMagic);
|
||||
}
|
||||
|
||||
let version = data[4];
|
||||
let msg_type_byte = data[5];
|
||||
let payload_len = u32::from_be_bytes([data[6], data[7], data[8], data[9]]) as usize;
|
||||
|
||||
if payload_len > MAX_PAYLOAD_SIZE {
|
||||
return Err(FrameError::PayloadTooLarge(payload_len));
|
||||
}
|
||||
|
||||
if data.len() < FRAME_HEADER_SIZE + payload_len {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let msg_type = MessageType::try_from(msg_type_byte)
|
||||
.map_err(|e| FrameError::UnknownMessageType(msg_type_byte, e))?;
|
||||
|
||||
let payload = data[FRAME_HEADER_SIZE..FRAME_HEADER_SIZE + payload_len].to_vec();
|
||||
|
||||
Ok(Some(Frame {
|
||||
version,
|
||||
msg_type,
|
||||
payload,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Deserialize the payload as JSON
|
||||
pub fn decode_payload<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
|
||||
serde_json::from_slice(&self.payload)
|
||||
}
|
||||
|
||||
/// Total encoded size of this frame
|
||||
pub fn encoded_size(&self) -> usize {
|
||||
FRAME_HEADER_SIZE + self.payload.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FrameError {
|
||||
#[error("Invalid magic bytes in frame header")]
|
||||
InvalidMagic,
|
||||
#[error("Unknown message type: 0x{0:02X} - {1}")]
|
||||
UnknownMessageType(u8, String),
|
||||
#[error("Payload too large: {0} bytes (max {})", MAX_PAYLOAD_SIZE)]
|
||||
PayloadTooLarge(usize),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
// ==================== Core Message Payloads ====================
|
||||
|
||||
/// Registration request payload
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
pub device_uid: String,
|
||||
pub hostname: String,
|
||||
pub registration_token: String,
|
||||
pub os_version: String,
|
||||
pub mac_address: Option<String>,
|
||||
}
|
||||
|
||||
/// Registration response payload
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterResponse {
|
||||
pub device_secret: String,
|
||||
pub config: ClientConfig,
|
||||
}
|
||||
|
||||
/// Server-pushed client configuration
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ClientConfig {
|
||||
pub heartbeat_interval_secs: u64,
|
||||
pub status_report_interval_secs: u64,
|
||||
pub asset_report_interval_secs: u64,
|
||||
pub server_version: String,
|
||||
}
|
||||
|
||||
impl Default for ClientConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
heartbeat_interval_secs: 30,
|
||||
status_report_interval_secs: 60,
|
||||
asset_report_interval_secs: 86400,
|
||||
server_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Heartbeat payload (minimal)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct HeartbeatPayload {
|
||||
pub device_uid: String,
|
||||
pub timestamp: String,
|
||||
pub hmac: String,
|
||||
}
|
||||
|
||||
/// Task execution request (Server → Client)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TaskExecutePayload {
|
||||
pub task_type: String,
|
||||
pub params: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Config update types (Server → Client)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum ConfigUpdateType {
|
||||
UpdateIntervals { heartbeat: u64, status: u64, asset: u64 },
|
||||
TlsCertRotate,
|
||||
SelfDestruct,
|
||||
}
|
||||
|
||||
// ==================== Plugin Message Payloads ====================
|
||||
|
||||
/// Plugin: Web Access Log entry (Client → Server)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct WebAccessLogEntry {
|
||||
pub device_uid: String,
|
||||
pub url: String,
|
||||
pub action: String, // "allowed" | "blocked"
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
/// Plugin: Daily Usage Report (Client → Server)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UsageDailyReport {
|
||||
pub device_uid: String,
|
||||
pub date: String,
|
||||
pub total_active_minutes: u32,
|
||||
pub total_idle_minutes: u32,
|
||||
pub first_active_at: Option<String>,
|
||||
pub last_active_at: Option<String>,
|
||||
}
|
||||
|
||||
/// Plugin: App Usage Report (Client → Server)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AppUsageEntry {
|
||||
pub device_uid: String,
|
||||
pub date: String,
|
||||
pub app_name: String,
|
||||
pub usage_minutes: u32,
|
||||
}
|
||||
|
||||
/// Plugin: Software Violation (Client → Server)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SoftwareViolationReport {
|
||||
pub device_uid: String,
|
||||
pub software_name: String,
|
||||
pub action_taken: String, // "blocked_install" | "auto_uninstalled" | "alerted"
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
/// Plugin: USB File Operation (Client → Server)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UsbFileOpEntry {
|
||||
pub device_uid: String,
|
||||
pub usb_serial: Option<String>,
|
||||
pub drive_letter: Option<String>,
|
||||
pub operation: String, // "create" | "delete" | "rename" | "modify"
|
||||
pub file_path: String,
|
||||
pub file_size: Option<u64>,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
/// Plugin: Watermark Config (Server → Client)
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct WatermarkConfigPayload {
|
||||
pub content: String,
|
||||
pub font_size: u32,
|
||||
pub opacity: f64,
|
||||
pub color: String,
|
||||
pub angle: i32,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Plugin enable/disable command (Server → Client)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PluginControlPayload {
|
||||
pub plugin_name: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Plugin: USB Policy Config (Server → Client)
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UsbPolicyPayload {
|
||||
pub policy_type: String, // "all_block" | "whitelist" | "blacklist"
|
||||
pub enabled: bool,
|
||||
pub rules: Vec<UsbDeviceRule>,
|
||||
}
|
||||
|
||||
/// A single USB device rule for whitelist/blacklist matching
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UsbDeviceRule {
|
||||
pub vendor_id: Option<String>,
|
||||
pub product_id: Option<String>,
|
||||
pub serial: Option<String>,
|
||||
pub device_name: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_frame_encode_decode_roundtrip() {
|
||||
let original = Frame::new(MessageType::Heartbeat, b"test payload".to_vec());
|
||||
let encoded = original.encode();
|
||||
let decoded = Frame::decode(&encoded).unwrap().unwrap();
|
||||
|
||||
assert_eq!(decoded.version, PROTOCOL_VERSION);
|
||||
assert_eq!(decoded.msg_type, MessageType::Heartbeat);
|
||||
assert_eq!(decoded.payload, b"test payload");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_decode_incomplete_data() {
|
||||
let data = [0x43, 0x53, 0x4D, 0x01, 0x01];
|
||||
let result = Frame::decode(&data).unwrap();
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_decode_invalid_magic() {
|
||||
let data = [0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00];
|
||||
let result = Frame::decode(&data);
|
||||
assert!(matches!(result, Err(FrameError::InvalidMagic)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_frame_roundtrip() {
|
||||
let heartbeat = HeartbeatPayload {
|
||||
device_uid: "test-uid".to_string(),
|
||||
timestamp: "2026-04-03T12:00:00Z".to_string(),
|
||||
hmac: "abc123".to_string(),
|
||||
};
|
||||
|
||||
let frame = Frame::new_json(MessageType::Heartbeat, &heartbeat).unwrap();
|
||||
let encoded = frame.encode();
|
||||
let decoded = Frame::decode(&encoded).unwrap().unwrap();
|
||||
let parsed: HeartbeatPayload = decoded.decode_payload().unwrap();
|
||||
|
||||
assert_eq!(parsed.device_uid, "test-uid");
|
||||
assert_eq!(parsed.hmac, "abc123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_message_types_roundtrip() {
|
||||
let types = [
|
||||
MessageType::WebAccessLog,
|
||||
MessageType::UsageReport,
|
||||
MessageType::AppUsageReport,
|
||||
MessageType::SoftwareViolation,
|
||||
MessageType::UsbFileOp,
|
||||
MessageType::WatermarkConfig,
|
||||
MessageType::PluginEnable,
|
||||
MessageType::PluginDisable,
|
||||
];
|
||||
|
||||
for mt in types {
|
||||
let frame = Frame::new(mt, vec![1, 2, 3]);
|
||||
let encoded = frame.encode();
|
||||
let decoded = Frame::decode(&encoded).unwrap().unwrap();
|
||||
assert_eq!(decoded.msg_type, mt);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_decode_payload_too_large() {
|
||||
// Craft a header that claims a 10 MB payload
|
||||
let mut data = Vec::with_capacity(FRAME_HEADER_SIZE);
|
||||
data.extend_from_slice(&MAGIC);
|
||||
data.push(PROTOCOL_VERSION);
|
||||
data.push(MessageType::Heartbeat as u8);
|
||||
data.extend_from_slice(&(10 * 1024 * 1024u32).to_be_bytes());
|
||||
// Don't actually include the payload — the size check should reject first
|
||||
let result = Frame::decode(&data);
|
||||
assert!(matches!(result, Err(FrameError::PayloadTooLarge(_))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user