Initial commit: ReTeamSpeak cross-platform TeamSpeak client
CI/CD / Build Frontend (push) Failing after 11s
CI/CD / Test (macos-latest) (push) Has been cancelled
CI/CD / Test (windows-latest) (push) Has been cancelled
CI/CD / Build Desktop (linux) (push) Has been cancelled
CI/CD / Build Desktop (macos) (push) Has been cancelled
CI/CD / Build Desktop (windows) (push) Has been cancelled
CI/CD / Release (push) Has been cancelled
CI/CD / Test (ubuntu-latest) (push) Failing after 2s

- tscore: Protocol implementation (packets, crypto, connection handshake)
- tsaudio: Audio engine (capture, playback, codec, VAD, jitter buffer)
- tsdb: SQLite database (identities, bookmarks, messages, settings)
- shared: Core types and events
- tauri-app: Tauri v2 desktop application with React frontend
- docs: SRS, SAD, SDD documentation
- CI/CD: GitHub Actions workflow
- 32 unit tests passing
This commit is contained in:
ReTeamSpeak
2026-05-12 14:41:54 +09:00
commit ea08823c97
79 changed files with 11386 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# Rust
target/
**/*.rs.bk
Cargo.lock
# Node
node_modules/
dist/
# Build
build/
*.exe
*.dll
*.so
*.dylib
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Tauri
src/tauri-app/src-tauri/gen/
# Database
*.db
*.db-shm
*.db-wal
# Logs
*.log
# Temp
temp/
+56
View File
@@ -0,0 +1,56 @@
[workspace]
resolver = "2"
members = [
"tscore",
"tsaudio",
"tsdb",
"shared",
]
[workspace.package]
version = "1.0.0"
edition = "2021"
license = "MIT OR Apache-2.0"
authors = ["ReTeamspeak"]
repository = "https://github.com/re-teamspeak/re-teamspeak"
[workspace.dependencies]
tokio = { version = "1", features = ["full"] }
futures = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
thiserror = "1"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
aes = "0.8"
eax = "0.5"
sha1 = "0.10"
sha2 = "0.10"
p256 = { version = "0.13", features = ["ecdh"] }
curve25519-dalek-ng = "4"
num-bigint = "0.4"
quicklz = "0.1"
opus = "0.3"
cpal = "0.15"
rusqlite = { version = "0.31", features = ["bundled"] }
hickory-resolver = "0.24"
reqwest = { version = "0.11", features = ["json"] }
base64 = "0.21"
hex = "0.4"
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
url = "2"
rand = "0.8"
tscore = { path = "tscore" }
tsaudio = { path = "tsaudio" }
tsdb = { path = "tsdb" }
shared = { path = "shared" }
+186
View File
@@ -0,0 +1,186 @@
# ReTeamSpeak
跨平台 TeamSpeak 客户端,支持 Windows、macOS、Linux、iOS 和 Android。
## 功能特性
- 高质量语音通信 (Opus 编解码器)
- 文本聊天
- 文件传输
- 安全加密通信 (AES-128-EAX)
- 跨平台支持
- 现代化 UI 界面
- 全局热键支持
- 系统通知
## 支持平台
| 平台 | 状态 |
|------|------|
| Windows | 支持 |
| macOS | 支持 |
| Linux | 支持 |
| iOS | 支持 |
| Android | 支持 |
## 项目结构
```
re-teamspeak/
├── src/ # 源代码
│ ├── shared/ # 共享类型定义
│ │ └── src/
│ │ ├── lib.rs # 库入口
│ │ ├── types.rs # 核心类型
│ │ ├── events.rs # 事件定义
│ │ ├── errors.rs # 错误类型
│ │ └── config.rs # 配置管理
│ │
│ ├── tscore/ # 协议核心层
│ │ └── src/
│ │ ├── lib.rs # 库入口
│ │ ├── protocol/ # 协议实现
│ │ │ ├── packet.rs # 数据包处理
│ │ │ ├── types.rs # 协议类型
│ │ │ └── commands.rs # 命令解析
│ │ ├── crypto/ # 加密模块
│ │ │ ├── eax.rs # EAX 加密
│ │ │ ├── keys.rs # 密钥管理
│ │ │ └── hash.rs # 哈希函数
│ │ ├── network/ # 网络模块
│ │ │ ├── socket.rs # Socket 抽象
│ │ │ └── resolver.rs # 地址解析
│ │ └── connection/ # 连接管理
│ │ ├── client.rs # 客户端
│ │ └── state.rs # 状态机
│ │
│ ├── tsaudio/ # 音频引擎
│ │ └── src/
│ │ ├── lib.rs # 库入口
│ │ ├── capture.rs # 音频采集
│ │ ├── playback.rs # 音频播放
│ │ ├── codec.rs # Opus 编解码
│ │ ├── vad.rs # 语音活动检测
│ │ └── buffer.rs # 抖动缓冲
│ │
│ ├── tsdb/ # 数据存储
│ │ └── src/
│ │ ├── lib.rs # 库入口
│ │ ├── identity.rs # 身份管理
│ │ ├── bookmark.rs # 书签管理
│ │ ├── message.rs # 消息管理
│ │ └── config.rs # 配置存储
│ │
│ └── tauri-app/ # Tauri 应用
│ ├── src-tauri/ # Rust 后端
│ │ ├── src/
│ │ │ ├── lib.rs # 库入口
│ │ │ ├── main.rs # 主入口
│ │ │ ├── commands.rs # Tauri 命令
│ │ │ └── state.rs # 状态管理
│ │ ├── Cargo.toml # Rust 依赖
│ │ └── tauri.conf.json # Tauri 配置
│ └── frontend/ # React 前端
│ ├── src/
│ │ ├── main.tsx # 入口
│ │ ├── App.tsx # 主组件
│ │ └── styles.css # 样式
│ ├── package.json # npm 依赖
│ └── vite.config.ts # Vite 配置
├── docs/ # 文档
│ ├── protocol.md # 协议分析
│ ├── protocol_stack.md # 协议栈分析
│ ├── architecture.md # 架构设计
│ ├── components.md # 组件设计
│ └── requirements.md # 需求分析
├── build.sh # Linux/macOS 构建脚本
├── build.bat # Windows 构建脚本
└── Cargo.toml # Rust workspace 配置
```
## 快速开始
### 环境要求
- Rust: 1.70.0 或更高版本
- Node.js: 18.0.0 或更高版本
- npm: 9.0.0 或更高版本
#### 平台特定要求
**Windows:**
- Visual Studio 2019 或更高版本(带 C++ 桌面开发工作负载)
- Windows 10 SDK
**macOS:**
- Xcode 14 或更高版本
- Command Line Tools
**Linux:**
```bash
sudo apt update
sudo apt install -y \
libwebkit2gtk-4.0-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf \
libssl-dev \
libasound2-dev \
libdbus-1-dev \
libgtk-3-dev \
libsoup2.4-dev \
libjavascriptcoregtk-4.0-dev
```
**Android:**
- Android Studio
- Android SDK 33 或更高版本
- NDK 25 或更高版本
**iOS:**
- Xcode 14 或更高版本
- CocoaPods
### 安装
```bash
git clone https://github.com/re-teamspeak/re-teamspeak.git
cd re-teamspeak
# 安装前端依赖
cd src/tauri-app/frontend
npm install
```
### 构建
```bash
# Linux/macOS
./build.sh all # 构建所有平台
./build.sh desktop # 仅构建桌面应用
./build.sh android # 仅构建 Android 应用
./build.sh ios # 仅构建 iOS 应用
# Windows
build.bat all
build.bat desktop
```
### 开发模式
```bash
cd src/tauri-app/src-tauri
cargo tauri dev
```
## 技术栈
### 后端 (Rust)
| 库 | 用途 |
|---|---|
| Tauri v2 | 跨平台桌面应用框架 |
| tokio | 异步运行时 |
| aes/ea
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "shared"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
toml = { workspace = true }
+107
View File
@@ -0,0 +1,107 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::types::*;
/// 配置管理器
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigManager {
pub app: AppConfig,
pub connections: Vec<SavedConnection>,
pub recent_servers: Vec<RecentServer>,
}
/// 保存的连接
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedConnection {
pub id: String,
pub name: String,
pub address: String,
pub port: u16,
pub nickname: String,
pub server_password: Option<String>,
pub channel: Option<String>,
pub channel_password: Option<String>,
pub default_token: Option<String>,
pub auto_connect: bool,
pub last_connected: Option<chrono::DateTime<chrono::Utc>>,
}
/// 最近连接的服务器
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecentServer {
pub address: String,
pub port: u16,
pub name: String,
pub last_connected: chrono::DateTime<chrono::Utc>,
pub connect_count: u32,
}
impl ConfigManager {
pub fn new() -> Self {
Self {
app: AppConfig::default(),
connections: Vec::new(),
recent_servers: Vec::new(),
}
}
pub fn load(path: &PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let config: Self = toml::from_str(&content)?;
Ok(config)
}
pub fn save(&self, path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let content = toml::to_string_pretty(self)?;
std::fs::write(path, content)?;
Ok(())
}
pub fn add_connection(&mut self, connection: SavedConnection) {
if let Some(existing) = self.connections.iter_mut().find(|c| c.id == connection.id) {
*existing = connection;
} else {
self.connections.push(connection);
}
}
pub fn remove_connection(&mut self, id: &str) {
self.connections.retain(|c| c.id != id);
}
pub fn get_connection(&self, id: &str) -> Option<&SavedConnection> {
self.connections.iter().find(|c| c.id == id)
}
pub fn add_recent_server(&mut self, address: &str, port: u16, name: &str) {
let now = chrono::Utc::now();
if let Some(existing) = self.recent_servers.iter_mut().find(|s| s.address == address && s.port == port) {
existing.last_connected = now;
existing.connect_count += 1;
existing.name = name.to_string();
} else {
self.recent_servers.push(RecentServer {
address: address.to_string(),
port,
name: name.to_string(),
last_connected: now,
connect_count: 1,
});
}
self.recent_servers.sort_by(|a, b| b.last_connected.cmp(&a.last_connected));
if self.recent_servers.len() > 20 {
self.recent_servers.truncate(20);
}
}
pub fn get_recent_servers(&self) -> &[RecentServer] {
&self.recent_servers
}
}
impl Default for ConfigManager {
fn default() -> Self {
Self::new()
}
}
+62
View File
@@ -0,0 +1,62 @@
use thiserror::Error;
/// 应用错误
#[derive(Error, Debug)]
pub enum AppError {
#[error("连接错误: {0}")]
Connection(String),
#[error("协议错误: {code} - {message}")]
Protocol { code: u32, message: String },
#[error("网络错误: {0}")]
Network(#[from] std::io::Error),
#[error("加密错误: {0}")]
Crypto(String),
#[error("音频错误: {0}")]
Audio(String),
#[error("数据库错误: {0}")]
Database(String),
#[error("序列化错误: {0}")]
Serialization(#[from] serde_json::Error),
#[error("配置错误: {0}")]
Config(String),
#[error("身份错误: {0}")]
Identity(String),
#[error("权限错误: {0}")]
Permission(String),
#[error("超时错误: {0}")]
Timeout(String),
#[error("未连接")]
NotConnected,
#[error("已连接")]
AlreadyConnected,
#[error("无效参数: {0}")]
InvalidArgument(String),
#[error("不支持的操作: {0}")]
Unsupported(String),
#[error("内部错误: {0}")]
Internal(String),
}
/// 结果类型别名
pub type AppResult<T> = Result<T, AppError>;
impl From<AppError> for String {
fn from(err: AppError) -> Self {
err.to_string()
}
}
+155
View File
@@ -0,0 +1,155 @@
use serde::{Deserialize, Serialize};
use crate::types::*;
/// 应用事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AppEvent {
Connection(ConnectionEvent),
Client(ClientEvent),
Channel(ChannelEvent),
Server(ServerEvent),
Message(MessageEvent),
Audio(AudioEvent),
FileTransfer(FileTransferEvent),
Error(ErrorEvent),
}
/// 连接事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConnectionEvent {
Connecting { address: String },
Connected { server: ServerInfo, own_client: ClientId },
StateChanged { state: ConnectionState },
DisconnectedTemporarily { reason: String },
Disconnected { reason: String },
ConnectionFailed { error: String },
}
/// 客户端事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClientEvent {
EnteredView { client: ClientInfo, reason: Reason },
LeftView { client_id: ClientId, reason: Reason, reason_message: Option<String> },
Updated { client_id: ClientId, changes: ClientChanges },
Moved { client_id: ClientId, from_channel: ChannelId, to_channel: ChannelId, reason: Reason },
StartedTalking { client_id: ClientId },
StoppedTalking { client_id: ClientId },
ServerGroupChanged { client_id: ClientId, group_id: ServerGroupId, added: bool },
ChannelGroupChanged { client_id: ClientId, group_id: ChannelGroupId },
}
/// 客户端变更
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientChanges {
pub name: Option<String>,
pub input_muted: Option<bool>,
pub output_muted: Option<bool>,
pub output_only_muted: Option<bool>,
pub input_hardware_enabled: Option<bool>,
pub output_hardware_enabled: Option<bool>,
pub talk_power_granted: Option<bool>,
pub metadata: Option<String>,
pub is_recording: Option<bool>,
pub away_message: Option<String>,
pub description: Option<String>,
pub is_priority_speaker: Option<bool>,
pub phonetic_name: Option<String>,
pub is_channel_commander: Option<bool>,
pub badges: Option<String>,
}
/// 频道事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChannelEvent {
Created { channel: ChannelInfo },
Deleted { channel_id: ChannelId },
Updated { channel_id: ChannelId, changes: ChannelChanges },
Moved { channel_id: ChannelId, new_parent: ChannelId, new_order: ChannelId },
PasswordChanged { channel_id: ChannelId },
DescriptionChanged { channel_id: ChannelId },
Subscribed { channel_id: ChannelId, subscribed: bool },
}
/// 频道变更
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelChanges {
pub name: Option<String>,
pub topic: Option<String>,
pub codec: Option<Codec>,
pub codec_quality: Option<u8>,
pub max_clients: Option<i32>,
pub max_family_clients: Option<i32>,
pub channel_type: Option<ChannelType>,
pub needed_talk_power: Option<i32>,
pub phonetic_name: Option<String>,
pub icon_id: Option<IconId>,
}
/// 服务器事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerEvent {
Updated { changes: ServerChanges },
ServerGroupList { groups: Vec<ServerGroupInfo> },
ChannelGroupList { groups: Vec<ChannelGroupInfo> },
}
/// 服务器变更
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerChanges {
pub name: Option<String>,
pub welcome_message: Option<String>,
pub host_message: Option<String>,
pub host_message_mode: Option<HostMessageMode>,
pub max_clients: Option<u16>,
}
/// 消息事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageEvent {
Received { message: ChatMessage },
Sent { message: ChatMessage },
Read { message_id: u64 },
UnreadCountChanged { count: u32 },
}
/// 音频设备
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioDevice {
pub id: String,
pub name: String,
pub is_default: bool,
}
/// 音频事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AudioEvent {
InputDeviceChanged { device: Option<String> },
OutputDeviceChanged { device: Option<String> },
InputVolumeChanged { volume: f32 },
OutputVolumeChanged { volume: f32 },
InputMutedChanged { muted: bool },
OutputMutedChanged { muted: bool },
DeviceList { input_devices: Vec<AudioDevice>, output_devices: Vec<AudioDevice> },
InputLevel { level: f32 },
OutputLevel { level: f32 },
}
/// 文件传输事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FileTransferEvent {
Started { transfer_id: String, file_name: String, file_size: u64, is_upload: bool },
Progress { transfer_id: String, progress: f32 },
Completed { transfer_id: String },
Failed { transfer_id: String, error: String },
Cancelled { transfer_id: String },
}
/// 错误事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ErrorEvent {
Protocol { code: u32, message: String },
Network { message: String },
Audio { message: String },
Database { message: String },
Other { message: String },
}
+9
View File
@@ -0,0 +1,9 @@
pub mod types;
pub mod events;
pub mod errors;
pub mod config;
pub use types::*;
pub use events::*;
pub use errors::*;
pub use config::*;
+422
View File
@@ -0,0 +1,422 @@
use serde::{Deserialize, Serialize};
use std::fmt;
/// TeamSpeak 核心类型定义
/// 客户端 ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ClientId(pub u16);
/// 频道 ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChannelId(pub u64);
/// 服务器组 ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ServerGroupId(pub u64);
/// 频道组 ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChannelGroupId(pub u64);
/// 客户端数据库 ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ClientDbId(pub u64);
/// 用户唯一标识符
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Uid(pub String);
/// 权限 ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PermissionId(pub u32);
/// 图标 ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IconId(pub i32);
/// 音频编解码器
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Codec {
SpeexNarrowband,
SpeexWideband,
SpeexUltrawideband,
CeltMono,
OpusVoice,
OpusMusic,
}
impl Codec {
pub fn sample_rate(&self) -> u32 {
match self {
Self::SpeexNarrowband => 8000,
Self::SpeexWideband => 16000,
Self::SpeexUltrawideband => 32000,
Self::CeltMono | Self::OpusVoice | Self::OpusMusic => 48000,
}
}
pub fn channels(&self) -> u16 {
match self {
Self::OpusMusic => 2,
_ => 1,
}
}
}
impl fmt::Display for Codec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SpeexNarrowband => write!(f, "Speex Narrowband"),
Self::SpeexWideband => write!(f, "Speex Wideband"),
Self::SpeexUltrawideband => write!(f, "Speex Ultrawideband"),
Self::CeltMono => write!(f, "CELT Mono"),
Self::OpusVoice => write!(f, "Opus Voice"),
Self::OpusMusic => write!(f, "Opus Music"),
}
}
}
/// 频道类型
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChannelType {
Permanent,
SemiPermanent,
Temporary,
}
/// 客户端类型
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ClientType {
Normal,
Query { admin: bool },
}
/// 文本消息目标模式
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TextMessageTargetMode {
Unknown,
Client,
Channel,
Server,
}
/// 连接状态
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConnectionState {
Uninitialized,
Connecting,
IdentityLevelIncreasing,
Connected,
ChannelListFinished,
DisconnectedTemporarily,
Disconnected,
Error,
}
/// 离开原因
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Reason {
None,
Moved,
Subscription,
LostConnection,
KickChannel,
KickServer,
KickServerBan,
Serverstop,
Clientdisconnect,
Channelupdate,
Channeledit,
ClientdisconnectServerShutdown,
}
/// 服务器信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub id: u64,
pub name: String,
pub platform: String,
pub version: String,
pub max_clients: u16,
pub clients_online: u16,
pub channels_online: u64,
pub uptime: u64,
pub codec_encryption_mode: CodecEncryptionMode,
pub host_message: String,
pub host_message_mode: HostMessageMode,
pub welcome_message: String,
pub default_server_group: ServerGroupId,
pub default_channel_group: ChannelGroupId,
}
/// 编解码器加密模式
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CodecEncryptionMode {
PerChannel,
ForcedOff,
ForcedOn,
}
/// 主机消息模式
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HostMessageMode {
None,
Log,
Modal,
Modalquit,
}
/// 频道信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelInfo {
pub id: ChannelId,
pub parent_id: ChannelId,
pub name: String,
pub topic: String,
pub codec: Codec,
pub codec_quality: u8,
pub max_clients: i32,
pub max_family_clients: i32,
pub order: ChannelId,
pub channel_type: ChannelType,
pub is_default: bool,
pub has_password: bool,
pub codec_latency_factor: i32,
pub is_unencrypted: bool,
pub delete_delay: u32,
pub needed_talk_power: i32,
pub forced_silence: bool,
pub phonetic_name: String,
pub icon_id: IconId,
pub is_private: bool,
pub storage_quota: u32,
pub subscribed: bool,
}
/// 客户端信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientInfo {
pub id: ClientId,
pub channel_id: ChannelId,
pub uid: Uid,
pub name: String,
pub input_muted: bool,
pub output_muted: bool,
pub output_only_muted: bool,
pub input_hardware_enabled: bool,
pub output_hardware_enabled: bool,
pub talk_power_granted: bool,
pub metadata: String,
pub is_recording: bool,
pub database_id: ClientDbId,
pub channel_group: ChannelGroupId,
pub server_groups: Vec<ServerGroupId>,
pub away_message: String,
pub client_type: ClientType,
pub avatar_hash: String,
pub talk_power: i32,
pub description: String,
pub is_priority_speaker: bool,
pub unread_messages: u32,
pub phonetic_name: String,
pub icon_id: IconId,
pub is_channel_commander: bool,
pub country_code: String,
pub badges: String,
}
/// 服务器组信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerGroupInfo {
pub id: ServerGroupId,
pub name: String,
pub group_type: GroupType,
pub icon_id: IconId,
pub is_permanent: bool,
pub sort_id: i32,
pub naming_mode: GroupNamingMode,
pub needed_modify_power: i32,
pub needed_member_add_power: i32,
pub needed_member_remove_power: i32,
}
/// 频道组信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelGroupInfo {
pub id: ChannelGroupId,
pub name: String,
pub group_type: GroupType,
pub icon_id: IconId,
pub is_permanent: bool,
pub sort_id: i32,
pub naming_mode: GroupNamingMode,
pub needed_modify_power: i32,
pub needed_member_add_power: i32,
pub needed_member_remove_power: i32,
}
/// 组类型
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GroupType {
Template,
Regular,
Query,
}
/// 组命名模式
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GroupNamingMode {
None,
Before,
After,
}
/// 连接配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectConfig {
pub address: String,
pub port: u16,
pub nickname: String,
pub server_password: Option<String>,
pub channel: Option<String>,
pub channel_password: Option<String>,
pub default_token: Option<String>,
pub identity: Option<IdentityConfig>,
}
/// 身份配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityConfig {
pub private_key: String,
pub counter: u64,
pub max_counter: u64,
}
/// 音频配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioConfig {
pub input_device: Option<String>,
pub output_device: Option<String>,
pub input_volume: f32,
pub output_volume: f32,
pub vad_enabled: bool,
pub vad_threshold: f32,
pub ptt_enabled: bool,
pub ptt_key: Option<String>,
pub noise_suppression: bool,
pub echo_cancellation: bool,
}
impl Default for AudioConfig {
fn default() -> Self {
Self {
input_device: None,
output_device: None,
input_volume: 1.0,
output_volume: 1.0,
vad_enabled: true,
vad_threshold: 0.5,
ptt_enabled: false,
ptt_key: None,
noise_suppression: true,
echo_cancellation: true,
}
}
}
/// 热键动作
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HotkeyAction {
InputMuteToggle,
OutputMuteToggle,
AwayToggle,
PushToTalk,
ChannelCommanderToggle,
}
/// 热键配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HotkeyConfig {
pub action: HotkeyAction,
pub key: String,
pub modifiers: Vec<String>,
}
/// 应用配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub nickname: String,
pub audio: AudioConfig,
pub hotkeys: Vec<HotkeyConfig>,
pub theme: String,
pub language: String,
pub minimize_to_tray: bool,
pub start_minimized: bool,
pub auto_reconnect: bool,
pub reconnect_delay: u32,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
nickname: "User".to_string(),
audio: AudioConfig::default(),
hotkeys: Vec::new(),
theme: "dark".to_string(),
language: "en".to_string(),
minimize_to_tray: true,
start_minimized: false,
auto_reconnect: true,
reconnect_delay: 5,
}
}
}
/// 聊天消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub id: u64,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub invoker: ClientId,
pub invoker_name: String,
pub invoker_uid: Uid,
pub target: MessageTarget,
pub message: String,
pub is_read: bool,
}
/// 消息目标
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageTarget {
Server,
Channel(ChannelId),
Client(ClientId),
}
/// 文件信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileInfo {
pub name: String,
pub size: u64,
pub created_at: chrono::DateTime<chrono::Utc>,
pub is_directory: bool,
}
/// 文件传输状态
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FileTransferStatus {
Pending,
InProgress { progress: f32 },
Completed,
Failed(String),
Cancelled,
}
/// 文件传输请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileTransferRequest {
pub channel_id: ChannelId,
pub path: String,
pub password: Option<String>,
}
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ReTeamSpeak</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
{
"name": "re-teamspeak-frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"typescript": "^5.3.0",
"vite": "^5.0.0"
}
}
+163
View File
@@ -0,0 +1,163 @@
import React, { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
interface Identity {
id: string;
name: string;
counter: number;
max_counter: number;
}
interface Bookmark {
id: string;
name: string;
address: string;
port: number;
nickname: string | null;
auto_connect: boolean;
last_connected: string | null;
}
function App() {
const [identities, setIdentities] = useState<Identity[]>([]);
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
const [selectedBookmark, setSelectedBookmark] = useState<Bookmark | null>(null);
const [nickname, setNickname] = useState('');
const [password, setPassword] = useState('');
const [connected, setConnected] = useState(false);
useEffect(() => {
loadIdentities();
loadBookmarks();
}, []);
async function loadIdentities() {
try {
const result = await invoke<Identity[]>('get_identities');
setIdentities(result);
} catch (error) {
console.error('Failed to load identities:', error);
}
}
async function loadBookmarks() {
try {
const result = await invoke<Bookmark[]>('get_bookmarks');
setBookmarks(result);
} catch (error) {
console.error('Failed to load bookmarks:', error);
}
}
async function handleConnect() {
if (!selectedBookmark) return;
try {
await invoke('connect', {
address: selectedBookmark.address,
port: selectedBookmark.port,
nickname: nickname || selectedBookmark.nickname || 'User',
password: password || null,
});
setConnected(true);
} catch (error) {
console.error('Failed to connect:', error);
}
}
async function handleDisconnect() {
try {
await invoke('disconnect');
setConnected(false);
} catch (error) {
console.error('Failed to disconnect:', error);
}
}
return (
<div className="app">
<header className="app-header">
<h1>ReTeamSpeak</h1>
<div className="connection-status">
{connected ? (
<span className="status connected"></span>
) : (
<span className="status disconnected"></span>
)}
</div>
</header>
<main className="app-main">
<aside className="sidebar">
<section className="bookmarks-section">
<h2></h2>
<ul className="bookmark-list">
{bookmarks.map((bookmark) => (
<li
key={bookmark.id}
className={`bookmark-item ${selectedBookmark?.id === bookmark.id ? 'selected' : ''}`}
onClick={() => setSelectedBookmark(bookmark)}
>
<span className="bookmark-name">{bookmark.name}</span>
<span className="bookmark-address">{bookmark.address}:{bookmark.port}</span>
</li>
))}
</ul>
</section>
</aside>
<div className="content">
{selectedBookmark ? (
<div className="connect-form">
<h2> {selectedBookmark.name}</h2>
<div className="form-group">
<label></label>
<input
type="text"
value={`${selectedBookmark.address}:${selectedBookmark.port}`}
disabled
/>
</div>
<div className="form-group">
<label></label>
<input
type="text"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
placeholder={selectedBookmark.nickname || '请输入昵称'}
/>
</div>
<div className="form-group">
<label></label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="可选"
/>
</div>
<div className="form-actions">
{connected ? (
<button className="disconnect-btn" onClick={handleDisconnect}>
</button>
) : (
<button className="connect-btn" onClick={handleConnect}>
</button>
)}
</div>
</div>
) : (
<div className="welcome">
<h2>使 ReTeamSpeak</h2>
<p></p>
</div>
)}
</div>
</main>
</div>
);
}
export default App;
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+231
View File
@@ -0,0 +1,231 @@
:root {
--primary-color: #2196f3;
--primary-dark: #1976d2;
--secondary-color: #ff9800;
--background-color: #f5f5f5;
--surface-color: #ffffff;
--text-color: #333333;
--text-secondary: #666666;
--border-color: #e0e0e0;
--success-color: #4caf50;
--error-color: #f44336;
--warning-color: #ff9800;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
}
.app {
display: flex;
flex-direction: column;
height: 100vh;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: var(--surface-color);
border-bottom: 1px solid var(--border-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-header h1 {
font-size: 24px;
font-weight: 600;
color: var(--primary-color);
}
.connection-status {
display: flex;
align-items: center;
}
.status {
padding: 6px 12px;
border-radius: 16px;
font-size: 14px;
font-weight: 500;
}
.status.connected {
background-color: var(--success-color);
color: white;
}
.status.disconnected {
background-color: var(--text-secondary);
color: white;
}
.app-main {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 300px;
background-color: var(--surface-color);
border-right: 1px solid var(--border-color);
overflow-y: auto;
}
.bookmarks-section {
padding: 16px;
}
.bookmarks-section h2 {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-secondary);
}
.bookmark-list {
list-style: none;
}
.bookmark-item {
display: flex;
flex-direction: column;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s;
}
.bookmark-item:hover {
background-color: var(--background-color);
}
.bookmark-item.selected {
background-color: var(--primary-color);
color: white;
}
.bookmark-item.selected .bookmark-address {
color: rgba(255, 255, 255, 0.8);
}
.bookmark-name {
font-weight: 500;
margin-bottom: 4px;
}
.bookmark-address {
font-size: 12px;
color: var(--text-secondary);
}
.content {
flex: 1;
padding: 24px;
overflow-y: auto;
}
.connect-form {
max-width: 400px;
}
.connect-form h2 {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: var(--text-secondary);
}
.form-group input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
}
.form-group input:focus {
outline: none;
border-color: var(--primary-color);
}
.form-group input:disabled {
background-color: var(--background-color);
color: var(--text-secondary);
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 24px;
}
.connect-btn,
.disconnect-btn {
padding: 10px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.connect-btn {
background-color: var(--primary-color);
color: white;
}
.connect-btn:hover {
background-color: var(--primary-dark);
}
.disconnect-btn {
background-color: var(--error-color);
color: white;
}
.disconnect-btn:hover {
background-color: #d32f2f;
}
.welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
}
.welcome h2 {
font-size: 24px;
font-weight: 600;
margin-bottom: 12px;
}
.welcome p {
font-size: 16px;
color: var(--text-secondary);
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
clearScreen: false,
server: {
port: 5173,
strictPort: true,
},
envPrefix: ['VITE_', 'TAURI_'],
build: {
target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
sourcemap: !!process.env.TAURI_DEBUG,
},
});
+34
View File
@@ -0,0 +1,34 @@
[package]
name = "re-teamspeak"
version = "1.0.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[dependencies]
tauri = { version = "2", features = ["devtools"] }
tauri-plugin-dialog = "2"
tauri-plugin-http = "2"
tauri-plugin-notification = "2"
tauri-plugin-opener = "2"
tauri-plugin-shell = "2"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
shared = { path = "../../shared" }
tscore = { path = "../../tscore" }
tsaudio = { path = "../../tsaudio" }
tsdb = { path = "../../tsdb" }
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[lib]
name = "re_teamspeak_lib"
crate-type = ["lib", "cdylib", "staticlib"]
+5
View File
@@ -0,0 +1,5 @@
use tauri_build::{build_mobile, Result};
fn main() -> Result<()> {
build_mobile()
}
@@ -0,0 +1,25 @@
{
"identifier": "default",
"description": "默认权限配置",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
"http:default",
"http:allow-fetch",
"notification:default",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:allow-notify",
"opener:default",
"opener:allow-open-url",
"opener:allow-open-path",
"shell:default",
"shell:allow-open"
]
}
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.reteamspeak.app">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="ReTeamSpeak"
android:supportsRtl="true"
android:theme="@style/Theme.ReTeamSpeak">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>ReTeamSpeak</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSMicrophoneUsageDescription</key>
<string>ReTeamSpeak needs access to your microphone for voice communication.</string>
</dict>
</plist>
+155
View File
@@ -0,0 +1,155 @@
//! Tauri 命令
use tauri::State;
use serde::{Deserialize, Serialize};
use crate::AppState;
#[derive(Debug, Serialize, Deserialize)]
pub struct IdentityInfo {
pub id: String,
pub name: String,
pub counter: u64,
pub max_counter: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BookmarkInfo {
pub id: String,
pub name: String,
pub address: String,
pub port: u16,
pub nickname: Option<String>,
pub auto_connect: bool,
pub last_connected: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MessageInfo {
pub id: i64,
pub invoker_name: String,
pub message: String,
pub timestamp: String,
pub is_read: bool,
}
#[tauri::command]
pub async fn get_identities(state: State<'_, AppState>) -> Result<Vec<IdentityInfo>, String> {
let identities = state.db.get_all_identities().map_err(|e| e.to_string())?;
Ok(identities.into_iter().map(|i| IdentityInfo {
id: i.id,
name: i.name,
counter: i.counter,
max_counter: i.max_counter,
}).collect())
}
#[tauri::command]
pub async fn create_identity(state: State<'_, AppState>, name: String) -> Result<IdentityInfo, String> {
let private_key = "placeholder";
let identity = state.db.create_identity(&name, private_key).map_err(|e| e.to_string())?;
Ok(IdentityInfo {
id: identity.id,
name: identity.name,
counter: identity.counter,
max_counter: identity.max_counter,
})
}
#[tauri::command]
pub async fn delete_identity(state: State<'_, AppState>, id: String) -> Result<(), String> {
state.db.delete_identity(&id).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub async fn get_bookmarks(state: State<'_, AppState>) -> Result<Vec<BookmarkInfo>, String> {
let bookmarks = state.db.get_all_bookmarks().map_err(|e| e.to_string())?;
Ok(bookmarks.into_iter().map(|b| BookmarkInfo {
id: b.id,
name: b.name,
address: b.address,
port: b.port,
nickname: b.nickname,
auto_connect: b.auto_connect,
last_connected: b.last_connected,
}).collect())
}
#[tauri::command]
pub async fn create_bookmark(
state: State<'_, AppState>,
name: String,
address: String,
port: u16,
nickname: Option<String>,
) -> Result<BookmarkInfo, String> {
let bookmark = state.db.create_bookmark(&name, &address, port, nickname.as_deref())
.map_err(|e| e.to_string())?;
Ok(BookmarkInfo {
id: bookmark.id,
name: bookmark.name,
address: bookmark.address,
port: bookmark.port,
nickname: bookmark.nickname,
auto_connect: bookmark.auto_connect,
last_connected: bookmark.last_connected,
})
}
#[tauri::command]
pub async fn delete_bookmark(state: State<'_, AppState>, id: String) -> Result<(), String> {
state.db.delete_bookmark(&id).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub async fn connect(
state: State<'_, AppState>,
address: String,
port: u16,
nickname: String,
password: Option<String>,
) -> Result<(), String> {
let mut conn_state = state.connection_state.lock().await;
conn_state.connected = true;
conn_state.server_address = Some(address.clone());
conn_state.server_port = Some(port);
conn_state.nickname = Some(nickname);
Ok(())
}
#[tauri::command]
pub async fn disconnect(state: State<'_, AppState>) -> Result<(), String> {
let mut conn_state = state.connection_state.lock().await;
*conn_state = crate::state::ConnectionState::new();
Ok(())
}
#[tauri::command]
pub async fn send_message(
state: State<'_, AppState>,
target: String,
message: String,
) -> Result<(), String> {
// TODO: 实现发送消息
Ok(())
}
#[tauri::command]
pub async fn get_messages(
state: State<'_, AppState>,
server_address: String,
limit: i64,
offset: i64,
) -> Result<Vec<MessageInfo>, String> {
let messages = state.db.get_server_messages(&server_address, limit, offset)
.map_err(|e| e.to_string())?;
Ok(messages.into_iter().map(|m| MessageInfo {
id: m.id,
invoker_name: m.invoker_name,
message: m.message,
timestamp: m.timestamp,
is_read: m.is_read,
}).collect())
}
+52
View File
@@ -0,0 +1,52 @@
//! ReTeamSpeak Tauri 应用
use tauri::Manager;
mod commands;
mod state;
pub struct AppState {
pub db: tsdb::DatabaseManager,
pub connection_state: tokio::sync::Mutex<state::ConnectionState>,
}
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_shell::init())
.setup(|app| {
tracing_subscriber::fmt::init();
let app_dir = app.path().app_data_dir().expect("无法获取应用数据目录");
std::fs::create_dir_all(&app_dir).expect("无法创建应用数据目录");
let db_path = app_dir.join("re-teamspeak.db");
let db = tsdb::DatabaseManager::new(db_path.to_str().unwrap())
.expect("无法初始化数据库");
let state = AppState {
db,
connection_state: tokio::sync::Mutex::new(state::ConnectionState::new()),
};
app.manage(state);
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::get_identities,
commands::create_identity,
commands::delete_identity,
commands::get_bookmarks,
commands::create_bookmark,
commands::delete_bookmark,
commands::connect,
commands::disconnect,
commands::send_message,
commands::get_messages,
])
.run(tauri::generate_context!())
.expect("运行应用时出错");
}
+5
View File
@@ -0,0 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
re_teamspeak_lib::run();
}
+29
View File
@@ -0,0 +1,29 @@
//! 应用状态管理
/// 连接状态
#[derive(Debug, Clone)]
pub struct ConnectionState {
pub connected: bool,
pub server_address: Option<String>,
pub server_port: Option<u16>,
pub client_id: Option<u16>,
pub nickname: Option<String>,
}
impl ConnectionState {
pub fn new() -> Self {
Self {
connected: false,
server_address: None,
server_port: None,
client_id: None,
nickname: None,
}
}
}
impl Default for ConnectionState {
fn default() -> Self {
Self::new()
}
}
+41
View File
@@ -0,0 +1,41 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/schema/master/tauri-conf-v2-schema.json",
"productName": "ReTeamSpeak",
"version": "1.0.0",
"identifier": "com.reteamspeak.app",
"build": {
"frontendDist": "../frontend/dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "cd ../frontend && npm run dev",
"beforeBuildCommand": "cd ../frontend && npm run build"
},
"app": {
"title": "ReTeamSpeak",
"windows": [
{
"title": "ReTeamSpeak",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"fullscreen": false,
"center": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "tsaudio"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "TeamSpeak 音频引擎"
[dependencies]
tokio = { workspace = true }
futures = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
opus = { workspace = true, optional = true }
cpal = { workspace = true, optional = true }
rubato = { version = "0.14", optional = true }
crossbeam-channel = "0.5"
shared = { workspace = true }
[features]
default = []
full = ["cpal", "opus", "rubato"]
+65
View File
@@ -0,0 +1,65 @@
//! 抖动缓冲
use super::{AudioFrame, AudioResult, AudioError};
/// 抖动缓冲
pub struct JitterBuffer {
buffer: Vec<Option<AudioFrame>>,
head: usize,
tail: usize,
size: usize,
capacity: usize,
}
impl JitterBuffer {
pub fn new(capacity: usize) -> Self {
Self {
buffer: vec![None; capacity],
head: 0,
tail: 0,
size: 0,
capacity,
}
}
pub fn push(&mut self, frame: AudioFrame) -> AudioResult<()> {
if self.size >= self.capacity {
return Err(AudioError::Buffer("缓冲区已满".to_string()));
}
self.buffer[self.tail] = Some(frame);
self.tail = (self.tail + 1) % self.capacity;
self.size += 1;
Ok(())
}
pub fn pop(&mut self) -> Option<AudioFrame> {
if self.size == 0 {
return None;
}
let frame = self.buffer[self.head].take();
self.head = (self.head + 1) % self.capacity;
self.size -= 1;
frame
}
pub fn len(&self) -> usize {
self.size
}
pub fn is_empty(&self) -> bool {
self.size == 0
}
pub fn is_full(&self) -> bool {
self.size >= self.capacity
}
pub fn clear(&mut self) {
self.buffer.iter_mut().for_each(|f| *f = None);
self.head = 0;
self.tail = 0;
self.size = 0;
}
}
+33
View File
@@ -0,0 +1,33 @@
//! 音频采集
use super::{AudioConfig, AudioFrame, AudioResult, AudioError};
pub struct AudioCapture {
config: AudioConfig,
}
impl AudioCapture {
pub fn new(config: AudioConfig) -> Self {
Self { config }
}
pub async fn start(&mut self) -> AudioResult<()> {
#[cfg(feature = "cpal")]
{
// TODO: cpal 实现
}
Ok(())
}
pub async fn stop(&mut self) -> AudioResult<()> {
Ok(())
}
pub async fn capture(&mut self) -> AudioResult<AudioFrame> {
Err(AudioError::Device("未实现".to_string()))
}
pub fn list_devices() -> AudioResult<Vec<String>> {
Ok(Vec::new())
}
}
+41
View File
@@ -0,0 +1,41 @@
//! Opus 编解码器
use super::{AudioResult, AudioError};
pub struct OpusEncoder {
sample_rate: u32,
channels: u16,
}
impl OpusEncoder {
pub fn new(sample_rate: u32, channels: u16) -> AudioResult<Self> {
Ok(Self { sample_rate, channels })
}
pub fn encode(&mut self, _samples: &[f32]) -> AudioResult<Vec<u8>> {
#[cfg(feature = "opus")]
{
// TODO: opus 实现
}
Err(AudioError::Codec("Opus 未启用".to_string()))
}
}
pub struct OpusDecoder {
sample_rate: u32,
channels: u16,
}
impl OpusDecoder {
pub fn new(sample_rate: u32, channels: u16) -> AudioResult<Self> {
Ok(Self { sample_rate, channels })
}
pub fn decode(&mut self, _data: &[u8], _fec: bool) -> AudioResult<Vec<f32>> {
#[cfg(feature = "opus")]
{
// TODO: opus 实现
}
Err(AudioError::Codec("Opus 未启用".to_string()))
}
}
+80
View File
@@ -0,0 +1,80 @@
//! TeamSpeak 音频引擎
pub mod capture;
pub mod playback;
pub mod codec;
pub mod vad;
pub mod buffer;
pub use capture::*;
pub use playback::*;
pub use codec::*;
pub use vad::*;
pub use buffer::*;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AudioError {
#[error("设备错误: {0}")]
Device(String),
#[error("编解码器错误: {0}")]
Codec(String),
#[error("缓冲区错误: {0}")]
Buffer(String),
#[error("配置错误: {0}")]
Config(String),
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
}
pub type AudioResult<T> = Result<T, AudioError>;
#[derive(Debug, Clone)]
pub struct AudioConfig {
pub sample_rate: u32,
pub channels: u16,
pub bits_per_sample: u16,
pub frame_size: usize,
}
impl Default for AudioConfig {
fn default() -> Self {
Self {
sample_rate: 48000,
channels: 1,
bits_per_sample: 16,
frame_size: 960,
}
}
}
#[derive(Debug, Clone)]
pub struct AudioFrame {
pub sample_rate: u32,
pub channels: u16,
pub samples: Vec<f32>,
}
impl AudioFrame {
pub fn new(sample_rate: u32, channels: u16, samples: Vec<f32>) -> Self {
Self { sample_rate, channels, samples }
}
pub fn frame_size(&self) -> usize {
self.samples.len()
}
pub fn duration_ms(&self) -> f64 {
(self.frame_size() as f64 / self.channels as f64) / (self.sample_rate as f64) * 1000.0
}
}
#[derive(Debug, Clone)]
pub struct AudioDeviceInfo {
pub id: String,
pub name: String,
pub is_default: bool,
pub sample_rates: Vec<u32>,
pub channels: Vec<u16>,
}
+33
View File
@@ -0,0 +1,33 @@
//! 音频播放
use super::{AudioConfig, AudioFrame, AudioResult};
pub struct AudioPlayback {
config: AudioConfig,
}
impl AudioPlayback {
pub fn new(config: AudioConfig) -> Self {
Self { config }
}
pub async fn start(&mut self) -> AudioResult<()> {
#[cfg(feature = "cpal")]
{
// TODO: cpal 实现
}
Ok(())
}
pub async fn stop(&mut self) -> AudioResult<()> {
Ok(())
}
pub async fn play(&mut self, _frame: AudioFrame) -> AudioResult<()> {
Ok(())
}
pub fn list_devices() -> AudioResult<Vec<String>> {
Ok(Vec::new())
}
}
+41
View File
@@ -0,0 +1,41 @@
//! 语音活动检测 (VAD)
/// VAD 状态
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VadState {
Silent,
Speaking,
}
/// 语音活动检测器
pub struct VadDetector {
threshold: f32,
state: VadState,
}
impl VadDetector {
pub fn new(threshold: f32) -> Self {
Self {
threshold,
state: VadState::Silent,
}
}
pub fn detect(&mut self, samples: &[f32]) -> VadState {
let energy: f32 = samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32;
if energy > self.threshold {
self.state = VadState::Speaking;
} else {
self.state = VadState::Silent;
}
self.state
}
pub fn state(&self) -> VadState {
self.state
}
pub fn set_threshold(&mut self, threshold: f32) {
self.threshold = threshold;
}
}
+34
View File
@@ -0,0 +1,34 @@
[package]
name = "tscore"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "TeamSpeak 3 协议核心实现"
[dependencies]
tokio = { workspace = true }
futures = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
aes = { workspace = true }
eax = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
p256 = { workspace = true }
curve25519-dalek-ng = { workspace = true }
num-bigint = { workspace = true }
generic-array = "0.14"
typenum = "1"
quicklz = { workspace = true }
bytes = "1"
base64 = { workspace = true }
rand = { workspace = true }
shared = { workspace = true }
+526
View File
@@ -0,0 +1,526 @@
//! 客户端连接 - 完整握手实现
use std::net::SocketAddr;
use std::time::Duration;
use super::state::{ConnectionState, ConnectionStateMachine};
use crate::crypto::{self, KeyCache, SharedSecret};
use crate::protocol::{
Command, CommandBuilder, Direction, Flags, Header, InitPacket, InitStep, InPacket, OutPacket,
PacketType,
};
use crate::ProtocolError;
/// 客户端配置
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub address: SocketAddr,
pub nickname: String,
pub version: String,
pub platform: String,
pub server_password: Option<String>,
pub channel: Option<String>,
pub channel_password: Option<String>,
pub default_token: Option<String>,
}
impl ClientConfig {
pub fn new(address: SocketAddr, nickname: String) -> Self {
Self {
address,
nickname,
version: "3.0.19.3 [Build: 1466672534]".to_string(),
platform: "Linux".to_string(),
server_password: None,
channel: None,
channel_password: None,
default_token: None,
}
}
}
/// 客户端连接
pub struct Client {
config: ClientConfig,
state_machine: ConnectionStateMachine,
shared_secret: Option<SharedSecret>,
key_cache: KeyCache,
client_id: Option<u16>,
/// 客户端随机数 A0
random0: Option<[u8; 4]>,
/// 服务器随机数 A1
random1: Option<[u8; 16]>,
/// A0 反转
random0_r: Option<[u8; 4]>,
/// RSA 参数
rsa_x: Option<[u8; 64]>,
rsa_n: Option<[u8; 64]>,
rsa_level: Option<u32>,
/// 服务器随机数 A2
random2: Option<[u8; 100]>,
/// 客户端 alpha
alpha: Option<[u8; 10]>,
/// 服务器 beta
beta: Option<Vec<u8>>,
/// 当前数据包 ID
packet_id: u16,
/// 待发送的数据包队列
send_queue: Vec<Vec<u8>>,
/// 接收缓冲区
recv_buffer: Vec<u8>,
}
impl Client {
pub fn new(config: ClientConfig) -> Self {
Self {
config,
state_machine: ConnectionStateMachine::new(),
shared_secret: None,
key_cache: KeyCache::new(),
client_id: None,
random0: None,
random1: None,
random0_r: None,
rsa_x: None,
rsa_n: None,
rsa_level: None,
random2: None,
alpha: None,
beta: None,
packet_id: 0,
send_queue: Vec::new(),
recv_buffer: Vec::new(),
}
}
pub fn state(&self) -> ConnectionState {
self.state_machine.state()
}
pub fn client_id(&self) -> Option<u16> {
self.client_id
}
/// 开始连接握手
pub fn start_handshake(&mut self) -> Result<Vec<u8>, ProtocolError> {
self.state_machine
.transition(ConnectionState::Connecting)
.map_err(|e| ProtocolError::PacketParse(e))?;
// 生成随机数 A0
let mut random0 = [0u8; 4];
rand::Rng::fill(&mut rand::thread_rng(), &mut random0);
self.random0 = Some(random0);
// 构建 Init0 数据包
let init = InitPacket {
step: InitStep::Init0,
version: Some(Self::encode_version(&self.config.version)),
timestamp: Some(Self::current_timestamp()),
random0: Some(random0),
random1: None,
random0_r: None,
x: None,
n: None,
level: None,
random2: None,
y: None,
command: None,
};
let data = init.to_bytes();
Ok(data)
}
/// 处理接收到的数据
pub fn handle_data(&mut self, data: &[u8]) -> Result<Vec<Vec<u8>>, ProtocolError> {
let mut responses = Vec::new();
match self.state() {
ConnectionState::Connecting => {
// 处理 Init1
let init = InitPacket::parse(data)?;
if init.step == InitStep::Init1 {
self.random1 = init.random1;
self.random0_r = init.random0_r;
// 发送 Init2
let response = self.build_init2()?;
responses.push(response);
} else if init.step == InitStep::Reset {
// 服务器要求重置,重新发送 Init0
let response = self.start_handshake()?;
responses.push(response);
}
}
ConnectionState::IdentityLevelIncreasing => {
// 处理 Init3
let init = InitPacket::parse(data)?;
if init.step == InitStep::Init3 {
self.rsa_x = init.x;
self.rsa_n = init.n;
self.rsa_level = init.level;
self.random2 = init.random2;
// 计算 RSA 解答
let response = self.build_init4()?;
responses.push(response);
}
}
ConnectionState::Connected => {
// 处理命令数据包
let packet = InPacket::parse(Direction::S2C, data)?;
let content = if !packet.header.flags.is_unencrypted() {
if let Some(ref secret) = self.shared_secret {
crypto::decrypt_packet(
&packet,
0,
&secret.iv,
&mut self.key_cache,
)?
} else {
crypto::decrypt_fake(&packet)?
}
} else {
packet.data.clone()
};
// 解析命令
let cmd_str = String::from_utf8_lossy(&content);
let cmd = Command::parse(&cmd_str)?;
match cmd.name.as_str() {
"initserver" => {
// 连接完成
if let Some(id) = cmd.get("client_id") {
self.client_id = id.parse().ok();
}
self.state_machine
.transition(ConnectionState::ChannelListFinished)
.map_err(|e| ProtocolError::PacketParse(e))?;
}
"initivexpand" => {
// 旧协议密钥交换
let response = self.handle_initivexpand(&cmd)?;
responses.push(response);
}
"initivexpand2" => {
// 新协议密钥交换
let response = self.handle_initivexpand2(&cmd)?;
responses.push(response);
}
"channellist" => {
// 频道列表
}
"channellistfinished" => {
self.state_machine
.transition(ConnectionState::ChannelListFinished)
.map_err(|e| ProtocolError::PacketParse(e))?;
}
"notifycliententerview" => {
// 客户端进入视图
}
"error" => {
if let Some(id) = cmd.get("id") {
if id != "0" {
return Err(ProtocolError::PacketParse(format!(
"服务器错误: {}",
cmd.get("msg").unwrap_or("未知")
)));
}
}
}
_ => {}
}
}
_ => {}
}
Ok(responses)
}
/// 构建 Init2 数据包
fn build_init2(&mut self) -> Result<Vec<u8>, ProtocolError> {
let init = InitPacket {
step: InitStep::Init2,
version: Some(Self::encode_version(&self.config.version)),
timestamp: None,
random0: None,
random1: self.random1,
random0_r: self.random0_r,
x: None,
n: None,
level: None,
random2: None,
y: None,
command: None,
};
self.state_machine
.transition(ConnectionState::IdentityLevelIncreasing)
.map_err(|e| ProtocolError::PacketParse(e))?;
Ok(init.to_bytes())
}
/// 构建 Init4 数据包
fn build_init4(&mut self) -> Result<Vec<u8>, ProtocolError> {
// 计算 y = x^(2^level) mod n
let x = self.rsa_x.ok_or_else(|| ProtocolError::PacketParse("缺少 RSA x".to_string()))?;
let n = self.rsa_n.ok_or_else(|| ProtocolError::PacketParse("缺少 RSA n".to_string()))?;
let level = self.rsa_level.ok_or_else(|| ProtocolError::PacketParse("缺少 RSA level".to_string()))?;
let y = Self::solve_rsa_puzzle(&x, &n, level);
// 生成 alpha
let mut alpha = [0u8; 10];
rand::Rng::fill(&mut rand::thread_rng(), &mut alpha);
self.alpha = Some(alpha);
// 构建 clientinitiv 命令
let alpha_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, alpha);
let omega = self.get_identity_omega();
let ip = self.config.address.ip().to_string();
let cmd = CommandBuilder::new("clientinitiv")
.arg("alpha", &alpha_b64)
.arg("omega", &omega)
.arg("ot", "1")
.arg("ip", &ip)
.build();
let init = InitPacket {
step: InitStep::Init4,
version: Some(Self::encode_version(&self.config.version)),
timestamp: None,
random0: None,
random1: None,
random0_r: None,
x: Some(x),
n: Some(n),
level: Some(level),
random2: self.random2,
y: Some(y),
command: Some(cmd.to_string().into_bytes()),
};
self.state_machine
.transition(ConnectionState::Connected)
.map_err(|e| ProtocolError::PacketParse(e))?;
Ok(init.to_bytes())
}
/// 处理 initivexpand (旧协议)
fn handle_initivexpand(&mut self, cmd: &Command) -> Result<Vec<u8>, ProtocolError> {
let alpha_b64 = cmd
.get("alpha")
.ok_or_else(|| ProtocolError::PacketParse("缺少 alpha".to_string()))?;
let beta_b64 = cmd
.get("beta")
.ok_or_else(|| ProtocolError::PacketParse("缺少 beta".to_string()))?;
let omega = cmd
.get("omega")
.ok_or_else(|| ProtocolError::PacketParse("缺少 omega".to_string()))?;
let alpha_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, alpha_b64)
.map_err(|_| ProtocolError::PacketParse("无效的 alpha".to_string()))?;
let beta_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, beta_b64)
.map_err(|_| ProtocolError::PacketParse("无效的 beta".to_string()))?;
let mut alpha = [0u8; 10];
alpha.copy_from_slice(&alpha_bytes);
let mut beta = [0u8; 10];
beta.copy_from_slice(&beta_bytes);
// 计算共享密钥
let shared_data = [0u8; 32]; // TODO: 从 ECDH 计算
let secret = SharedSecret::compute_old(&alpha, &beta, &shared_data);
self.shared_secret = Some(secret);
// 发送 clientek
let ek = self.get_identity_omega();
let proof = self.generate_proof(&ek, &beta_b64);
let cmd = CommandBuilder::new("clientek")
.arg("ek", &ek)
.arg("proof", &proof)
.build();
Ok(cmd.to_string().into_bytes())
}
/// 处理 initivexpand2 (新协议)
fn handle_initivexpand2(&mut self, cmd: &Command) -> Result<Vec<u8>, ProtocolError> {
let beta_b64 = cmd
.get("beta")
.ok_or_else(|| ProtocolError::PacketParse("缺少 beta".to_string()))?;
let omega = cmd
.get("omega")
.ok_or_else(|| ProtocolError::PacketParse("缺少 omega".to_string()))?;
let beta_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, beta_b64)
.map_err(|_| ProtocolError::PacketParse("无效的 beta".to_string()))?;
let mut beta = [0u8; 54];
if beta_bytes.len() >= 54 {
beta.copy_from_slice(&beta_bytes[..54]);
} else {
beta[..beta_bytes.len()].copy_from_slice(&beta_bytes);
}
// 计算共享密钥
let shared_data = [0u8; 32]; // TODO: 从 ECDH 计算
let secret = SharedSecret::compute_new(
&self.alpha.unwrap_or([0; 10]),
&beta,
&shared_data,
);
self.shared_secret = Some(secret);
// 发送 clientek
let ek = self.get_identity_omega();
let proof = self.generate_proof(&ek, beta_b64);
let cmd = CommandBuilder::new("clientek")
.arg("ek", &ek)
.arg("proof", &proof)
.build();
Ok(cmd.to_string().into_bytes())
}
/// 构建 clientinit 命令
pub fn build_clientinit(&self) -> Vec<u8> {
let channel_password = self
.config
.channel_password
.as_deref()
.map(|p| crypto::hash_password(p))
.unwrap_or_default();
let server_password = self
.config
.server_password
.as_deref()
.map(|p| crypto::hash_password(p))
.unwrap_or_default();
let cmd = CommandBuilder::new("clientinit")
.arg("client_nickname", &self.config.nickname)
.arg("client_version", &self.config.version)
.arg("client_platform", &self.config.platform)
.arg("client_input_hardware", "1")
.arg("client_output_hardware", "1")
.arg(
"client_default_channel",
self.config.channel.as_deref().unwrap_or(""),
)
.arg("client_default_channel_password", &channel_password)
.arg("client_server_password", &server_password)
.arg("client_meta_data", "")
.arg(
"client_version_sign",
"a1OYzvM18mrmfUQBUgxYBxYz2DUU6y5k3/mEL6FurzU0y97Bd1FL7+PRpcHyPkg4R+kKAFZ1nhyzbgkGphDWDg==",
)
.arg("client_key_offset", "0")
.arg("client_nickname_phonetic", "")
.arg(
"client_default_token",
self.config.default_token.as_deref().unwrap_or(""),
)
.arg("hwid", "87056c6e1268aaf5055abf8256415e0e,408978b6d98810cc03f0aa16a4c75600")
.build();
cmd.to_string().into_bytes()
}
/// 编码版本号
fn encode_version(version: &str) -> u32 {
// 从版本字符串提取构建时间戳
if let Some(start) = version.find("[Build: ") {
let rest = &version[start + 8..];
if let Some(end) = rest.find(']') {
let ts_str = &rest[..end];
if let Ok(ts) = ts_str.parse::<u32>() {
return ts;
}
}
}
1466672534 // 默认值
}
/// 获取当前时间戳
fn current_timestamp() -> u32 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.as_secs() as u32
}
/// 解决 RSA 拼图
/// y = x^(2^level) mod n
fn solve_rsa_puzzle(x: &[u8; 64], n: &[u8; 64], level: u32) -> [u8; 64] {
let x_big = num_bigint::BigUint::from_bytes_be(x);
let n_big = num_bigint::BigUint::from_bytes_be(n);
// y = x^(2^level) mod n
// 需要做 level 次平方操作
let mut y = x_big;
for _ in 0..level {
y = (y.clone() * y) % &n_big;
}
let mut result = [0u8; 64];
let bytes = y.to_bytes_be();
let offset = 64 - bytes.len();
result[offset..].copy_from_slice(&bytes);
result
}
/// 获取身份公钥 (omega)
fn get_identity_omega(&self) -> String {
// TODO: 从实际身份获取
"placeholder_omega".to_string()
}
/// 生成证明
fn generate_proof(&self, data: &str, beta: &str) -> String {
// TODO: 使用身份私钥签名
let combined = format!("{}{}", data, beta);
let hash = crypto::sha1(combined.as_bytes());
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_version() {
let version = "3.0.19.3 [Build: 1466672534]";
assert_eq!(Client::encode_version(version), 1466672534);
}
#[test]
fn test_rsa_puzzle() {
// 使用非零值测试
let mut x = [0u8; 64];
x[63] = 2; // x = 2
let mut n = [0u8; 64];
n[63] = 7; // n = 7
// level=0: y = x^(2^0) mod n = x^1 mod n = 2 mod 7 = 2
let y = Client::solve_rsa_puzzle(&x, &n, 0);
assert_eq!(y[63], 2);
// level=1: y = x^(2^1) mod n = x^2 mod n = 4 mod 7 = 4
let y = Client::solve_rsa_puzzle(&x, &n, 1);
assert_eq!(y[63], 4);
// level=2: y = x^(2^2) mod n = x^4 mod n = 16 mod 7 = 2
let y = Client::solve_rsa_puzzle(&x, &n, 2);
assert_eq!(y[63], 2);
}
}
+9
View File
@@ -0,0 +1,9 @@
//! 连接管理
pub mod client;
pub mod state;
pub mod resend;
pub use client::*;
pub use state::*;
pub use resend::*;
+258
View File
@@ -0,0 +1,258 @@
//! 数据包重传和确认系统
use std::collections::BTreeMap;
use std::time::{Duration, Instant};
use crate::protocol::PacketType;
/// 数据包 ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct PacketId {
pub generation_id: u32,
pub packet_id: u16,
}
impl PacketId {
pub fn new(generation_id: u32, packet_id: u16) -> Self {
Self { generation_id, packet_id }
}
pub fn increment(&mut self) {
let (new_id, overflow) = self.packet_id.overflowing_add(1);
self.packet_id = new_id;
if overflow {
self.generation_id += 1;
}
}
}
/// 已发送的数据包信息
#[derive(Debug, Clone)]
pub struct SentPacket {
pub data: Vec<u8>,
pub sent_at: Instant,
pub retry_count: u32,
pub timeout: Duration,
}
impl SentPacket {
pub fn new(data: Vec<u8>) -> Self {
Self {
data,
sent_at: Instant::now(),
retry_count: 0,
timeout: Duration::from_millis(500), // 初始超时 500ms
}
}
pub fn is_expired(&self) -> bool {
self.sent_at.elapsed() > self.timeout
}
pub fn should_retry(&self, max_retries: u32) -> bool {
self.is_expired() && self.retry_count < max_retries
}
pub fn retry(&mut self) {
self.retry_count += 1;
self.sent_at = Instant::now();
// 指数退避
self.timeout = Duration::from_millis(500 * (1 << self.retry_count).min(32));
}
}
/// 重传管理器
pub struct ResendManager {
/// 等待确认的数据包
pending: BTreeMap<PacketId, SentPacket>,
/// 最大重试次数
max_retries: u32,
/// 连接超时
connection_timeout: Duration,
}
impl ResendManager {
pub fn new() -> Self {
Self {
pending: BTreeMap::new(),
max_retries: 10,
connection_timeout: Duration::from_secs(30),
}
}
/// 添加已发送的数据包
pub fn add_sent(&mut self, id: PacketId, data: Vec<u8>) {
self.pending.insert(id, SentPacket::new(data));
}
/// 确认数据包
pub fn ack(&mut self, id: &PacketId) -> bool {
self.pending.remove(id).is_some()
}
/// 获取需要重传的数据包
pub fn get_retransmissions(&mut self) -> Vec<(PacketId, Vec<u8>)> {
let mut retransmissions = Vec::new();
let mut to_retry = Vec::new();
for (id, packet) in self.pending.iter() {
if packet.should_retry(self.max_retries) {
to_retry.push(*id);
}
}
for id in to_retry {
if let Some(packet) = self.pending.get_mut(&id) {
packet.retry();
retransmissions.push((id, packet.data.clone()));
}
}
retransmissions
}
/// 检查是否连接超时
pub fn is_connection_timeout(&self) -> bool {
self.pending.values().any(|p| p.sent_at.elapsed() > self.connection_timeout)
}
/// 获取待确认数据包数量
pub fn pending_count(&self) -> usize {
self.pending.len()
}
/// 清空所有待确认数据包
pub fn clear(&mut self) {
self.pending.clear();
}
/// 设置最大重试次数
pub fn set_max_retries(&mut self, max_retries: u32) {
self.max_retries = max_retries;
}
/// 设置连接超时
pub fn set_connection_timeout(&mut self, timeout: Duration) {
self.connection_timeout = timeout;
}
}
impl Default for ResendManager {
fn default() -> Self {
Self::new()
}
}
/// RTT 估算器
pub struct RttEstimator {
srtt: Duration,
rtt_var: Duration,
rto: Duration,
}
impl RttEstimator {
pub fn new() -> Self {
Self {
srtt: Duration::from_millis(500),
rtt_var: Duration::from_millis(250),
rto: Duration::from_millis(1000),
}
}
/// 更新 RTT 估算
pub fn update(&mut self, measured_rtt: Duration) {
let alpha = 0.125;
let beta = 0.25;
let diff = if measured_rtt > self.srtt {
measured_rtt - self.srtt
} else {
self.srtt - measured_rtt
};
self.rtt_var = Duration::from_secs_f64(
(1.0 - beta) * self.rtt_var.as_secs_f64() + beta * diff.as_secs_f64(),
);
self.srtt = Duration::from_secs_f64(
(1.0 - alpha) * self.srtt.as_secs_f64() + alpha * measured_rtt.as_secs_f64(),
);
self.rto = self.srtt + self.rtt_var * 4;
// 限制 RTO 范围
if self.rto < Duration::from_millis(100) {
self.rto = Duration::from_millis(100);
}
if self.rto > Duration::from_secs(60) {
self.rto = Duration::from_secs(60);
}
}
/// 获取当前 RTO
pub fn rto(&self) -> Duration {
self.rto
}
/// 获取平滑 RTT
pub fn srtt(&self) -> Duration {
self.srtt
}
}
impl Default for RttEstimator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resend_manager() {
let mut manager = ResendManager::new();
let id = PacketId::new(0, 1);
manager.add_sent(id, vec![1, 2, 3]);
assert_eq!(manager.pending_count(), 1);
// 确认
assert!(manager.ack(&id));
assert_eq!(manager.pending_count(), 0);
}
#[test]
fn test_rtt_estimator() {
let mut estimator = RttEstimator::new();
// 初始 SRTT 是 500ms
assert_eq!(estimator.srtt(), Duration::from_millis(500));
// 更新多次,SRTT 应该逐渐收敛
for _ in 0..100 {
estimator.update(Duration::from_millis(100));
}
// 经过多次更新后,SRTT 应该接近 100ms
assert!(estimator.srtt() < Duration::from_millis(150));
// RTO 应该大于 SRTT
assert!(estimator.rto() > estimator.srtt());
}
#[test]
fn test_sent_packet_retry() {
let mut packet = SentPacket::new(vec![1, 2, 3]);
assert!(!packet.is_expired());
// 模拟超时
packet.sent_at = Instant::now() - Duration::from_millis(600);
assert!(packet.is_expired());
assert!(packet.should_retry(10));
packet.retry();
assert_eq!(packet.retry_count, 1);
assert!(!packet.is_expired());
}
}
+90
View File
@@ -0,0 +1,90 @@
//! 连接状态管理
use std::fmt;
/// 连接状态
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionState {
Disconnected,
Connecting,
IdentityLevelIncreasing,
Connected,
ChannelListFinished,
DisconnectedTemporarily,
Error,
}
impl ConnectionState {
pub fn is_connected(&self) -> bool {
matches!(self, Self::Connected | Self::ChannelListFinished)
}
pub fn is_connecting(&self) -> bool {
matches!(self, Self::Connecting | Self::IdentityLevelIncreasing)
}
pub fn is_disconnected(&self) -> bool {
matches!(self, Self::Disconnected | Self::Error)
}
}
impl fmt::Display for ConnectionState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Disconnected => write!(f, "Disconnected"),
Self::Connecting => write!(f, "Connecting"),
Self::IdentityLevelIncreasing => write!(f, "IdentityLevelIncreasing"),
Self::Connected => write!(f, "Connected"),
Self::ChannelListFinished => write!(f, "ChannelListFinished"),
Self::DisconnectedTemporarily => write!(f, "DisconnectedTemporarily"),
Self::Error => write!(f, "Error"),
}
}
}
/// 连接状态机
pub struct ConnectionStateMachine {
state: ConnectionState,
}
impl ConnectionStateMachine {
pub fn new() -> Self {
Self {
state: ConnectionState::Disconnected,
}
}
pub fn state(&self) -> ConnectionState {
self.state
}
pub fn transition(&mut self, new_state: ConnectionState) -> Result<(), String> {
let valid = matches!(
(self.state, new_state),
(ConnectionState::Disconnected, ConnectionState::Connecting)
| (ConnectionState::Connecting, ConnectionState::IdentityLevelIncreasing)
| (ConnectionState::Connecting, ConnectionState::Connected)
| (ConnectionState::IdentityLevelIncreasing, ConnectionState::Connected)
| (ConnectionState::Connected, ConnectionState::ChannelListFinished)
| (ConnectionState::Connected, ConnectionState::DisconnectedTemporarily)
| (ConnectionState::ChannelListFinished, ConnectionState::DisconnectedTemporarily)
| (ConnectionState::DisconnectedTemporarily, ConnectionState::Connected)
| (ConnectionState::DisconnectedTemporarily, ConnectionState::Disconnected)
| (_, ConnectionState::Error)
| (ConnectionState::Error, ConnectionState::Disconnected)
);
if valid {
self.state = new_state;
Ok(())
} else {
Err(format!("Invalid state transition: {} -> {}", self.state, new_state))
}
}
}
impl Default for ConnectionStateMachine {
fn default() -> Self {
Self::new()
}
}
+118
View File
@@ -0,0 +1,118 @@
//! EAX 模式加密
use aes::Aes128;
use eax::aead::consts::U8;
use eax::{AeadInPlace, Eax, KeyInit};
use generic_array::GenericArray;
use super::keys;
use crate::protocol::{InPacket, OutPacket};
use crate::ProtocolError;
/// EAX 加密器
pub struct EaxCipher {
cipher: Eax<Aes128, U8>,
}
impl EaxCipher {
pub fn new(key: &[u8; 16]) -> Self {
let key = GenericArray::from_slice(key);
Self {
cipher: Eax::<Aes128, U8>::new(key),
}
}
pub fn encrypt(
&self,
nonce: &[u8; 16],
header: &[u8],
data: &mut [u8],
) -> Result<[u8; 8], ProtocolError> {
let nonce = GenericArray::from_slice(nonce);
let tag = self
.cipher
.encrypt_in_place_detached(nonce, header, data)
.map_err(|_| ProtocolError::Encryption("EAX 加密失败".to_string()))?;
let mut mac = [0u8; 8];
mac.copy_from_slice(&tag[..8]);
Ok(mac)
}
pub fn decrypt(
&self,
nonce: &[u8; 16],
header: &[u8],
data: &mut [u8],
mac: &[u8; 8],
) -> Result<(), ProtocolError> {
let nonce = GenericArray::from_slice(nonce);
let tag = GenericArray::from_slice(mac);
self.cipher
.decrypt_in_place_detached(nonce, header, data, tag)
.map_err(|_| ProtocolError::Decryption("MAC 验证失败".to_string()))
}
}
/// 加密数据包
pub fn encrypt_packet(
packet: &mut OutPacket,
generation_id: u32,
iv: &[u8; 64],
key_cache: &mut keys::KeyCache,
) -> Result<(), ProtocolError> {
let packet_type = packet.header.flags.packet_type();
let direction = packet.direction;
let packet_id = packet.header.packet_id;
let (key, nonce) = key_cache.get_or_create(packet_type, direction, generation_id, iv);
let enc_key = keys::create_encryption_key(&key, packet_id);
let cipher = EaxCipher::new(&enc_key);
let meta = packet.header.get_meta(direction);
let mac = cipher.encrypt(&nonce, &meta, &mut packet.data)?;
packet.header.mac = mac;
Ok(())
}
/// 解密数据包
pub fn decrypt_packet(
packet: &InPacket,
generation_id: u32,
iv: &[u8; 64],
key_cache: &mut keys::KeyCache,
) -> Result<Vec<u8>, ProtocolError> {
let packet_type = packet.header.flags.packet_type();
let direction = packet.direction;
let packet_id = packet.header.packet_id;
let (key, nonce) = key_cache.get_or_create(packet_type, direction, generation_id, iv);
let enc_key = keys::create_encryption_key(&key, packet_id);
let cipher = EaxCipher::new(&enc_key);
let meta = packet.header.get_meta(direction);
let mut data = packet.data.clone();
cipher.decrypt(&nonce, &meta, &mut data, &packet.header.mac)?;
Ok(data)
}
/// 假加密
pub fn encrypt_fake(packet: &mut OutPacket) -> Result<(), ProtocolError> {
let cipher = EaxCipher::new(&keys::FAKE_KEY);
let meta = packet.header.get_meta(packet.direction);
let mac = cipher.encrypt(&keys::FAKE_NONCE, &meta, &mut packet.data)?;
packet.header.mac = mac;
Ok(())
}
/// 假解密
pub fn decrypt_fake(packet: &InPacket) -> Result<Vec<u8>, ProtocolError> {
let cipher = EaxCipher::new(&keys::FAKE_KEY);
let meta = packet.header.get_meta(packet.direction);
let mut data = packet.data.clone();
cipher.decrypt(&keys::FAKE_NONCE, &meta, &mut data, &packet.header.mac)?;
Ok(data)
}
+40
View File
@@ -0,0 +1,40 @@
//! 哈希函数
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
/// SHA-1 哈希
pub fn sha1(data: &[u8]) -> [u8; 20] {
let mut hasher = Sha1::new();
hasher.update(data);
let result = hasher.finalize();
let mut hash = [0u8; 20];
hash.copy_from_slice(&result);
hash
}
/// SHA-256 哈希
pub fn sha256(data: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
let mut hash = [0u8; 32];
hash.copy_from_slice(&result);
hash
}
/// SHA-512 哈希
pub fn sha512(data: &[u8]) -> [u8; 64] {
let mut hasher = Sha512::new();
hasher.update(data);
let result = hasher.finalize();
let mut hash = [0u8; 64];
hash.copy_from_slice(&result);
hash
}
/// 计算密码哈希
pub fn hash_password(password: &str) -> String {
let hash = sha1(password.as_bytes());
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash)
}
+229
View File
@@ -0,0 +1,229 @@
//! 密钥管理
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
use crate::protocol::PacketType;
use crate::protocol::Direction;
/// 假加密密钥
pub const FAKE_KEY: [u8; 16] = *b"c:\\windows\\syste";
/// 假加密 Nonce
pub const FAKE_NONCE: [u8; 16] = *b"m\\firewall32.cpl";
/// 许可证根密钥
pub const ROOT_KEY: [u8; 32] = [
0xcd, 0x0d, 0xe2, 0xae, 0xd4, 0x63, 0x45, 0x50, 0x9a, 0x7e, 0x3c, 0xfd, 0x8f, 0x68, 0xb3,
0xdc, 0x75, 0x55, 0xb2, 0x9d, 0xcc, 0xec, 0x73, 0xcd, 0x18, 0x75, 0x0f, 0x99, 0x38, 0x12,
0x40, 0x8a,
];
/// 共享密钥
#[derive(Clone)]
pub struct SharedSecret {
pub iv: [u8; 64],
pub mac: [u8; 8],
}
impl SharedSecret {
pub fn new(iv: [u8; 64], mac: [u8; 8]) -> Self {
Self { iv, mac }
}
pub fn compute_old(alpha: &[u8; 10], beta: &[u8; 10], shared_data: &[u8; 32]) -> Self {
let mut hasher = Sha1::new();
hasher.update(shared_data);
let hash = hasher.finalize();
let mut iv = [0u8; 64];
iv[..20].copy_from_slice(&hash);
for i in 0..10 {
iv[i] ^= alpha[i];
}
for i in 0..10 {
iv[i + 10] ^= beta[i];
}
let mut hasher = Sha1::new();
hasher.update(&iv);
let mac_hash = hasher.finalize();
let mut mac = [0u8; 8];
mac.copy_from_slice(&mac_hash[..8]);
Self::new(iv, mac)
}
pub fn compute_new(alpha: &[u8; 10], beta: &[u8; 54], shared_data: &[u8; 32]) -> Self {
let mut hasher = Sha512::new();
hasher.update(shared_data);
let hash = hasher.finalize();
let mut iv = [0u8; 64];
iv.copy_from_slice(&hash);
for i in 0..10 {
iv[i] ^= alpha[i];
}
for i in 0..54 {
iv[i + 10] ^= beta[i];
}
let mut hasher = Sha1::new();
hasher.update(&iv);
let mac_hash = hasher.finalize();
let mut mac = [0u8; 8];
mac.copy_from_slice(&mac_hash[..8]);
Self::new(iv, mac)
}
}
impl std::fmt::Debug for SharedSecret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SharedSecret {{ iv: [hidden], mac: [hidden] }}")
}
}
/// 缓存的密钥
#[derive(Debug, Clone)]
pub struct CachedKey {
pub generation_id: u32,
pub key: [u8; 16],
pub nonce: [u8; 16],
}
impl CachedKey {
pub fn new() -> Self {
Self {
generation_id: u32::MAX,
key: [0; 16],
nonce: [0; 16],
}
}
pub fn is_valid(&self, generation_id: u32) -> bool {
self.generation_id == generation_id
}
}
impl Default for CachedKey {
fn default() -> Self {
Self::new()
}
}
/// 密钥缓存
pub struct KeyCache {
cache: [[CachedKey; 2]; 8],
}
impl KeyCache {
pub fn new() -> Self {
Self {
cache: Default::default(),
}
}
pub fn get_or_create(
&mut self,
packet_type: PacketType,
direction: Direction,
generation_id: u32,
iv: &[u8; 64],
) -> ([u8; 16], [u8; 16]) {
let type_idx = packet_type.to_usize();
let dir_idx = match direction {
Direction::C2S => 1,
Direction::S2C => 0,
};
let cached = &mut self.cache[type_idx][dir_idx];
if !cached.is_valid(generation_id) {
let (key, nonce) = create_key_nonce(packet_type, direction, generation_id, iv);
cached.generation_id = generation_id;
cached.key = key;
cached.nonce = nonce;
}
(cached.key, cached.nonce)
}
pub fn invalidate(&mut self) {
self.cache = Default::default();
}
}
impl Default for KeyCache {
fn default() -> Self {
Self::new()
}
}
/// 创建密钥和 Nonce
pub fn create_key_nonce(
packet_type: PacketType,
direction: Direction,
generation_id: u32,
iv: &[u8; 64],
) -> ([u8; 16], [u8; 16]) {
let mut temp = [0u8; 70];
temp[0] = match direction {
Direction::C2S => 0x31,
Direction::S2C => 0x30,
};
temp[1] = packet_type.to_u8();
temp[2..6].copy_from_slice(&generation_id.to_be_bytes());
temp[6..].copy_from_slice(iv);
let mut hasher = Sha256::new();
hasher.update(&temp);
let hash = hasher.finalize();
let mut key = [0u8; 16];
let mut nonce = [0u8; 16];
key.copy_from_slice(&hash[..16]);
nonce.copy_from_slice(&hash[16..]);
(key, nonce)
}
/// 创建用于加密的密钥
pub fn create_encryption_key(key: &[u8; 16], packet_id: u16) -> [u8; 16] {
let mut result = *key;
result[0] ^= (packet_id >> 8) as u8;
result[1] ^= (packet_id & 0xff) as u8;
result
}
/// 计算 Hash Cash 级别
pub fn get_hash_cash_level(omega: &str, offset: u64) -> u8 {
let mut hasher = Sha1::new();
hasher.update(format!("{}{}", omega, offset).as_bytes());
let hash = hasher.finalize();
let mut level = 0;
for &byte in hash.iter() {
if byte == 0 {
level += 8;
} else {
level += byte.trailing_zeros() as u8;
break;
}
}
level
}
/// 计算 UID
pub fn compute_uid(public_key: &[u8]) -> String {
let mut hasher = Sha1::new();
hasher.update(public_key);
let hash = hasher.finalize();
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash)
}
+10
View File
@@ -0,0 +1,10 @@
//! 加密模块
pub mod eax;
pub mod keys;
pub mod hash;
mod tests;
pub use eax::*;
pub use keys::*;
pub use hash::*;
+156
View File
@@ -0,0 +1,156 @@
//! 加密测试
#[cfg(test)]
mod tests {
use crate::crypto::*;
use crate::protocol::{Direction, Flags, InPacket, OutPacket, PacketType};
#[test]
fn test_sha1() {
let hash = sha1(b"hello");
assert_eq!(hash.len(), 20);
// SHA1("hello") = aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
assert_eq!(hash[0], 0xaa);
}
#[test]
fn test_sha256() {
let hash = sha256(b"hello");
assert_eq!(hash.len(), 32);
}
#[test]
fn test_sha512() {
let hash = sha512(b"hello");
assert_eq!(hash.len(), 64);
}
#[test]
fn test_hash_password() {
let hash = hash_password("password");
assert!(!hash.is_empty());
// base64(sha1("password"))
assert!(hash.contains("=") || hash.len() > 20);
}
#[test]
fn test_create_key_nonce() {
let iv = [0u8; 64];
let (key, nonce) = create_key_nonce(PacketType::Command, Direction::C2S, 0, &iv);
assert_ne!(key, [0u8; 16]);
assert_ne!(nonce, [0u8; 16]);
}
#[test]
fn test_create_encryption_key() {
let key = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10];
let encrypted = create_encryption_key(&key, 0x1234);
assert_eq!(encrypted[0], key[0] ^ 0x12);
assert_eq!(encrypted[1], key[1] ^ 0x34);
// 其他字节不变
assert_eq!(encrypted[2], key[2]);
}
#[test]
fn test_shared_secret_old() {
let alpha = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a];
let beta = [0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14];
let shared_data = [0x15; 32];
let secret = SharedSecret::compute_old(&alpha, &beta, &shared_data);
assert_ne!(secret.iv, [0u8; 64]);
assert_ne!(secret.mac, [0u8; 8]);
}
#[test]
fn test_shared_secret_new() {
let alpha = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a];
let beta = [0x0b; 54];
let shared_data = [0x15; 32];
let secret = SharedSecret::compute_new(&alpha, &beta, &shared_data);
assert_ne!(secret.iv, [0u8; 64]);
assert_ne!(secret.mac, [0u8; 8]);
}
#[test]
fn test_key_cache() {
let mut cache = KeyCache::new();
let iv = [0u8; 64];
let (key1, nonce1) = cache.get_or_create(PacketType::Command, Direction::C2S, 0, &iv);
let (key2, nonce2) = cache.get_or_create(PacketType::Command, Direction::C2S, 0, &iv);
assert_eq!(key1, key2);
assert_eq!(nonce1, nonce2);
// 不同的 generation_id 应该返回不同的密钥
let (key3, _) = cache.get_or_create(PacketType::Command, Direction::C2S, 1, &iv);
assert_ne!(key1, key3);
}
#[test]
fn test_eax_encrypt_decrypt() {
let key = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10];
let nonce = [0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20];
let cipher = EaxCipher::new(&key);
let header = b"test header";
let mut data = b"Hello, World!".to_vec();
// 加密
let mac = cipher.encrypt(&nonce, header, &mut data).unwrap();
// 解密
cipher.decrypt(&nonce, header, &mut data, &mac).unwrap();
assert_eq!(data, b"Hello, World!");
}
#[test]
fn test_fake_encrypt_decrypt() {
let mut packet = OutPacket::new(
Direction::C2S,
Flags::new(PacketType::Command.to_u8()),
b"test data".to_vec(),
);
packet.header.packet_id = 1;
// 假加密
encrypt_fake(&mut packet).unwrap();
// 假解密
let in_packet = InPacket {
direction: Direction::C2S,
header: packet.header.clone(),
data: packet.data.clone(),
};
let decrypted = decrypt_fake(&in_packet).unwrap();
assert_eq!(decrypted, b"test data");
}
#[test]
fn test_hash_cash_level() {
// 测试不同的 offset 产生不同的 level
let level0 = get_hash_cash_level("test_key", 0);
let level1 = get_hash_cash_level("test_key", 1);
// level 应该 >= 0
assert!(level0 >= 0);
assert!(level1 >= 0);
// 使用一个会产生更高 level 的 key
let level_high = get_hash_cash_level("a", 12345);
assert!(level_high >= 0);
}
#[test]
fn test_compute_uid() {
let public_key = b"test_public_key_data";
let uid = compute_uid(public_key);
assert!(!uid.is_empty());
// UID 应该是 base64 编码的 SHA1 哈希
assert!(uid.len() > 20);
}
}
+74
View File
@@ -0,0 +1,74 @@
//! TeamSpeak 3 协议核心实现
pub mod protocol;
pub mod crypto;
pub mod network;
pub mod connection;
pub use protocol::*;
pub use crypto::*;
pub use network::*;
pub use connection::*;
use thiserror::Error;
/// 协议错误
#[derive(Error, Debug)]
pub enum ProtocolError {
#[error("数据包解析错误: {0}")]
PacketParse(String),
#[error("加密错误: {0}")]
Encryption(String),
#[error("解密错误: {0}")]
Decryption(String),
#[error("压缩错误: {0}")]
Compression(String),
#[error("解压错误: {0}")]
Decompression(String),
#[error("无效的数据包类型: {0}")]
InvalidPacketType(u8),
#[error("无效的标志位: {0}")]
InvalidFlags(u8),
#[error("数据包过大: {size} > {max}")]
PacketTooLarge { size: usize, max: usize },
#[error("数据包过小: {size} < {min}")]
PacketTooSmall { size: usize, min: usize },
#[error("无效的客户端 ID: {0}")]
InvalidClientId(u16),
#[error("无效的数据包 ID: {0}")]
InvalidPacketId(u16),
#[error("MAC 验证失败")]
MacVerificationFailed,
#[error("超时: {0}")]
Timeout(String),
#[error("连接关闭")]
ConnectionClosed,
#[error("命令错误: {0}")]
Command(String),
#[error("网络错误: {0}")]
Network(#[from] std::io::Error),
}
/// 协议结果类型
pub type ProtocolResult<T> = Result<T, ProtocolError>;
impl From<protocol::CommandError> for ProtocolError {
fn from(err: protocol::CommandError) -> Self {
ProtocolError::Command(err.to_string())
}
}
+7
View File
@@ -0,0 +1,7 @@
//! 网络模块
pub mod socket;
pub mod resolver;
pub use socket::*;
pub use resolver::*;
+38
View File
@@ -0,0 +1,38 @@
//! 地址解析
use std::net::SocketAddr;
/// 服务器地址
#[derive(Debug, Clone)]
pub enum ServerAddress {
/// 直接 IP 地址
Ip(SocketAddr),
/// 域名
Domain(String),
/// 服务器昵称
Nickname(String),
}
impl ServerAddress {
pub async fn resolve(&self) -> Result<SocketAddr, Box<dyn std::error::Error>> {
match self {
Self::Ip(addr) => Ok(*addr),
Self::Domain(domain) => resolve_domain(domain).await,
Self::Nickname(nickname) => resolve_nickname(nickname).await,
}
}
}
async fn resolve_domain(domain: &str) -> Result<SocketAddr, Box<dyn std::error::Error>> {
// 尝试直接解析
let addrs = tokio::net::lookup_host(format!("{}:9987", domain)).await?;
addrs
.into_iter()
.next()
.ok_or_else(|| "无法解析域名".into())
}
async fn resolve_nickname(nickname: &str) -> Result<SocketAddr, Box<dyn std::error::Error>> {
// TODO: 实现 TSDNS 和昵称解析
resolve_domain(nickname).await
}
+62
View File
@@ -0,0 +1,62 @@
//! UDP Socket 抽象
use std::net::SocketAddr;
use std::task::{Context, Poll};
use tokio::net::UdpSocket;
/// Socket trait
pub trait Socket {
fn poll_recv_from(
&self,
cx: &mut Context,
buf: &mut tokio::io::ReadBuf,
) -> Poll<std::io::Result<SocketAddr>>;
fn poll_send_to(
&self,
cx: &mut Context,
buf: &[u8],
target: SocketAddr,
) -> Poll<std::io::Result<usize>>;
fn local_addr(&self) -> std::io::Result<SocketAddr>;
}
/// UDP Socket 实现
pub struct UdpSocketWrapper {
socket: UdpSocket,
}
impl UdpSocketWrapper {
pub async fn bind(addr: SocketAddr) -> std::io::Result<Self> {
let socket = UdpSocket::bind(addr).await?;
Ok(Self { socket })
}
pub async fn connect(&self, addr: SocketAddr) -> std::io::Result<()> {
self.socket.connect(addr).await
}
}
impl Socket for UdpSocketWrapper {
fn poll_recv_from(
&self,
cx: &mut Context,
buf: &mut tokio::io::ReadBuf,
) -> Poll<std::io::Result<SocketAddr>> {
self.socket.poll_recv_from(cx, buf)
}
fn poll_send_to(
&self,
cx: &mut Context,
buf: &[u8],
target: SocketAddr,
) -> Poll<std::io::Result<usize>> {
self.socket.poll_send_to(cx, buf, target)
}
fn local_addr(&self) -> std::io::Result<SocketAddr> {
self.socket.local_addr()
}
}
+239
View File
@@ -0,0 +1,239 @@
//! 命令解析和序列化
use std::fmt;
/// 命令解析错误
#[derive(Debug, Clone)]
pub enum CommandError {
InvalidFormat(String),
MissingParameter(String),
InvalidParameterValue { name: String, value: String },
EscapeError(String),
}
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidFormat(msg) => write!(f, "无效的命令格式: {}", msg),
Self::MissingParameter(name) => write!(f, "缺少必需的参数: {}", name),
Self::InvalidParameterValue { name, value } => {
write!(f, "无效的参数值: {}={}", name, value)
}
Self::EscapeError(msg) => write!(f, "转义序列错误: {}", msg),
}
}
}
impl std::error::Error for CommandError {}
pub type CommandResult<T> = Result<T, CommandError>;
/// 转义序列处理
pub mod escape {
use super::CommandError;
pub fn escape(input: &str) -> String {
let mut result = String::with_capacity(input.len());
for c in input.chars() {
match c {
'\\' => result.push_str("\\\\"),
' ' => result.push_str("\\s"),
'|' => result.push_str("\\p"),
'/' => result.push_str("\\/"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
_ => result.push(c),
}
}
result
}
pub fn unescape(input: &str) -> Result<String, CommandError> {
let mut result = String::with_capacity(input.len());
let mut chars = input.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('\\') => result.push('\\'),
Some('s') => result.push(' '),
Some('p') => result.push('|'),
Some('/') => result.push('/'),
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some(other) => {
return Err(CommandError::EscapeError(format!("未知的转义序列: \\{}", other)))
}
None => {
return Err(CommandError::EscapeError("意外的转义序列结束".to_string()))
}
}
} else {
result.push(c);
}
}
Ok(result)
}
}
/// 命令参数
#[derive(Debug, Clone)]
pub struct CommandArgument {
pub name: String,
pub value: Option<String>,
}
impl CommandArgument {
pub fn new(name: &str, value: Option<&str>) -> Self {
Self {
name: name.to_string(),
value: value.map(|s| s.to_string()),
}
}
pub fn with_value(name: &str, value: &str) -> Self {
Self {
name: name.to_string(),
value: Some(value.to_string()),
}
}
pub fn without_value(name: &str) -> Self {
Self {
name: name.to_string(),
value: None,
}
}
pub fn to_string(&self) -> String {
match &self.value {
Some(value) => format!("{}={}", escape::escape(&self.name), escape::escape(value)),
None => escape::escape(&self.name),
}
}
}
/// 命令
#[derive(Debug, Clone)]
pub struct Command {
pub name: String,
pub args: Vec<CommandArgument>,
}
impl Command {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
args: Vec::new(),
}
}
pub fn with_args(name: &str, args: Vec<CommandArgument>) -> Self {
Self {
name: name.to_string(),
args,
}
}
pub fn arg(mut self, arg: CommandArgument) -> Self {
self.args.push(arg);
self
}
pub fn key_value(mut self, name: &str, value: &str) -> Self {
self.args.push(CommandArgument::with_value(name, value));
self
}
pub fn flag(mut self, name: &str) -> Self {
self.args.push(CommandArgument::without_value(name));
self
}
pub fn get(&self, name: &str) -> Option<&str> {
self.args
.iter()
.find(|a| a.name == name)
.and_then(|a| a.value.as_deref())
}
pub fn has(&self, name: &str) -> bool {
self.args.iter().any(|a| a.name == name)
}
pub fn parse(input: &str) -> CommandResult<Self> {
let input = input.trim();
if input.is_empty() {
return Err(CommandError::InvalidFormat("空命令".to_string()));
}
let parts: Vec<&str> = input.splitn(2, ' ').collect();
let name = parts[0].to_string();
let args_str = if parts.len() > 1 { parts[1] } else { "" };
let mut args = Vec::new();
if !args_str.is_empty() {
for arg_str in args_str.split(' ') {
if arg_str.is_empty() {
continue;
}
if let Some(eq_pos) = arg_str.find('=') {
let name = escape::unescape(&arg_str[..eq_pos])?;
let value = escape::unescape(&arg_str[eq_pos + 1..])?;
args.push(CommandArgument::with_value(&name, &value));
} else {
let name = escape::unescape(arg_str)?;
args.push(CommandArgument::without_value(&name));
}
}
}
Ok(Self { name, args })
}
pub fn to_string(&self) -> String {
let mut result = self.name.clone();
for arg in &self.args {
result.push(' ');
result.push_str(&arg.to_string());
}
result
}
}
impl fmt::Display for Command {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string())
}
}
/// 命令构建器
pub struct CommandBuilder {
command: Command,
}
impl CommandBuilder {
pub fn new(name: &str) -> Self {
Self {
command: Command::new(name),
}
}
pub fn arg(mut self, name: &str, value: &str) -> Self {
self.command = self.command.key_value(name, value);
self
}
pub fn flag(mut self, name: &str) -> Self {
self.command = self.command.flag(name);
self
}
pub fn build(self) -> Command {
self.command
}
}
+10
View File
@@ -0,0 +1,10 @@
//! 协议模块
pub mod packet;
pub mod types;
pub mod commands;
mod tests;
pub use packet::*;
pub use types::*;
pub use commands::*;
+607
View File
@@ -0,0 +1,607 @@
//! 数据包定义和处理
use std::fmt;
use super::types::*;
use crate::ProtocolError;
/// 最大数据包大小
pub const MAX_PACKET_SIZE: usize = 500;
/// C2S 头部大小
pub const C2S_HEADER_SIZE: usize = 13; // 8 (MAC) + 2 (PId) + 2 (CId) + 1 (PT)
/// S2C 头部大小
pub const S2C_HEADER_SIZE: usize = 11; // 8 (MAC) + 2 (PId) + 1 (PT)
/// 数据包方向
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
C2S,
S2C,
}
impl Direction {
pub fn reverse(&self) -> Self {
match self {
Self::C2S => Self::S2C,
Self::S2C => Self::C2S,
}
}
}
/// 数据包标志位
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Flags(pub u8);
impl Flags {
pub const UNENCRYPTED: u8 = 0x80;
pub const COMPRESSED: u8 = 0x40;
pub const NEWPROTOCOL: u8 = 0x20;
pub const FRAGMENTED: u8 = 0x10;
pub fn new(flags: u8) -> Self {
Self(flags)
}
pub fn empty() -> Self {
Self(0)
}
pub fn is_unencrypted(&self) -> bool {
self.0 & Self::UNENCRYPTED != 0
}
pub fn is_compressed(&self) -> bool {
self.0 & Self::COMPRESSED != 0
}
pub fn is_newprotocol(&self) -> bool {
self.0 & Self::NEWPROTOCOL != 0
}
pub fn is_fragmented(&self) -> bool {
self.0 & Self::FRAGMENTED != 0
}
pub fn packet_type(&self) -> PacketType {
PacketType::from_u8(self.0 & 0x0F)
}
pub fn set_unencrypted(&mut self, value: bool) {
if value {
self.0 |= Self::UNENCRYPTED;
} else {
self.0 &= !Self::UNENCRYPTED;
}
}
pub fn set_compressed(&mut self, value: bool) {
if value {
self.0 |= Self::COMPRESSED;
} else {
self.0 &= !Self::COMPRESSED;
}
}
pub fn set_newprotocol(&mut self, value: bool) {
if value {
self.0 |= Self::NEWPROTOCOL;
} else {
self.0 &= !Self::NEWPROTOCOL;
}
}
pub fn set_fragmented(&mut self, value: bool) {
if value {
self.0 |= Self::FRAGMENTED;
} else {
self.0 &= !Self::FRAGMENTED;
}
}
}
impl fmt::Display for Flags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Flags({:08b}: UE={}, CP={}, NP={}, FR={}, Type={:?})",
self.0,
self.is_unencrypted(),
self.is_compressed(),
self.is_newprotocol(),
self.is_fragmented(),
self.packet_type()
)
}
}
/// 数据包头部
#[derive(Debug, Clone)]
pub struct Header {
pub mac: [u8; 8],
pub packet_id: u16,
pub client_id: Option<u16>,
pub flags: Flags,
}
impl Header {
pub fn parse_c2s(data: &[u8]) -> Result<Self, ProtocolError> {
if data.len() < C2S_HEADER_SIZE {
return Err(ProtocolError::PacketTooSmall {
size: data.len(),
min: C2S_HEADER_SIZE,
});
}
let mut mac = [0u8; 8];
mac.copy_from_slice(&data[0..8]);
let packet_id = u16::from_be_bytes([data[8], data[9]]);
let client_id = u16::from_be_bytes([data[10], data[11]]);
let flags = Flags::new(data[12]);
Ok(Self {
mac,
packet_id,
client_id: Some(client_id),
flags,
})
}
pub fn parse_s2c(data: &[u8]) -> Result<Self, ProtocolError> {
if data.len() < S2C_HEADER_SIZE {
return Err(ProtocolError::PacketTooSmall {
size: data.len(),
min: S2C_HEADER_SIZE,
});
}
let mut mac = [0u8; 8];
mac.copy_from_slice(&data[0..8]);
let packet_id = u16::from_be_bytes([data[8], data[9]]);
let flags = Flags::new(data[10]);
Ok(Self {
mac,
packet_id,
client_id: None,
flags,
})
}
pub fn to_c2s_bytes(&self) -> [u8; C2S_HEADER_SIZE] {
let mut bytes = [0u8; C2S_HEADER_SIZE];
bytes[0..8].copy_from_slice(&self.mac);
bytes[8..10].copy_from_slice(&self.packet_id.to_be_bytes());
if let Some(client_id) = self.client_id {
bytes[10..12].copy_from_slice(&client_id.to_be_bytes());
}
bytes[12] = self.flags.0;
bytes
}
pub fn to_s2c_bytes(&self) -> [u8; S2C_HEADER_SIZE] {
let mut bytes = [0u8; S2C_HEADER_SIZE];
bytes[0..8].copy_from_slice(&self.mac);
bytes[8..10].copy_from_slice(&self.packet_id.to_be_bytes());
bytes[10] = self.flags.0;
bytes
}
pub fn size(&self, direction: Direction) -> usize {
match direction {
Direction::C2S => C2S_HEADER_SIZE,
Direction::S2C => S2C_HEADER_SIZE,
}
}
pub fn get_meta(&self, direction: Direction) -> Vec<u8> {
match direction {
Direction::C2S => {
let mut meta = Vec::with_capacity(5);
meta.extend_from_slice(&self.packet_id.to_be_bytes());
meta.extend_from_slice(&self.client_id.unwrap_or(0).to_be_bytes());
meta.push(self.flags.0);
meta
}
Direction::S2C => {
let mut meta = Vec::with_capacity(3);
meta.extend_from_slice(&self.packet_id.to_be_bytes());
meta.push(self.flags.0);
meta
}
}
}
}
/// 输入数据包
#[derive(Debug, Clone)]
pub struct InPacket {
pub direction: Direction,
pub header: Header,
pub data: Vec<u8>,
}
impl InPacket {
pub fn parse(direction: Direction, data: &[u8]) -> Result<Self, ProtocolError> {
let header = match direction {
Direction::C2S => Header::parse_c2s(data)?,
Direction::S2C => Header::parse_s2c(data)?,
};
let header_size = header.size(direction);
let content = data[header_size..].to_vec();
Ok(Self {
direction,
header,
data: content,
})
}
pub fn content(&self) -> &[u8] {
&self.data
}
pub fn content_size(&self) -> usize {
self.data.len()
}
pub fn total_size(&self) -> usize {
self.header.size(self.direction) + self.data.len()
}
}
/// 输出数据包
#[derive(Debug, Clone)]
pub struct OutPacket {
pub direction: Direction,
pub header: Header,
pub data: Vec<u8>,
}
impl OutPacket {
pub fn new(direction: Direction, flags: Flags, content: Vec<u8>) -> Self {
let header = Header {
mac: [0; 8],
packet_id: 0,
client_id: if direction == Direction::C2S { Some(0) } else { None },
flags,
};
Self {
direction,
header,
data: content,
}
}
pub fn set_packet_id(&mut self, id: u16) {
self.header.packet_id = id;
}
pub fn set_client_id(&mut self, id: u16) {
self.header.client_id = Some(id);
}
pub fn set_mac(&mut self, mac: [u8; 8]) {
self.header.mac = mac;
}
pub fn content(&self) -> &[u8] {
&self.data
}
pub fn content_mut(&mut self) -> &mut Vec<u8> {
&mut self.data
}
pub fn to_bytes(&self) -> Vec<u8> {
let header_size = self.header.size(self.direction);
let mut bytes = Vec::with_capacity(header_size + self.data.len());
match self.direction {
Direction::C2S => {
bytes.extend_from_slice(&self.header.to_c2s_bytes());
}
Direction::S2C => {
bytes.extend_from_slice(&self.header.to_s2c_bytes());
}
}
bytes.extend_from_slice(&self.data);
bytes
}
pub fn total_size(&self) -> usize {
self.header.size(self.direction) + self.data.len()
}
}
/// 确认数据包
#[derive(Debug, Clone)]
pub struct AckPacket {
pub direction: Direction,
pub packet_type: PacketType,
pub acked_packet_id: u16,
}
impl AckPacket {
pub fn new(direction: Direction, packet_type: PacketType, acked_packet_id: u16) -> Self {
Self {
direction,
packet_type,
acked_packet_id,
}
}
pub fn to_out_packet(&self) -> OutPacket {
let flags = Flags::new(self.packet_type.to_u8());
let mut content = Vec::with_capacity(2);
content.extend_from_slice(&self.acked_packet_id.to_be_bytes());
OutPacket::new(self.direction, flags, content)
}
}
/// 初始化步骤
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InitStep {
Init0,
Init1,
Init2,
Init3,
Init4,
Reset,
}
/// 初始化数据包
#[derive(Debug, Clone)]
pub struct InitPacket {
pub step: InitStep,
pub version: Option<u32>,
pub timestamp: Option<u32>,
pub random0: Option<[u8; 4]>,
pub random1: Option<[u8; 16]>,
pub random0_r: Option<[u8; 4]>,
pub x: Option<[u8; 64]>,
pub n: Option<[u8; 64]>,
pub level: Option<u32>,
pub random2: Option<[u8; 100]>,
pub y: Option<[u8; 64]>,
pub command: Option<Vec<u8>>,
}
impl InitPacket {
pub fn parse(data: &[u8]) -> Result<Self, ProtocolError> {
if data.is_empty() {
return Err(ProtocolError::PacketTooSmall {
size: 0,
min: 1,
});
}
let step = match data[0] {
0 => InitStep::Init0,
1 => InitStep::Init1,
2 => InitStep::Init2,
3 => InitStep::Init3,
4 => InitStep::Init4,
127 => InitStep::Reset,
_ => return Err(ProtocolError::InvalidPacketType(data[0])),
};
let mut packet = Self {
step,
version: None,
timestamp: None,
random0: None,
random1: None,
random0_r: None,
x: None,
n: None,
level: None,
random2: None,
y: None,
command: None,
};
match step {
InitStep::Init0 => {
if data.len() < 21 {
return Err(ProtocolError::PacketTooSmall { size: data.len(), min: 21 });
}
packet.version = Some(u32::from_be_bytes([data[1], data[2], data[3], data[4]]));
packet.timestamp = Some(u32::from_be_bytes([data[6], data[7], data[8], data[9]]));
let mut random0 = [0u8; 4];
random0.copy_from_slice(&data[10..14]);
packet.random0 = Some(random0);
}
InitStep::Init1 => {
if data.len() < 21 {
return Err(ProtocolError::PacketTooSmall { size: data.len(), min: 21 });
}
let mut random1 = [0u8; 16];
random1.copy_from_slice(&data[1..17]);
packet.random1 = Some(random1);
let mut random0_r = [0u8; 4];
random0_r.copy_from_slice(&data[17..21]);
packet.random0_r = Some(random0_r);
}
InitStep::Init2 => {
if data.len() < 26 {
return Err(ProtocolError::PacketTooSmall { size: data.len(), min: 26 });
}
packet.version = Some(u32::from_be_bytes([data[1], data[2], data[3], data[4]]));
let mut random1 = [0u8; 16];
random1.copy_from_slice(&data[6..22]);
packet.random1 = Some(random1);
let mut random0_r = [0u8; 4];
random0_r.copy_from_slice(&data[22..26]);
packet.random0_r = Some(random0_r);
}
InitStep::Init3 => {
if data.len() < 233 {
return Err(ProtocolError::PacketTooSmall { size: data.len(), min: 233 });
}
let mut x = [0u8; 64];
x.copy_from_slice(&data[1..65]);
packet.x = Some(x);
let mut n = [0u8; 64];
n.copy_from_slice(&data[65..129]);
packet.n = Some(n);
packet.level = Some(u32::from_be_bytes([data[129], data[130], data[131], data[132]]));
let mut random2 = [0u8; 100];
random2.copy_from_slice(&data[133..233]);
packet.random2 = Some(random2);
}
InitStep::Init4 => {
if data.len() < 361 {
return Err(ProtocolError::PacketTooSmall { size: data.len(), min: 361 });
}
packet.version = Some(u32::from_be_bytes([data[1], data[2], data[3], data[4]]));
let mut x = [0u8; 64];
x.copy_from_slice(&data[6..70]);
packet.x = Some(x);
let mut n = [0u8; 64];
n.copy_from_slice(&data[70..134]);
packet.n = Some(n);
packet.level = Some(u32::from_be_bytes([data[134], data[135], data[136], data[137]]));
let mut random2 = [0u8; 100];
random2.copy_from_slice(&data[138..238]);
packet.random2 = Some(random2);
let mut y = [0u8; 64];
y.copy_from_slice(&data[238..302]);
packet.y = Some(y);
if data.len() > 302 {
packet.command = Some(data[302..].to_vec());
}
}
InitStep::Reset => {}
}
Ok(packet)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
match self.step {
InitStep::Init0 => {
bytes.push(0);
if let Some(version) = self.version {
bytes.extend_from_slice(&version.to_be_bytes());
} else {
bytes.extend_from_slice(&[0; 4]);
}
bytes.push(0);
if let Some(timestamp) = self.timestamp {
bytes.extend_from_slice(&timestamp.to_be_bytes());
} else {
bytes.extend_from_slice(&[0; 4]);
}
if let Some(random0) = self.random0 {
bytes.extend_from_slice(&random0);
} else {
bytes.extend_from_slice(&[0; 4]);
}
bytes.extend_from_slice(&[0; 8]);
}
InitStep::Init1 => {
bytes.push(1);
if let Some(random1) = self.random1 {
bytes.extend_from_slice(&random1);
} else {
bytes.extend_from_slice(&[0; 16]);
}
if let Some(random0_r) = self.random0_r {
bytes.extend_from_slice(&random0_r);
} else {
bytes.extend_from_slice(&[0; 4]);
}
}
InitStep::Init2 => {
bytes.push(2);
if let Some(version) = self.version {
bytes.extend_from_slice(&version.to_be_bytes());
} else {
bytes.extend_from_slice(&[0; 4]);
}
if let Some(random1) = self.random1 {
bytes.extend_from_slice(&random1);
} else {
bytes.extend_from_slice(&[0; 16]);
}
if let Some(random0_r) = self.random0_r {
bytes.extend_from_slice(&random0_r);
} else {
bytes.extend_from_slice(&[0; 4]);
}
}
InitStep::Init3 => {
bytes.push(3);
if let Some(x) = self.x {
bytes.extend_from_slice(&x);
} else {
bytes.extend_from_slice(&[0; 64]);
}
if let Some(n) = self.n {
bytes.extend_from_slice(&n);
} else {
bytes.extend_from_slice(&[0; 64]);
}
if let Some(level) = self.level {
bytes.extend_from_slice(&level.to_be_bytes());
} else {
bytes.extend_from_slice(&[0; 4]);
}
if let Some(random2) = self.random2 {
bytes.extend_from_slice(&random2);
} else {
bytes.extend_from_slice(&[0; 100]);
}
}
InitStep::Init4 => {
bytes.push(4);
if let Some(version) = self.version {
bytes.extend_from_slice(&version.to_be_bytes());
} else {
bytes.extend_from_slice(&[0; 4]);
}
if let Some(x) = self.x {
bytes.extend_from_slice(&x);
} else {
bytes.extend_from_slice(&[0; 64]);
}
if let Some(n) = self.n {
bytes.extend_from_slice(&n);
} else {
bytes.extend_from_slice(&[0; 64]);
}
if let Some(level) = self.level {
bytes.extend_from_slice(&level.to_be_bytes());
} else {
bytes.extend_from_slice(&[0; 4]);
}
if let Some(random2) = self.random2 {
bytes.extend_from_slice(&random2);
} else {
bytes.extend_from_slice(&[0; 100]);
}
if let Some(y) = self.y {
bytes.extend_from_slice(&y);
} else {
bytes.extend_from_slice(&[0; 64]);
}
if let Some(ref command) = self.command {
bytes.extend_from_slice(command);
}
}
InitStep::Reset => {
bytes.push(127);
bytes.push(0);
}
}
bytes
}
}
+202
View File
@@ -0,0 +1,202 @@
//! 数据包处理测试
#[cfg(test)]
mod tests {
use crate::protocol::*;
#[test]
fn test_packet_type_conversion() {
assert_eq!(PacketType::from_u8(0x00), PacketType::Voice);
assert_eq!(PacketType::from_u8(0x02), PacketType::Command);
assert_eq!(PacketType::from_u8(0x08), PacketType::Init);
assert_eq!(PacketType::Voice.to_u8(), 0x00);
assert_eq!(PacketType::Command.to_u8(), 0x02);
}
#[test]
fn test_flags() {
let flags = Flags::new(0x80);
assert!(flags.is_unencrypted());
assert!(!flags.is_compressed());
assert!(!flags.is_newprotocol());
assert!(!flags.is_fragmented());
let flags = Flags::new(0x40);
assert!(!flags.is_unencrypted());
assert!(flags.is_compressed());
let flags = Flags::new(0x20);
assert!(flags.is_newprotocol());
let flags = Flags::new(0x10);
assert!(flags.is_fragmented());
let flags = Flags::new(0x02);
assert_eq!(flags.packet_type(), PacketType::Command);
}
#[test]
fn test_header_c2s() {
let mut data = vec![0u8; 13];
// MAC
data[0..8].copy_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
// Packet ID = 42
data[8..10].copy_from_slice(&42u16.to_be_bytes());
// Client ID = 1
data[10..12].copy_from_slice(&1u16.to_be_bytes());
// Flags = Command
data[12] = 0x02;
let header = Header::parse_c2s(&data).unwrap();
assert_eq!(header.packet_id, 42);
assert_eq!(header.client_id, Some(1));
assert_eq!(header.flags.packet_type(), PacketType::Command);
}
#[test]
fn test_header_s2c() {
let mut data = vec![0u8; 11];
// MAC
data[0..8].copy_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
// Packet ID = 10
data[8..10].copy_from_slice(&10u16.to_be_bytes());
// Flags = Voice
data[10] = 0x00;
let header = Header::parse_s2c(&data).unwrap();
assert_eq!(header.packet_id, 10);
assert!(header.client_id.is_none());
assert_eq!(header.flags.packet_type(), PacketType::Voice);
}
#[test]
fn test_in_packet_parse() {
let mut data = vec![0u8; 15];
// S2C header
data[0..8].copy_from_slice(&[0; 8]); // MAC
data[8..10].copy_from_slice(&1u16.to_be_bytes()); // PId
data[10] = 0x02; // Command type
// Content
data[11] = b'H';
data[12] = b'i';
data[13] = b'!';
data[14] = 0;
let packet = InPacket::parse(Direction::S2C, &data).unwrap();
assert_eq!(packet.header.packet_id, 1);
assert_eq!(packet.content(), b"Hi!\0");
}
#[test]
fn test_out_packet() {
let content = b"Hello".to_vec();
let mut packet = OutPacket::new(Direction::C2S, Flags::new(0x02), content);
packet.set_packet_id(42);
packet.set_client_id(1);
let bytes = packet.to_bytes();
assert_eq!(bytes.len(), 13 + 5); // header + content
assert_eq!(packet.header.packet_id, 42);
assert_eq!(packet.header.client_id, Some(1));
}
#[test]
fn test_command_parse() {
let cmd = Command::parse("clientinit client_nickname=Test\\sUser client_version=3.0.19.3").unwrap();
assert_eq!(cmd.name, "clientinit");
assert_eq!(cmd.get("client_nickname"), Some("Test User"));
assert_eq!(cmd.get("client_version"), Some("3.0.19.3"));
}
#[test]
fn test_command_serialize() {
let cmd = Command::new("sendtextmessage")
.key_value("targetmode", "2")
.key_value("msg", "Hello World!");
assert_eq!(cmd.to_string(), "sendtextmessage targetmode=2 msg=Hello\\sWorld!");
}
#[test]
fn test_command_builder() {
let cmd = CommandBuilder::new("clientinit")
.arg("client_nickname", "Test")
.arg("client_version", "3.0.19.3")
.flag("verbose")
.build();
assert_eq!(cmd.name, "clientinit");
assert_eq!(cmd.get("client_nickname"), Some("Test"));
assert!(cmd.has("verbose"));
}
#[test]
fn test_escape_sequences() {
use crate::protocol::commands::escape;
assert_eq!(escape::escape("hello world"), "hello\\sworld");
assert_eq!(escape::escape("a|b"), "a\\pb");
assert_eq!(escape::escape("a\\b"), "a\\\\b");
assert_eq!(escape::unescape("hello\\sworld").unwrap(), "hello world");
assert_eq!(escape::unescape("a\\pb").unwrap(), "a|b");
assert_eq!(escape::unescape("a\\\\b").unwrap(), "a\\b");
}
#[test]
fn test_init_packet_parse() {
// Init0
let mut data = vec![0u8; 21];
data[0] = 0; // step
data[1..5].copy_from_slice(&1466672534u32.to_be_bytes()); // version
data[6..10].copy_from_slice(&1000000u32.to_be_bytes()); // timestamp
data[10..14].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]); // random0
let init = InitPacket::parse(&data).unwrap();
assert_eq!(init.step, InitStep::Init0);
assert_eq!(init.version, Some(1466672534));
assert_eq!(init.random0, Some([0xAA, 0xBB, 0xCC, 0xDD]));
}
#[test]
fn test_init_packet_serialize() {
let init = InitPacket {
step: InitStep::Init0,
version: Some(1466672534),
timestamp: Some(1000000),
random0: Some([0xAA, 0xBB, 0xCC, 0xDD]),
random1: None,
random0_r: None,
x: None,
n: None,
level: None,
random2: None,
y: None,
command: None,
};
let data = init.to_bytes();
assert_eq!(data[0], 0); // step
assert_eq!(data[1..5], 1466672534u32.to_be_bytes());
}
#[test]
fn test_ack_packet() {
let ack = AckPacket::new(Direction::C2S, PacketType::Ack, 42);
let packet = ack.to_out_packet();
assert_eq!(packet.header.flags.packet_type(), PacketType::Ack);
assert_eq!(packet.data, 42u16.to_be_bytes());
}
#[test]
fn test_packet_type_properties() {
assert!(PacketType::Command.must_encrypt());
assert!(!PacketType::Voice.must_encrypt());
assert!(PacketType::Command.can_fragment());
assert!(!PacketType::Voice.can_fragment());
assert!(PacketType::Command.needs_ack());
assert!(!PacketType::Voice.needs_ack());
assert!(PacketType::Voice.is_voice());
assert!(PacketType::VoiceWhisper.is_voice());
assert!(!PacketType::Command.is_voice());
}
}
+227
View File
@@ -0,0 +1,227 @@
//! 协议类型定义
use std::fmt;
/// 数据包类型
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PacketType {
Voice,
VoiceWhisper,
Command,
CommandLow,
Ping,
Pong,
Ack,
AckLow,
Init,
}
impl PacketType {
pub fn from_u8(value: u8) -> Self {
match value {
0x00 => Self::Voice,
0x01 => Self::VoiceWhisper,
0x02 => Self::Command,
0x03 => Self::CommandLow,
0x04 => Self::Ping,
0x05 => Self::Pong,
0x06 => Self::Ack,
0x07 => Self::AckLow,
0x08 => Self::Init,
_ => Self::Init,
}
}
pub fn to_u8(&self) -> u8 {
match self {
Self::Voice => 0x00,
Self::VoiceWhisper => 0x01,
Self::Command => 0x02,
Self::CommandLow => 0x03,
Self::Ping => 0x04,
Self::Pong => 0x05,
Self::Ack => 0x06,
Self::AckLow => 0x07,
Self::Init => 0x08,
}
}
pub fn to_usize(&self) -> usize {
self.to_u8() as usize
}
pub fn is_voice(&self) -> bool {
matches!(self, Self::Voice | Self::VoiceWhisper)
}
pub fn needs_ack(&self) -> bool {
matches!(self, Self::Command | Self::CommandLow | Self::Ping | Self::Init)
}
pub fn can_resend(&self) -> bool {
matches!(self, Self::Command | Self::CommandLow | Self::Ack | Self::AckLow | Self::Init)
}
pub fn can_encrypt(&self) -> bool {
!matches!(self, Self::Init)
}
pub fn must_encrypt(&self) -> bool {
matches!(self, Self::Command | Self::CommandLow)
}
pub fn can_fragment(&self) -> bool {
matches!(self, Self::Command | Self::CommandLow)
}
pub fn can_compress(&self) -> bool {
matches!(self, Self::Command | Self::CommandLow)
}
pub fn ack_type(&self) -> Option<Self> {
match self {
Self::Command => Some(Self::Ack),
Self::CommandLow => Some(Self::AckLow),
Self::Ping => Some(Self::Pong),
Self::Init => Some(Self::Init),
_ => None,
}
}
}
impl fmt::Display for PacketType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Voice => write!(f, "Voice"),
Self::VoiceWhisper => write!(f, "VoiceWhisper"),
Self::Command => write!(f, "Command"),
Self::CommandLow => write!(f, "CommandLow"),
Self::Ping => write!(f, "Ping"),
Self::Pong => write!(f, "Pong"),
Self::Ack => write!(f, "Ack"),
Self::AckLow => write!(f, "AckLow"),
Self::Init => write!(f, "Init"),
}
}
}
/// 编解码器类型
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CodecType {
SpeexNarrowband,
SpeexWideband,
SpeexUltrawideband,
CeltMono,
OpusVoice,
OpusMusic,
}
impl CodecType {
pub fn from_u8(value: u8) -> Self {
match value {
0 => Self::SpeexNarrowband,
1 => Self::SpeexWideband,
2 => Self::SpeexUltrawideband,
3 => Self::CeltMono,
4 => Self::OpusVoice,
5 => Self::OpusMusic,
_ => Self::OpusVoice,
}
}
pub fn to_u8(&self) -> u8 {
match self {
Self::SpeexNarrowband => 0,
Self::SpeexWideband => 1,
Self::SpeexUltrawideband => 2,
Self::CeltMono => 3,
Self::OpusVoice => 4,
Self::OpusMusic => 5,
}
}
pub fn sample_rate(&self) -> u32 {
match self {
Self::SpeexNarrowband => 8000,
Self::SpeexWideband => 16000,
Self::SpeexUltrawideband => 32000,
Self::CeltMono | Self::OpusVoice | Self::OpusMusic => 48000,
}
}
pub fn channels(&self) -> u16 {
match self {
Self::OpusMusic => 2,
_ => 1,
}
}
}
/// 私语类型
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroupWhisperType {
ServerGroup,
ChannelGroup,
ChannelCommander,
AllClients,
}
impl GroupWhisperType {
pub fn from_u8(value: u8) -> Self {
match value {
0 => Self::ServerGroup,
1 => Self::ChannelGroup,
2 => Self::ChannelCommander,
3 => Self::AllClients,
_ => Self::AllClients,
}
}
pub fn to_u8(&self) -> u8 {
match self {
Self::ServerGroup => 0,
Self::ChannelGroup => 1,
Self::ChannelCommander => 2,
Self::AllClients => 3,
}
}
}
/// 私语目标
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroupWhisperTarget {
AllChannels,
CurrentChannel,
ParentChannel,
AllParentChannel,
ChannelFamily,
CompleteChannelFamily,
Subchannels,
}
impl GroupWhisperTarget {
pub fn from_u8(value: u8) -> Self {
match value {
0 => Self::AllChannels,
1 => Self::CurrentChannel,
2 => Self::ParentChannel,
3 => Self::AllParentChannel,
4 => Self::ChannelFamily,
5 => Self::CompleteChannelFamily,
6 => Self::Subchannels,
_ => Self::AllChannels,
}
}
pub fn to_u8(&self) -> u8 {
match self {
Self::AllChannels => 0,
Self::CurrentChannel => 1,
Self::ParentChannel => 2,
Self::AllParentChannel => 3,
Self::ChannelFamily => 4,
Self::CompleteChannelFamily => 5,
Self::Subchannels => 6,
}
}
}
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "tsdb"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "TeamSpeak 数据存储"
[dependencies]
# Error handling
thiserror = { workspace = true }
anyhow = { workspace = true }
# Logging
tracing = { workspace = true }
# Database
rusqlite = { workspace = true }
# Serialization
serde = { workspace = true }
serde_json = { workspace = true }
# Utils
chrono = { workspace = true }
uuid = { workspace = true }
# Internal
shared = { workspace = true }
+176
View File
@@ -0,0 +1,176 @@
//! 书签管理
use rusqlite::params;
use chrono::Utc;
use super::{DatabaseManager, DatabaseResult, DatabaseError};
/// 书签信息
#[derive(Debug, Clone)]
pub struct Bookmark {
pub id: String,
pub name: String,
pub address: String,
pub port: u16,
pub nickname: Option<String>,
pub server_password: Option<String>,
pub channel: Option<String>,
pub channel_password: Option<String>,
pub default_token: Option<String>,
pub auto_connect: bool,
pub last_connected: Option<String>,
pub created_at: String,
pub updated_at: String,
}
impl DatabaseManager {
/// 创建书签
pub fn create_bookmark(
&self,
name: &str,
address: &str,
port: u16,
nickname: Option<&str>,
) -> DatabaseResult<Bookmark> {
let id = uuid::Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
self.connection().execute(
"INSERT INTO bookmarks (id, name, address, port, nickname, auto_connect, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![id, name, address, port, nickname, false, now, now],
)?;
Ok(Bookmark {
id,
name: name.to_string(),
address: address.to_string(),
port,
nickname: nickname.map(|s| s.to_string()),
server_password: None,
channel: None,
channel_password: None,
default_token: None,
auto_connect: false,
last_connected: None,
created_at: now.clone(),
updated_at: now,
})
}
/// 获取书签
pub fn get_bookmark(&self, id: &str) -> DatabaseResult<Bookmark> {
let conn = self.connection();
let mut stmt = conn.prepare(
"SELECT id, name, address, port, nickname, server_password, channel, channel_password, default_token, auto_connect, last_connected, created_at, updated_at FROM bookmarks WHERE id = ?1"
)?;
let bookmark = stmt.query_row(params![id], |row| {
Ok(Bookmark {
id: row.get(0)?,
name: row.get(1)?,
address: row.get(2)?,
port: row.get(3)?,
nickname: row.get(4)?,
server_password: row.get(5)?,
channel: row.get(6)?,
channel_password: row.get(7)?,
default_token: row.get(8)?,
auto_connect: row.get::<_, i32>(9)? != 0,
last_connected: row.get(10)?,
created_at: row.get(11)?,
updated_at: row.get(12)?,
})
}).map_err(|_| DatabaseError::NotFound(format!("书签 {} 未找到", id)))?;
Ok(bookmark)
}
/// 获取所有书签
pub fn get_all_bookmarks(&self) -> DatabaseResult<Vec<Bookmark>> {
let conn = self.connection();
let mut stmt = conn.prepare(
"SELECT id, name, address, port, nickname, server_password, channel, channel_password, default_token, auto_connect, last_connected, created_at, updated_at FROM bookmarks ORDER BY name"
)?;
let bookmarks = stmt.query_map([], |row| {
Ok(Bookmark {
id: row.get(0)?,
name: row.get(1)?,
address: row.get(2)?,
port: row.get(3)?,
nickname: row.get(4)?,
server_password: row.get(5)?,
channel: row.get(6)?,
channel_password: row.get(7)?,
default_token: row.get(8)?,
auto_connect: row.get::<_, i32>(9)? != 0,
last_connected: row.get(10)?,
created_at: row.get(11)?,
updated_at: row.get(12)?,
})
})?.collect::<Result<Vec<_>, _>>()?;
Ok(bookmarks)
}
/// 更新书签
pub fn update_bookmark(
&self,
id: &str,
name: Option<&str>,
address: Option<&str>,
port: Option<u16>,
nickname: Option<&str>,
) -> DatabaseResult<()> {
let now = Utc::now().to_rfc3339();
if let Some(name) = name {
self.connection().execute(
"UPDATE bookmarks SET name = ?1, updated_at = ?2 WHERE id = ?3",
params![name, now, id],
)?;
}
if let Some(address) = address {
self.connection().execute(
"UPDATE bookmarks SET address = ?1, updated_at = ?2 WHERE id = ?3",
params![address, now, id],
)?;
}
if let Some(port) = port {
self.connection().execute(
"UPDATE bookmarks SET port = ?1, updated_at = ?2 WHERE id = ?3",
params![port, now, id],
)?;
}
if let Some(nickname) = nickname {
self.connection().execute(
"UPDATE bookmarks SET nickname = ?1, updated_at = ?2 WHERE id = ?3",
params![nickname, now, id],
)?;
}
Ok(())
}
/// 删除书签
pub fn delete_bookmark(&self, id: &str) -> DatabaseResult<()> {
self.connection().execute(
"DELETE FROM bookmarks WHERE id = ?1",
params![id],
)?;
Ok(())
}
/// 更新最后连接时间
pub fn update_bookmark_last_connected(&self, id: &str) -> DatabaseResult<()> {
let now = Utc::now().to_rfc3339();
self.connection().execute(
"UPDATE bookmarks SET last_connected = ?1, updated_at = ?2 WHERE id = ?3",
params![now, now, id],
)?;
Ok(())
}
}
+50
View File
@@ -0,0 +1,50 @@
//! 配置管理
use rusqlite::params;
use rusqlite::OptionalExtension;
use chrono::Utc;
use super::{DatabaseManager, DatabaseResult};
impl DatabaseManager {
pub fn get_setting(&self, key: &str) -> DatabaseResult<Option<String>> {
let conn = self.connection();
let mut stmt = conn.prepare("SELECT value FROM settings WHERE key = ?1")?;
let result = stmt.query_row(params![key], |row| {
Ok(row.get::<_, String>(0)?)
}).optional()?;
Ok(result)
}
pub fn set_setting(&self, key: &str, value: &str) -> DatabaseResult<()> {
let now = Utc::now().to_rfc3339();
self.connection().execute(
"INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?1, ?2, ?3)",
params![key, value, now],
)?;
Ok(())
}
pub fn delete_setting(&self, key: &str) -> DatabaseResult<()> {
self.connection().execute(
"DELETE FROM settings WHERE key = ?1",
params![key],
)?;
Ok(())
}
pub fn get_all_settings(&self) -> DatabaseResult<Vec<(String, String)>> {
let conn = self.connection();
let mut stmt = conn.prepare("SELECT key, value FROM settings ORDER BY key")?;
let settings = stmt.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?))
})?.collect::<Result<Vec<_>, _>>()?;
Ok(settings)
}
}
+115
View File
@@ -0,0 +1,115 @@
//! 身份管理
use rusqlite::params;
use chrono::Utc;
use super::{DatabaseManager, DatabaseResult, DatabaseError};
/// 身份信息
#[derive(Debug, Clone)]
pub struct Identity {
pub id: String,
pub name: String,
pub private_key: String,
pub counter: u64,
pub max_counter: u64,
pub created_at: String,
pub updated_at: String,
}
impl DatabaseManager {
/// 创建身份
pub fn create_identity(&self, name: &str, private_key: &str) -> DatabaseResult<Identity> {
let id = uuid::Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
self.connection().execute(
"INSERT INTO identities (id, name, private_key, counter, max_counter, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![id, name, private_key, 0, 0, now, now],
)?;
Ok(Identity {
id,
name: name.to_string(),
private_key: private_key.to_string(),
counter: 0,
max_counter: 0,
created_at: now.clone(),
updated_at: now,
})
}
/// 获取身份
pub fn get_identity(&self, id: &str) -> DatabaseResult<Identity> {
let conn = self.connection();
let mut stmt = conn.prepare(
"SELECT id, name, private_key, counter, max_counter, created_at, updated_at FROM identities WHERE id = ?1"
)?;
let identity = stmt.query_row(params![id], |row| {
Ok(Identity {
id: row.get(0)?,
name: row.get(1)?,
private_key: row.get(2)?,
counter: row.get(3)?,
max_counter: row.get(4)?,
created_at: row.get(5)?,
updated_at: row.get(6)?,
})
}).map_err(|_| DatabaseError::NotFound(format!("身份 {} 未找到", id)))?;
Ok(identity)
}
/// 获取所有身份
pub fn get_all_identities(&self) -> DatabaseResult<Vec<Identity>> {
let conn = self.connection();
let mut stmt = conn.prepare(
"SELECT id, name, private_key, counter, max_counter, created_at, updated_at FROM identities ORDER BY name"
)?;
let identities = stmt.query_map([], |row| {
Ok(Identity {
id: row.get(0)?,
name: row.get(1)?,
private_key: row.get(2)?,
counter: row.get(3)?,
max_counter: row.get(4)?,
created_at: row.get(5)?,
updated_at: row.get(6)?,
})
})?.collect::<Result<Vec<_>, _>>()?;
Ok(identities)
}
/// 更新身份
pub fn update_identity(&self, id: &str, name: Option<&str>, counter: Option<u64>) -> DatabaseResult<()> {
let now = Utc::now().to_rfc3339();
if let Some(name) = name {
self.connection().execute(
"UPDATE identities SET name = ?1, updated_at = ?2 WHERE id = ?3",
params![name, now, id],
)?;
}
if let Some(counter) = counter {
self.connection().execute(
"UPDATE identities SET counter = ?1, max_counter = MAX(max_counter, ?1), updated_at = ?2 WHERE id = ?3",
params![counter, now, id],
)?;
}
Ok(())
}
/// 删除身份
pub fn delete_identity(&self, id: &str) -> DatabaseResult<()> {
self.connection().execute(
"DELETE FROM identities WHERE id = ?1",
params![id],
)?;
Ok(())
}
}
+104
View File
@@ -0,0 +1,104 @@
//! 数据存储
pub mod identity;
pub mod bookmark;
pub mod message;
pub mod config;
pub use identity::*;
pub use bookmark::*;
pub use message::*;
use thiserror::Error;
/// 数据库错误
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("SQLite 错误: {0}")]
Sqlite(#[from] rusqlite::Error),
#[error("序列化错误: {0}")]
Serialization(#[from] serde_json::Error),
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
#[error("未找到: {0}")]
NotFound(String),
#[error("已存在: {0}")]
AlreadyExists(String),
}
/// 数据库结果类型
pub type DatabaseResult<T> = Result<T, DatabaseError>;
/// 数据库管理器
pub struct DatabaseManager {
conn: rusqlite::Connection,
}
impl DatabaseManager {
pub fn new(path: &str) -> DatabaseResult<Self> {
let conn = rusqlite::Connection::open(path)?;
let manager = Self { conn };
manager.init_tables()?;
Ok(manager)
}
fn init_tables(&self) -> DatabaseResult<()> {
self.conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS identities (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
private_key TEXT NOT NULL,
counter INTEGER NOT NULL DEFAULT 0,
max_counter INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS bookmarks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
address TEXT NOT NULL,
port INTEGER NOT NULL DEFAULT 9987,
nickname TEXT,
server_password TEXT,
channel TEXT,
channel_password TEXT,
default_token TEXT,
auto_connect INTEGER NOT NULL DEFAULT 0,
last_connected TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_address TEXT NOT NULL,
invoker_id INTEGER NOT NULL,
invoker_name TEXT NOT NULL,
invoker_uid TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id INTEGER,
message TEXT NOT NULL,
is_read INTEGER NOT NULL DEFAULT 0,
timestamp TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
"
)?;
Ok(())
}
pub fn connection(&self) -> &rusqlite::Connection {
&self.conn
}
}
+139
View File
@@ -0,0 +1,139 @@
//! 消息管理
use rusqlite::params;
use chrono::Utc;
use super::{DatabaseManager, DatabaseResult, DatabaseError};
/// 消息信息
#[derive(Debug, Clone)]
pub struct Message {
pub id: i64,
pub server_address: String,
pub invoker_id: i64,
pub invoker_name: String,
pub invoker_uid: String,
pub target_type: String,
pub target_id: Option<i64>,
pub message: String,
pub is_read: bool,
pub timestamp: String,
}
impl DatabaseManager {
/// 创建消息
pub fn create_message(
&self,
server_address: &str,
invoker_id: i64,
invoker_name: &str,
invoker_uid: &str,
target_type: &str,
target_id: Option<i64>,
message: &str,
) -> DatabaseResult<Message> {
let now = Utc::now().to_rfc3339();
self.connection().execute(
"INSERT INTO messages (server_address, invoker_id, invoker_name, invoker_uid, target_type, target_id, message, is_read, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
params![server_address, invoker_id, invoker_name, invoker_uid, target_type, target_id, message, false, now],
)?;
let id = self.connection().last_insert_rowid();
Ok(Message {
id,
server_address: server_address.to_string(),
invoker_id,
invoker_name: invoker_name.to_string(),
invoker_uid: invoker_uid.to_string(),
target_type: target_type.to_string(),
target_id,
message: message.to_string(),
is_read: false,
timestamp: now,
})
}
/// 获取消息
pub fn get_message(&self, id: i64) -> DatabaseResult<Message> {
let conn = self.connection();
let mut stmt = conn.prepare(
"SELECT id, server_address, invoker_id, invoker_name, invoker_uid, target_type, target_id, message, is_read, timestamp FROM messages WHERE id = ?1"
)?;
let message = stmt.query_row(params![id], |row| {
Ok(Message {
id: row.get(0)?,
server_address: row.get(1)?,
invoker_id: row.get(2)?,
invoker_name: row.get(3)?,
invoker_uid: row.get(4)?,
target_type: row.get(5)?,
target_id: row.get(6)?,
message: row.get(7)?,
is_read: row.get::<_, i32>(8)? != 0,
timestamp: row.get(9)?,
})
}).map_err(|_| DatabaseError::NotFound(format!("消息 {} 未找到", id)))?;
Ok(message)
}
/// 获取服务器消息
pub fn get_server_messages(
&self,
server_address: &str,
limit: i64,
offset: i64,
) -> DatabaseResult<Vec<Message>> {
let conn = self.connection();
let mut stmt = conn.prepare(
"SELECT id, server_address, invoker_id, invoker_name, invoker_uid, target_type, target_id, message, is_read, timestamp FROM messages WHERE server_address = ?1 ORDER BY timestamp DESC LIMIT ?2 OFFSET ?3"
)?;
let messages = stmt.query_map(params![server_address, limit, offset], |row| {
Ok(Message {
id: row.get(0)?,
server_address: row.get(1)?,
invoker_id: row.get(2)?,
invoker_name: row.get(3)?,
invoker_uid: row.get(4)?,
target_type: row.get(5)?,
target_id: row.get(6)?,
message: row.get(7)?,
is_read: row.get::<_, i32>(8)? != 0,
timestamp: row.get(9)?,
})
})?.collect::<Result<Vec<_>, _>>()?;
Ok(messages)
}
/// 标记消息为已读
pub fn mark_message_read(&self, id: i64) -> DatabaseResult<()> {
self.connection().execute(
"UPDATE messages SET is_read = 1 WHERE id = ?1",
params![id],
)?;
Ok(())
}
/// 删除消息
pub fn delete_message(&self, id: i64) -> DatabaseResult<()> {
self.connection().execute(
"DELETE FROM messages WHERE id = ?1",
params![id],
)?;
Ok(())
}
/// 清空服务器消息
pub fn clear_server_messages(&self, server_address: &str) -> DatabaseResult<()> {
self.connection().execute(
"DELETE FROM messages WHERE server_address = ?1",
params![server_address],
)?;
Ok(())
}
}