feat: 初始化项目基础架构和核心功能

- 添加项目基础结构:Cargo.toml、.gitignore、设备UID和密钥文件
- 实现前端Vue3项目结构:路由、登录页面、设备管理页面
- 添加核心协议定义(crates/protocol):设备状态、资产、USB事件等
- 实现客户端监控模块:系统状态收集、资产收集
- 实现服务端基础API和插件系统
- 添加数据库迁移脚本:设备管理、资产跟踪、告警系统等
- 实现前端设备状态展示和基本交互
- 添加使用时长统计和水印功能插件
This commit is contained in:
iven
2026-04-05 00:57:51 +08:00
commit fd6fb5cca0
87 changed files with 19576 additions and 0 deletions

View 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 }

View 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>,
}

View 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,
};

View 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(_))));
}
}