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
730 lines
31 KiB
Markdown
730 lines
31 KiB
Markdown
# Software Design Document (SDD)
|
|
# ReTeamSpeak - Cross-Platform TeamSpeak Client
|
|
|
|
**Version**: 1.0.0
|
|
**Date**: 2026-05-12
|
|
**Status**: Based on actual implementation
|
|
|
|
---
|
|
|
|
## 1. Detailed Design
|
|
|
|
### 1.1 `shared` Module Design
|
|
|
|
#### 1.1.1 Core Type System
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Type Hierarchy │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ Identifier Types (Newtype Pattern) │
|
|
│ ├── ClientId(u16) // In-session client ID │
|
|
│ ├── ChannelId(u64) // Channel identifier │
|
|
│ ├── ServerGroupId(u64) // Server group │
|
|
│ ├── ChannelGroupId(u64) // Channel group │
|
|
│ ├── ClientDbId(u64) // Database client ID │
|
|
│ ├── Uid(String) // Unique identity (base64) │
|
|
│ ├── PermissionId(u32) // Permission ID │
|
|
│ └── IconId(i32) // Icon identifier │
|
|
│ │
|
|
│ Enumerations │
|
|
│ ├── Codec { SpeexNB, SpeexWB, SpeexUWB, Celt, OpusVoice, │
|
|
│ │ OpusMusic } │
|
|
│ ├── ChannelType { Permanent, SemiPermanent, Temporary } │
|
|
│ ├── ClientType { Normal, Query { admin } } │
|
|
│ ├── ConnectionState { Disconnected, Connecting, ... } │
|
|
│ ├── Reason { None, Moved, LostConnection, KickChannel, ... }│
|
|
│ ├── CodecEncryptionMode { PerChannel, ForcedOff, ForcedOn }│
|
|
│ ├── HostMessageMode { None, Log, Modal, Modalquit } │
|
|
│ ├── GroupType { Template, Regular, Query } │
|
|
│ └── GroupNamingMode { None, Before, After } │
|
|
│ │
|
|
│ Data Structures │
|
|
│ ├── ServerInfo { id, name, platform, version, max_clients, │
|
|
│ │ clients_online, ... } │
|
|
│ ├── ChannelInfo { id, parent_id, name, codec, max_clients, │
|
|
│ │ channel_type, ... } │
|
|
│ ├── ClientInfo { id, channel_id, uid, name, muted, ... } │
|
|
│ ├── ChatMessage { id, timestamp, invoker, target, message }│
|
|
│ └── AppConfig { nickname, audio, hotkeys, theme, ... } │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
#### 1.1.2 Event System Design
|
|
|
|
```
|
|
AppEvent
|
|
├── Connection(ConnectionEvent)
|
|
│ ├── Connecting { address }
|
|
│ ├── Connected { server, own_client }
|
|
│ ├── StateChanged { state }
|
|
│ ├── DisconnectedTemporarily { reason }
|
|
│ ├── Disconnected { reason }
|
|
│ └── ConnectionFailed { error }
|
|
│
|
|
├── Client(ClientEvent)
|
|
│ ├── EnteredView { client, reason }
|
|
│ ├── LeftView { client_id, reason, reason_message }
|
|
│ ├── Updated { client_id, changes: ClientChanges }
|
|
│ ├── Moved { client_id, from_channel, to_channel, reason }
|
|
│ ├── StartedTalking { client_id }
|
|
│ ├── StoppedTalking { client_id }
|
|
│ ├── ServerGroupChanged { client_id, group_id, added }
|
|
│ └── ChannelGroupChanged { client_id, group_id }
|
|
│
|
|
├── Channel(ChannelEvent)
|
|
│ ├── Created { channel }
|
|
│ ├── Deleted { channel_id }
|
|
│ ├── Updated { channel_id, changes: ChannelChanges }
|
|
│ ├── Moved { channel_id, new_parent, new_order }
|
|
│ ├── PasswordChanged { channel_id }
|
|
│ ├── DescriptionChanged { channel_id }
|
|
│ └── Subscribed { channel_id, subscribed }
|
|
│
|
|
├── Server(ServerEvent)
|
|
│ ├── Updated { changes: ServerChanges }
|
|
│ ├── ServerGroupList { groups }
|
|
│ └── ChannelGroupList { groups }
|
|
│
|
|
├── Message(MessageEvent)
|
|
│ ├── Received { message }
|
|
│ ├── Sent { message }
|
|
│ ├── Read { message_id }
|
|
│ └── UnreadCountChanged { count }
|
|
│
|
|
├── Audio(AudioEvent)
|
|
│ ├── InputDeviceChanged { device }
|
|
│ ├── OutputDeviceChanged { device }
|
|
│ ├── InputVolumeChanged { volume }
|
|
│ ├── OutputVolumeChanged { volume }
|
|
│ ├── InputMutedChanged { muted }
|
|
│ ├── OutputMutedChanged { muted }
|
|
│ ├── DeviceList { input_devices, output_devices }
|
|
│ ├── InputLevel { level }
|
|
│ └── OutputLevel { level }
|
|
│
|
|
├── FileTransfer(FileTransferEvent)
|
|
│ ├── Started { transfer_id, file_name, file_size, is_upload }
|
|
│ ├── Progress { transfer_id, progress }
|
|
│ ├── Completed { transfer_id }
|
|
│ ├── Failed { transfer_id, error }
|
|
│ └── Cancelled { transfer_id }
|
|
│
|
|
└── Error(ErrorEvent)
|
|
├── Protocol { code, message }
|
|
├── Network { message }
|
|
├── Audio { message }
|
|
├── Database { message }
|
|
└── Other { message }
|
|
```
|
|
|
|
---
|
|
|
|
### 1.2 `tscore` Module Design
|
|
|
|
#### 1.2.1 Packet Processing Pipeline
|
|
|
|
```
|
|
SEND RECEIVE
|
|
┌──────────────────────────────┐ ┌──────────────────────────────┐
|
|
│ Command String │ │ UDP Packet │
|
|
│ │ │ │ │ │
|
|
│ ▼ │ │ ▼ │
|
|
│ Command::serialize() │ │ InPacket::parse() │
|
|
│ │ │ │ │ │
|
|
│ ▼ │ │ ▼ │
|
|
│ QuickLZ compress │ │ AES-EAX decrypt │
|
|
│ (if Command/CommandLow) │ │ (or fake decrypt) │
|
|
│ │ │ │ │ │
|
|
│ ▼ │ │ ▼ │
|
|
│ Fragment (if > 500 bytes) │ │ QuickLZ decompress │
|
|
│ │ │ │ (if COMPRESSED flag) │
|
|
│ ▼ │ │ │ │
|
|
│ AES-EAX encrypt │ │ ▼ │
|
|
│ (or fake encrypt) │ │ Defragment │
|
|
│ │ │ │ (if FRAGMENTED flag) │
|
|
│ ▼ │ │ │ │
|
|
│ Assign Packet ID │ │ ▼ │
|
|
│ │ │ │ Command::parse() │
|
|
│ ▼ │ │ │ │
|
|
│ UDP Send │ │ ▼ │
|
|
└──────────────────────────────┘ │ Application Layer │
|
|
└──────────────────────────────┘
|
|
```
|
|
|
|
#### 1.2.2 Encryption Key Derivation
|
|
|
|
```
|
|
Input: packet_type, direction, generation_id, shared_iv[64]
|
|
│
|
|
▼
|
|
┌───────────────────────────────────────────────────────┐
|
|
│ temp[0] = direction_byte (0x30=S2C, 0x31=C2S) │
|
|
│ temp[1] = packet_type.u8() │
|
|
│ temp[2..6] = generation_id.to_be_bytes() │
|
|
│ temp[6..70] = shared_iv[0..64] │
|
|
│ │
|
|
│ key_nonce = SHA-256(temp) │
|
|
│ key = key_nonce[0..16] │
|
|
│ nonce = key_nonce[16..32] │
|
|
│ │
|
|
│ key[0] ^= (packet_id >> 8) as u8 │
|
|
│ key[1] ^= (packet_id & 0xFF) as u8 │
|
|
└───────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
Output: key[16], nonce[16] → AES-128-EAX
|
|
```
|
|
|
|
#### 1.2.3 Connection State Machine
|
|
|
|
```
|
|
┌──────────────┐
|
|
│ Disconnected │
|
|
└──────┬───────┘
|
|
│ start_handshake()
|
|
▼
|
|
┌──────────────┐
|
|
┌──────│ Connecting │◄─────────────────┐
|
|
│ └──────┬───────┘ │
|
|
│ │ Init1 received │
|
|
│ ▼ │
|
|
│ ┌──────────────────────┐ │
|
|
│ │ IdentityLevelIncreasing│ │
|
|
│ └──────┬───────────────┘ │
|
|
│ │ Init3 received │
|
|
│ ▼ │
|
|
│ ┌──────────────┐ │
|
|
│ │ Connected │──────────────────┤
|
|
│ └──────┬───────┘ │
|
|
│ │ channellistfinished │
|
|
│ ▼ │
|
|
│ ┌─────────────────────┐ │
|
|
│ │ ChannelListFinished │ │
|
|
│ └──────┬──────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌──────────────────────────┐ │
|
|
└─────>│ DisconnectedTemporarily │──────┘
|
|
└──────┬───────────────────┘
|
|
│ timeout / manual
|
|
▼
|
|
┌──────────────┐
|
|
│ Error │
|
|
└──────┬───────┘
|
|
│
|
|
▼
|
|
┌──────────────┐
|
|
│ Disconnected │
|
|
└──────────────┘
|
|
```
|
|
|
|
#### 1.2.4 RSA Puzzle Solver
|
|
|
|
```rust
|
|
/// Solves y = x^(2^level) mod n
|
|
///
|
|
/// Algorithm:
|
|
/// y = x
|
|
/// for i in 0..level:
|
|
/// y = (y * y) mod n
|
|
///
|
|
/// Time complexity: O(level * M(n)) where M(n) is multiplication cost
|
|
/// Space complexity: O(n) for big integer storage
|
|
fn solve_rsa_puzzle(x: &[u8; 64], n: &[u8; 64], level: u32) -> [u8; 64] {
|
|
let x_big = BigUint::from_bytes_be(x);
|
|
let n_big = BigUint::from_bytes_be(n);
|
|
let mut y = x_big;
|
|
for _ in 0..level {
|
|
y = (y.clone() * y) % &n_big;
|
|
}
|
|
// Convert back to 64-byte array (big-endian, zero-padded)
|
|
}
|
|
```
|
|
|
|
#### 1.2.5 Retransmission System
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ ResendManager │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ pending: BTreeMap<PacketId, SentPacket> │
|
|
│ max_retries: u32 (default: 10) │
|
|
│ connection_timeout: Duration (default: 30s) │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ add_sent(id, data) → Add to pending queue │
|
|
│ ack(id) → bool → Remove from pending │
|
|
│ get_retransmissions() → Vec<(id, data)> to resend │
|
|
│ is_connection_timeout()→ Check for dead connection │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ SentPacket: │
|
|
│ data: Vec<u8> │
|
|
│ sent_at: Instant │
|
|
│ retry_count: u32 │
|
|
│ timeout: Duration (starts at 500ms, doubles) │
|
|
│ │
|
|
│ RttEstimator: │
|
|
│ srtt: Duration (smoothed RTT) │
|
|
│ rtt_var: Duration (RTT variance) │
|
|
│ rto: Duration (retransmission timeout) │
|
|
│ update(measured_rtt) → recalculate SRTT, RTO │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
### 1.3 `tsaudio` Module Design
|
|
|
|
#### 1.3.1 Audio Pipeline
|
|
|
|
```
|
|
CAPTURE PIPELINE:
|
|
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
│ cpal │───>│ VAD │───>│ Opus │───>│ Packet │
|
|
│ capture │ │ detect │ │ encode │ │ output │
|
|
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
|
|
|
PLAYBACK PIPELINE:
|
|
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
│ Packet │───>│ Jitter │───>│ Opus │───>│ cpal │
|
|
│ input │ │ buffer │ │ decode │ │ playback │
|
|
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
|
```
|
|
|
|
#### 1.3.2 Voice Activity Detection
|
|
|
|
```rust
|
|
pub struct VadDetector {
|
|
threshold: f32,
|
|
state: VadState,
|
|
}
|
|
|
|
impl VadDetector {
|
|
pub fn detect(&mut self, samples: &[f32]) -> VadState {
|
|
let energy = 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
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 1.3.3 Jitter Buffer
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ JitterBuffer │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ buffer: Vec<Option<AudioFrame>> (ring buffer) │
|
|
│ head: usize │
|
|
│ tail: usize │
|
|
│ size: usize │
|
|
│ capacity: usize │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ push(frame) → Result<()> // Add frame │
|
|
│ pop() → Option<AudioFrame> // Get next frame │
|
|
│ len() → usize // Current buffer size │
|
|
│ is_empty() → bool │
|
|
│ is_full() → bool │
|
|
│ clear() // Reset buffer │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
### 1.4 `tsdb` Module Design
|
|
|
|
#### 1.4.1 Database Schema
|
|
|
|
```sql
|
|
-- Identity storage
|
|
CREATE TABLE identities (
|
|
id TEXT PRIMARY KEY, -- UUID
|
|
name TEXT NOT NULL, -- Display name
|
|
private_key TEXT NOT NULL, -- Base64 encoded ECC private key
|
|
counter INTEGER DEFAULT 0, -- Hash Cash counter
|
|
max_counter INTEGER DEFAULT 0, -- Maximum counter tried
|
|
created_at TEXT NOT NULL, -- ISO 8601 timestamp
|
|
updated_at TEXT NOT NULL -- ISO 8601 timestamp
|
|
);
|
|
|
|
-- Server bookmarks
|
|
CREATE TABLE bookmarks (
|
|
id TEXT PRIMARY KEY, -- UUID
|
|
name TEXT NOT NULL, -- Display name
|
|
address TEXT NOT NULL, -- Server address
|
|
port INTEGER DEFAULT 9987, -- Server port
|
|
nickname TEXT, -- Preferred nickname
|
|
server_password TEXT, -- Encrypted server password
|
|
channel TEXT, -- Default channel
|
|
channel_password TEXT, -- Encrypted channel password
|
|
default_token TEXT, -- Permission token
|
|
auto_connect INTEGER DEFAULT 0,-- Auto-connect on startup
|
|
last_connected TEXT, -- Last connection timestamp
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
-- Message history
|
|
CREATE TABLE messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server_address TEXT NOT NULL, -- Server address
|
|
invoker_id INTEGER NOT NULL, -- Client ID
|
|
invoker_name TEXT NOT NULL, -- Display name
|
|
invoker_uid TEXT NOT NULL, -- Unique ID
|
|
target_type TEXT NOT NULL, -- "server", "channel", "client"
|
|
target_id INTEGER, -- Target ID
|
|
message TEXT NOT NULL, -- Message content
|
|
is_read INTEGER DEFAULT 0, -- Read status
|
|
timestamp TEXT NOT NULL -- ISO 8601 timestamp
|
|
);
|
|
|
|
-- Key-value settings
|
|
CREATE TABLE settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
#### 1.4.2 CRUD Operations
|
|
|
|
```rust
|
|
impl DatabaseManager {
|
|
// Identity operations
|
|
fn create_identity(&self, name, private_key) -> DatabaseResult<Identity>;
|
|
fn get_identity(&self, id) -> DatabaseResult<Identity>;
|
|
fn get_all_identities(&self) -> DatabaseResult<Vec<Identity>>;
|
|
fn update_identity(&self, id, name?, counter?) -> DatabaseResult<()>;
|
|
fn delete_identity(&self, id) -> DatabaseResult<()>;
|
|
|
|
// Bookmark operations
|
|
fn create_bookmark(&self, name, address, port, nickname?) -> DatabaseResult<Bookmark>;
|
|
fn get_bookmark(&self, id) -> DatabaseResult<Bookmark>;
|
|
fn get_all_bookmarks(&self) -> DatabaseResult<Vec<Bookmark>>;
|
|
fn update_bookmark(&self, id, name?, address?, port?, nickname?) -> DatabaseResult<()>;
|
|
fn delete_bookmark(&self, id) -> DatabaseResult<()>;
|
|
|
|
// Message operations
|
|
fn create_message(&self, server_address, invoker_id, invoker_name,
|
|
invoker_uid, target_type, target_id?, message) -> DatabaseResult<Message>;
|
|
fn get_server_messages(&self, server_address, limit, offset) -> DatabaseResult<Vec<Message>>;
|
|
fn mark_message_read(&self, id) -> DatabaseResult<()>;
|
|
|
|
// Settings operations
|
|
fn get_setting(&self, key) -> DatabaseResult<Option<String>>;
|
|
fn set_setting(&self, key, value) -> DatabaseResult<()>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 1.5 `tauri-app` Design
|
|
|
|
#### 1.5.1 Tauri Command Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Frontend (React) │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ invoke("get_bookmarks") → Promise<BookmarkInfo[]> │ │
|
|
│ │ invoke("connect", {addr, port, nick, pass}) │ │
|
|
│ │ invoke("send_message", {target, message}) │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
└───────────────────────────┬─────────────────────────────────┘
|
|
│ Tauri IPC
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Tauri Shell (Rust) │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ #[tauri::command] │ │
|
|
│ │ async fn get_bookmarks(state: State<AppState>) │ │
|
|
│ │ -> Result<Vec<BookmarkInfo>, String> │ │
|
|
│ │ { │ │
|
|
│ │ state.db.get_all_bookmarks() │ │
|
|
│ │ .map(|b| b.into_iter().map(Into::into)) │ │
|
|
│ │ .map_err(|e| e.to_string()) │ │
|
|
│ │ } │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ AppState { │ │
|
|
│ │ db: DatabaseManager, │ │
|
|
│ │ connection_state: Mutex<ConnectionState>, │ │
|
|
│ │ } │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
#### 1.5.2 React Component Structure
|
|
|
|
```
|
|
App
|
|
├── Header
|
|
│ ├── Logo
|
|
│ ├── ConnectionStatus
|
|
│ └── SettingsButton
|
|
│
|
|
├── Sidebar
|
|
│ ├── BookmarkList
|
|
│ │ └── BookmarkItem (clickable)
|
|
│ └── RecentServers
|
|
│
|
|
├── MainContent
|
|
│ ├── ConnectForm (when no connection)
|
|
│ │ ├── AddressInput
|
|
│ │ ├── NicknameInput
|
|
│ │ ├── PasswordInput
|
|
│ │ └── ConnectButton
|
|
│ │
|
|
│ └── ChatView (when connected)
|
|
│ ├── ChannelTree
|
|
│ ├── ClientList
|
|
│ ├── MessageList
|
|
│ └── MessageInput
|
|
│
|
|
└── StatusBar
|
|
├── ConnectionInfo
|
|
├── AudioStatus
|
|
└── LatencyDisplay
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Algorithm Specifications
|
|
|
|
### 2.1 Shared Secret Computation (Old Protocol <3.1)
|
|
|
|
```
|
|
Input: alpha[10], beta[10], shared_data[32]
|
|
Output: SharedIV[64], SharedMac[8]
|
|
|
|
1. SharedIV[0..20] = SHA-1(shared_data)
|
|
2. SharedIV[0..10] ^= alpha[0..10]
|
|
3. SharedIV[10..20] ^= beta[0..10]
|
|
4. SharedMac[0..8] = SHA-1(SharedIV)[0..8]
|
|
```
|
|
|
|
### 2.2 Shared Secret Computation (New Protocol ≥3.1)
|
|
|
|
```
|
|
Input: alpha[10], beta[54], shared_data[32]
|
|
Output: SharedIV[64], SharedMac[8]
|
|
|
|
1. SharedIV[0..64] = SHA-512(shared_data)
|
|
2. SharedIV[0..10] ^= alpha[0..10]
|
|
3. SharedIV[10..64] ^= beta[0..54]
|
|
4. SharedMac[0..8] = SHA-1(SharedIV)[0..8]
|
|
```
|
|
|
|
### 2.3 Hash Cash Level Computation
|
|
|
|
```
|
|
Input: omega (public key string), offset (u64)
|
|
Output: level (u8)
|
|
|
|
1. data = SHA-1(omega + offset.to_string())
|
|
2. level = 0
|
|
3. for byte in data:
|
|
4. if byte == 0:
|
|
5. level += 8
|
|
6. else:
|
|
7. level += trailing_zeros(byte)
|
|
8. break
|
|
9. return level
|
|
```
|
|
|
|
### 2.4 UID Computation
|
|
|
|
```
|
|
Input: publicKey (ASN.1-DER encoded)
|
|
Output: uid (base64 string)
|
|
|
|
1. hash = SHA-1(publicKey)
|
|
2. uid = base64(hash)
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Interface Specifications
|
|
|
|
### 3.1 Tauri IPC Interface
|
|
|
|
```typescript
|
|
// TypeScript interface for Tauri commands
|
|
interface ITauriCommands {
|
|
// Identity management
|
|
get_identities(): Promise<IdentityInfo[]>;
|
|
create_identity(name: string): Promise<IdentityInfo>;
|
|
delete_identity(id: string): Promise<void>;
|
|
|
|
// Bookmark management
|
|
get_bookmarks(): Promise<BookmarkInfo[]>;
|
|
create_bookmark(name: string, address: string, port: number,
|
|
nickname?: string): Promise<BookmarkInfo>;
|
|
delete_bookmark(id: string): Promise<void>;
|
|
|
|
// Connection
|
|
connect(address: string, port: number, nickname: string,
|
|
password?: string): Promise<void>;
|
|
disconnect(): Promise<void>;
|
|
|
|
// Messaging
|
|
send_message(target: string, message: string): Promise<void>;
|
|
get_messages(server_address: string, limit: number,
|
|
offset: number): Promise<MessageInfo[]>;
|
|
}
|
|
|
|
interface IdentityInfo {
|
|
id: string;
|
|
name: string;
|
|
counter: number;
|
|
max_counter: number;
|
|
}
|
|
|
|
interface BookmarkInfo {
|
|
id: string;
|
|
name: string;
|
|
address: string;
|
|
port: number;
|
|
nickname: string | null;
|
|
auto_connect: boolean;
|
|
last_connected: string | null;
|
|
}
|
|
|
|
interface MessageInfo {
|
|
id: number;
|
|
invoker_name: string;
|
|
message: string;
|
|
timestamp: string;
|
|
is_read: boolean;
|
|
}
|
|
```
|
|
|
|
### 3.2 Internal Rust Interfaces
|
|
|
|
```rust
|
|
// Protocol layer
|
|
pub trait PacketProcessor {
|
|
fn encode(&self, packet: OutPacket) -> Result<Vec<OutUdpPacket>>;
|
|
fn decode(&self, data: &[u8]) -> Result<InPacket>;
|
|
}
|
|
|
|
// Audio layer
|
|
pub trait AudioCapture {
|
|
async fn start(&mut self) -> AudioResult<()>;
|
|
async fn stop(&mut self) -> AudioResult<()>;
|
|
async fn capture(&mut self) -> AudioResult<AudioFrame>;
|
|
}
|
|
|
|
pub trait AudioPlayback {
|
|
async fn start(&mut self) -> AudioResult<()>;
|
|
async fn stop(&mut self) -> AudioResult<()>;
|
|
async fn play(&mut self, frame: AudioFrame) -> AudioResult<()>;
|
|
}
|
|
|
|
// Database layer
|
|
pub trait IdentityStore {
|
|
fn create(&self, name: &str, key: &str) -> DatabaseResult<Identity>;
|
|
fn get(&self, id: &str) -> DatabaseResult<Identity>;
|
|
fn list(&self) -> DatabaseResult<Vec<Identity>>;
|
|
fn update(&self, id: &str, updates: IdentityUpdates) -> DatabaseResult<()>;
|
|
fn delete(&self, id: &str) -> DatabaseResult<()>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Data Dictionary
|
|
|
|
### 4.1 Protocol Fields
|
|
|
|
| Field | Type | Size | Description |
|
|
|-------|------|------|-------------|
|
|
| MAC | [u8; 8] | 8 bytes | EAX message authentication code |
|
|
| PId | u16 | 2 bytes | Packet sequence ID |
|
|
| CId | u16 | 2 bytes | Client ID (C2S only) |
|
|
| PT | u8 | 1 byte | Packet type + flags |
|
|
| VId | u16 | 2 bytes | Voice packet ID |
|
|
| Codec | u8 | 1 byte | Audio codec type |
|
|
|
|
### 4.2 Flag Bits
|
|
|
|
| Bit | Name | Mask | Description |
|
|
|-----|------|------|-------------|
|
|
| 7 | UE | 0x80 | Unencrypted |
|
|
| 6 | CP | 0x40 | Compressed (QuickLZ) |
|
|
| 5 | NP | 0x20 | New protocol |
|
|
| 4 | FR | 0x10 | Fragmented |
|
|
| 3-0 | Type | 0x0F | Packet type (0-8) |
|
|
|
|
### 4.3 Error Codes
|
|
|
|
| Code | Name | Description |
|
|
|------|------|-------------|
|
|
| 0x0000 | ok | Success |
|
|
| 0x0200 | client_invalid_id | Invalid client ID |
|
|
| 0x0201 | client_nickname_inuse | Nickname already in use |
|
|
| 0x0208 | client_invalid_password | Wrong password |
|
|
| 0x0300 | channel_invalid_id | Invalid channel ID |
|
|
| 0x0400 | server_invalid_id | Invalid server ID |
|
|
| 0x0403 | server_maxclients_reached | Server full |
|
|
| 0x0701 | connection_lost | Connection lost |
|
|
|
|
---
|
|
|
|
## 5. Test Design
|
|
|
|
### 5.1 Test Cases (32 total)
|
|
|
|
#### Protocol Tests (14)
|
|
1. `test_packet_type_conversion` - PacketType enum conversion
|
|
2. `test_flags` - Flag bit manipulation
|
|
3. `test_header_c2s` - C2S header parsing
|
|
4. `test_header_s2c` - S2C header parsing
|
|
5. `test_in_packet_parse` - Input packet parsing
|
|
6. `test_out_packet` - Output packet creation
|
|
7. `test_command_parse` - Command string parsing
|
|
8. `test_command_serialize` - Command serialization
|
|
9. `test_command_builder` - CommandBuilder pattern
|
|
10. `test_escape_sequences` - Escape/unescape
|
|
11. `test_init_packet_parse` - Init packet parsing
|
|
12. `test_init_packet_serialize` - Init packet serialization
|
|
13. `test_ack_packet` - Acknowledgement packet
|
|
14. `test_packet_type_properties` - Type property queries
|
|
|
|
#### Crypto Tests (12)
|
|
1. `test_sha1` - SHA-1 hash
|
|
2. `test_sha256` - SHA-256 hash
|
|
3. `test_sha512` - SHA-512 hash
|
|
4. `test_hash_password` - Password hashing
|
|
5. `test_create_key_nonce` - Key derivation
|
|
6. `test_create_encryption_key` - Packet-specific key
|
|
7. `test_shared_secret_old` - Old protocol shared secret
|
|
8. `test_shared_secret_new` - New protocol shared secret
|
|
9. `test_key_cache` - Key caching
|
|
10. `test_eax_encrypt_decrypt` - EAX encryption/decryption
|
|
11. `test_fake_encrypt_decrypt` - Fake encryption
|
|
12. `test_hash_cash_level` - Hash Cash computation
|
|
13. `test_compute_uid` - UID computation
|
|
|
|
#### Connection Tests (6)
|
|
1. `test_encode_version` - Version encoding
|
|
2. `test_rsa_puzzle` - RSA puzzle solver
|
|
3. `test_resend_manager` - Retransmission manager
|
|
4. `test_rtt_estimator` - RTT estimation
|
|
5. `test_sent_packet_retry` - Packet retry logic
|
|
|
|
---
|
|
|
|
## 6. References
|
|
|
|
1. TS3 Protocol Paper: `refercence/tsdeclarations/ts3protocol.md`
|
|
2. Packet Definitions: `refercence/tsdeclarations/Packets.txt`
|
|
3. Message Definitions: `refercence/tsdeclarations/Messages.toml`
|
|
4. Source Code: `src/` (4780 lines, 39 Rust files)
|