From 36302fb49389ac6d7a3b571bcacd1989c66eeb9e Mon Sep 17 00:00:00 2001 From: David Li Date: Thu, 14 May 2026 17:35:27 +0800 Subject: [PATCH] remove the notes --- notes/ARCH.md | 455 --------------------------- notes/TCP.md | 847 -------------------------------------------------- 2 files changed, 1302 deletions(-) delete mode 100644 notes/ARCH.md delete mode 100644 notes/TCP.md diff --git a/notes/ARCH.md b/notes/ARCH.md deleted file mode 100644 index 683a2cd..0000000 --- a/notes/ARCH.md +++ /dev/null @@ -1,455 +0,0 @@ -# wx-cli Architecture Analysis - -## Overview - -**wx-cli** is a cross-platform Rust CLI tool for extracting and querying local WeChat 4.x data. It decrypts SQLCipher-encrypted databases, caches decrypted copies with mtime-aware invalidation, and provides a daemon-based IPC architecture for fast repeated queries. - -**Key characteristics:** -- Single binary, zero runtime dependencies -- Cross-platform: macOS, Linux, Windows -- Millisecond response times via daemon caching -- AI-friendly output (YAML by default, JSON optional) -- All data processed locally, no network calls - ---- - -## High-Level Architecture - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ wx (CLI client) │ -│ src/cli/mod.rs - clap-based command parsing │ -│ Commands: init, sessions, history, search, contacts, export, │ -│ unread, members, new-messages, stats, favorites, sns-* │ -└────────────────────────────┬────────────────────────────────────────┘ - │ IPC (Unix socket / Windows named pipe) - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ wx-daemon (background process) │ -│ src/daemon/mod.rs - tokio async runtime │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ -│ │ DbCache │ │ Names │ │ IPC Server │ │ -│ │ (mtime-aware)│ │ (contact map)│ │ (JSON line protocol) │ │ -│ │ src/daemon/ │ │ src/daemon/ │ │ src/daemon/ │ │ -│ │ cache.rs │ │ query.rs │ │ server.rs │ │ -│ └──────────────┘ └──────────────┘ └──────────────────────┘ │ -│ │ -│ On startup: │ -│ 1. Load config + keys from ~/.wx-cli/ │ -│ 2. Pre-warm: decrypt session.db, sns.db, load contacts │ -│ 3. Listen on socket/pipe for requests │ -└────────────────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Crypto Layer │ -│ src/crypto/mod.rs + wal.rs │ -│ │ -│ - SQLCipher 4 page decryption (AES-256-CBC) │ -│ - WAL (Write-Ahead Log) application │ -│ - Streaming decryption (page-by-page, avoids full-file load) │ -└────────────────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Scanner Layer │ -│ src/scanner/{macos,linux,windows}.rs │ -│ │ -│ Platform-specific memory scanners: │ -│ - macOS: Mach VM API (task_for_pid, mach_vm_region, mach_vm_read) │ -│ - Linux: /proc//mem + /proc//maps │ -│ - Windows: CreateToolhelp32Snapshot + ReadProcessMemory │ -│ │ -│ Pattern: x'<64hex_key><32hex_salt>' in WeChat process memory │ -└─────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Module Breakdown - -### 1. Entry Point (`src/main.rs`) - -```rust -fn main() { - if std::env::var("WX_DAEMON_MODE").is_ok() { - daemon::run(); // Background daemon mode - } else { - cli::run(); // CLI client mode - } -} -``` - -Single binary acts as both client and daemon. Daemon spawned via `WX_DAEMON_MODE=1` env var. - ---- - -### 2. CLI Layer (`src/cli/`) - -**`mod.rs`** - Command definitions via clap derive macros: -- 17 subcommands (Init, Sessions, History, Search, Contacts, Export, Unread, Members, NewMessages, Stats, Favorites, SnsNotifications, SnsFeed, SnsSearch, Daemon) -- Each command dispatches to dedicated module (e.g., `history::cmd_history`) -- All commands share `--json` flag for output format toggle - -**`transport.rs`** - IPC client: -- `ensure_daemon()` - auto-start daemon if not running -- `send()` - JSON line protocol over Unix socket / Windows named pipe -- Timeout handling (15s startup, 120s request) -- Permission preflight check for ~/.wx-cli/ directory - -**Command modules** (`sessions.rs`, `history.rs`, etc.): -- Parse CLI args → build IPC `Request` -- Send to daemon → receive `Response` -- Format output (YAML/JSON) via `output.rs` - ---- - -### 3. Daemon Layer (`src/daemon/`) - -**`mod.rs`** - Daemon lifecycle: -```rust -async fn async_run() -> Result<()> { - // 1. Create ~/.wx-cli/ + cache/ directories - // 2. Write PID file - // 3. Setup signal handlers (SIGTERM/SIGINT) - // 4. Load config + keys - // 5. Initialize DbCache (mtime-aware decryption cache) - // 6. Pre-warm: load contacts, decrypt session.db + sns.db - // 7. Start IPC server (blocking loop) -} -``` - -**`cache.rs`** - DbCache (critical performance component): -- `HashMap` in-memory cache -- `CacheEntry`: `{ db_mtime, wal_mtime, decrypted_path }` -- **mtime-aware invalidation**: re-decrypt only when `.db` or `.db-wal` mtime changes -- Persistent mtime records in `~/.wx-cli/cache/_mtimes.json` -- Cache reuse on daemon restart (avoids re-decryption) -- Uses MD5 hash of rel_key for cache filename - -**`server.rs`** - IPC server: -- Unix: `tokio::net::UnixListener` on `~/.wx-cli/daemon.sock` -- Windows: `interprocess` named pipe `\\.\pipe\wx-cli-daemon` -- One connection per request, JSON line protocol -- `dispatch()` routes `Request` → query functions - -**`query.rs`** - Query implementations (~1500 lines): -- `Names` struct: contact name cache + MD5→username lookup + verify_flags -- `chat_type_of()`: classify as `private`/`group`/`official_account`/`folded` -- Query functions: `q_sessions`, `q_history`, `q_search`, `q_contacts`, `q_unread`, `q_members`, `q_new_messages`, `q_stats`, `q_favorites`, `q_sns_*` -- Message parsing: zstd decompression, XML extraction (appmsg, sysmsg, revokemsg) -- Uses `spawn_blocking` for SQLite queries (rusqlite is sync) - ---- - -### 4. Crypto Layer (`src/crypto/`) - -**`mod.rs`** - SQLCipher 4 decryption: -```rust -// Constants -PAGE_SZ = 4096 -SALT_SZ = 16 -RESERVE_SZ = 80 // IV(16) + HMAC(64) - -// Key operations -fn decrypt_page(enc_key: &[u8; 32], page_data: &[u8], pgno: u32) -> Vec -fn full_decrypt(db_path: &Path, out_path: &Path, enc_key: &[u8; 32]) -``` - -**Algorithm:** -- AES-256-CBC decryption -- IV located at page end: `PAGE_SZ - RESERVE_SZ` offset -- Page 1 special handling: skip 16-byte SALT, write SQLite magic header -- Other pages: decrypt `[0..PAGE_SZ-RESERVE_SZ]` -- Streaming (page-by-page) to avoid full-file memory load - -**`wal.rs`** - WAL application: -- WAL header: 32 bytes (magic, format, page_sz, ckpt_seq, salt1/2, cksum1/2) -- Frame: 24-byte header + PAGE_SZ data -- Frame matching via salt1/2 validation -- Random-write to decrypted DB at `(pgno-1) * PAGE_SZ` - ---- - -### 5. Scanner Layer (`src/scanner/`) - -**Common interface** (`mod.rs`): -```rust -pub struct KeyEntry { - db_name: String, // relative path - enc_key: String, // 64-char hex (32 bytes) - salt: String, // 32-char hex (16 bytes) -} - -pub fn scan_keys(db_dir: &Path) -> Result> // platform-specific -pub fn read_db_salt(path: &Path) -> Option -pub fn collect_db_salts(db_dir: &Path) -> Vec<(String, String)> -``` - -**Pattern searched**: `x'<96 hex chars>'` = 64-char key + 32-char salt - -**macOS** (`macos.rs`): -- `task_for_pid` → get Mach task port (requires root + ad-hoc signed WeChat) -- `mach_vm_region` → enumerate VM regions -- `mach_vm_read` → read 2MB chunks -- Filter: `VM_PROT_READ | VM_PROT_WRITE` regions only -- Deduplication by (key, salt) pair - -**Linux** (`linux.rs`): -- `/proc//comm` → find `wechat`/`weixin` process -- `/proc//maps` → parse `rw-` regions -- `/proc//mem` → seek + read -- Same chunk/dedup strategy - -**Windows** (`windows.rs`): -- `CreateToolhelp32Snapshot` → find `Weixin.exe` -- `OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)` -- `VirtualQueryEx` → enumerate `MEM_COMMIT + PAGE_READWRITE` regions -- `ReadProcessMemory` → chunk read - ---- - -### 6. IPC Protocol (`src/ipc.rs`) - -**Request** (tagged enum): -```rust -pub enum Request { - Ping, - Sessions { limit: usize }, - History { chat, limit, offset, since, until, msg_type }, - Search { keyword, chats, limit, since, until, msg_type }, - Contacts { query, limit }, - Unread { limit, filter }, - Members { chat }, - NewMessages { state, limit }, - Stats { chat, since, until }, - Favorites { limit, fav_type, query }, - SnsNotifications { limit, since, until, include_read }, - SnsFeed { limit, since, until, user }, - SnsSearch { keyword, limit, since, until, user }, -} -``` - -**Response**: -```rust -pub struct Response { - ok: bool, - error: Option, - data: Value, // flattened JSON -} -``` - -Protocol: newline-delimited JSON, one request per connection. - ---- - -### 7. Config Layer (`src/config.rs`) - -**Config struct**: -```rust -pub struct Config { - db_dir: PathBuf, // WeChat db_storage path - keys_file: PathBuf, // all_keys.json - decrypted_dir: PathBuf, // (unused, cache dir used instead) - wechat_process: String, // process name for scanner -} -``` - -**Paths**: -- `cli_dir()`: `~/.wx-cli/` -- `sock_path()`: `~/.wx-cli/daemon.sock` -- `cache_dir()`: `~/.wx-cli/cache/` -- `mtime_file()`: `~/.wx-cli/cache/_mtimes.json` - -**Auto-detection** (`auto_detect_db_dir()`): -- macOS: `~/Library/Containers/com.tencent.xinWeChat/.../xwechat_files/*/db_storage` -- Linux: `~/Documents/xwechat_files/*/db_storage` + legacy path -- Windows: `%APPDATA%/Tencent/xwechat/config/*.ini` → parse data root - ---- - -## Data Flow - -### Init Flow (`wx init`) - -``` -1. Auto-detect db_dir → scan for db_storage directory -2. collect_db_salts(db_dir) → (salt_hex, rel_path) list -3. scan_keys(db_dir) → memory scan → (key_hex, salt_hex) candidates -4. Match: salt_hex == db_salt → KeyEntry { db_name, enc_key, salt } -5. Write ~/.wx-cli/config.json + ~/.wx-cli/all_keys.json -``` - -### Query Flow (e.g., `wx history "张三"`) - -``` -1. CLI: parse args → Request::History { chat: "张三", limit: 50 } -2. transport::ensure_daemon() → start if not alive -3. transport::send(Request) → Unix socket/pipe → daemon -4. daemon::dispatch(Request) → q_history() - a. resolve_username("张三") → "wxid_xxx" (fuzzy match against Names) - b. find_msg_tables(db, names, username) → [(db_path, "Msg_")] - c. spawn_blocking: SQLite query on decrypted db_path - d. decompress_message (zstd) + fmt_content (XML parsing) -5. Response::ok(json!{ chat, messages, ... }) -6. CLI: output.rs → YAML/JSON formatting -``` - -### Decryption Flow (DbCache::get) - -``` -1. Check in-memory cache: if entry.mtime matches → return cached path -2. mtime mismatch or missing → spawn_blocking decrypt: - a. crypto::full_decrypt(db_path, out_path, enc_key) - b. If .db-wal exists: wal::apply_wal(wal_path, out_path, enc_key) -3. Update cache entry + persist mtimes to _mtimes.json -4. Return decrypted path for SQLite query -``` - ---- - -## Database Schema Knowledge - -**session/session.db**: -- `SessionTable`: username, unread_count, summary, last_timestamp, last_msg_type, last_msg_sender - -**contact/contact.db**: -- `contact`: username, nick_name, remark, verify_flag -- `chat_room`: id, owner (for group info) -- `chatroom_member`: room_id, member_id (joined with contact) - -**message/message_N.db**: -- `Msg_`: local_id, local_type, create_time, real_sender_id, message_content, WCDB_CT_message_content -- `Name2Id`: rowid → user_name (sender lookup) -- WCDB_CT = 4 means zstd compression - -**sns/sns.db**: -- `sns_notification`: type (like/comment), from_nickname, content, feed_preview -- `sns_feed_xml`: author, contentDesc, media XML, createTime - -**favorite/favorite.db**: -- `fav_db_item`: local_id, type, update_time, content, fromusr - ---- - -## Performance Optimizations - -1. **mtime-aware caching**: Only re-decrypt when source file changes -2. **Pre-warming**: Decrypt session.db + sns.db + contacts on daemon start -3. **Arc-wrapped Names**: Contact cache shared via Arc, cloned in O(1) -4. **spawn_blocking**: Sync SQLite ops off async runtime -5. **Streaming decrypt**: Page-by-page, no full file in memory -6. **WAL handling**: Apply uncommitted writes without re-decrypt -7. **MD5 table lookup**: `Msg_` → username via precomputed hash map - ---- - -## Security Considerations - -1. **Root/Admin required**: Memory scan needs elevated privileges -2. **No secrets logged**: Keys written to file, never echoed -3. **Socket permissions**: Unix socket mode 0600 -4. **Local-only**: All IPC is localhost, no network exposure -5. **User consent implied**: Only decrypts own WeChat data - ---- - -## Error Handling Patterns - -- `anyhow::Result` throughout -- Context messages for chain debugging -- Graceful degradation: missing tables → fallback paths -- Preflight checks (e.g., ~/.wx-cli writable before daemon spawn) -- Signal handlers for clean shutdown (socket/PID file cleanup) - ---- - -## Cross-Platform Notes - -| Platform | Scanner API | IPC | Privilege | DB Path | -|----------|-------------|-----|-----------|---------| -| macOS | Mach VM | Unix socket | sudo + codesign | ~/Library/Containers/... | -| Linux | /proc/pid/mem | Unix socket | sudo | ~/Documents/xwechat_files | -| Windows | ToolHelp + ReadProcessMemory | Named pipe | Admin | %APPDATA%/Tencent/xwechat | - ---- - -## Testing Coverage - -- `src/crypto/mod.rs`: hex encoding, salt reading, recursive collection -- `src/scanner/macos.rs`: pattern matching (uppercase, dedup, embedded, edge cases) -- Unit tests for helper functions; integration tests would require live WeChat - ---- - -## Extension Points - -1. **New commands**: Add to `cli/mod.rs` enum + dispatch + query.rs function -2. **New message types**: Extend `fmt_type()` + `fmt_content()` parsers -3. **New DB sources**: Add to DbCache key list + query functions -4. **Output formats**: Extend `output.rs` formatter - ---- - -## File Structure Summary - -``` -src/ -├── main.rs # Entry point (daemon/CLI switch) -├── config.rs # Config loading + auto-detect -├── ipc.rs # Request/Response protocol types -├── cli/ -│ ├── mod.rs # clap command definitions + dispatch -│ ├── transport.rs # IPC client + daemon lifecycle -│ ├── output.rs # YAML/JSON formatting -│ ├── init.rs # wx init implementation -│ ├── sessions.rs # etc. (thin wrappers around IPC) -│ └── daemon_cmd.rs # daemon status/stop/logs -├── daemon/ -│ ├── mod.rs # daemon entry + async_run -│ ├── cache.rs # DbCache (mtime-aware decryption cache) -│ ├── server.rs # IPC server (Unix/Windows) -│ └── query.rs # All query implementations -├── crypto/ -│ ├── mod.rs # SQLCipher page decryption -│ └── wal.rs # WAL application -└── scanner/ - ├── mod.rs # common interface + salt collection - ├── macos.rs # Mach VM memory scanner - ├── linux.rs # /proc scanner - └── windows.rs # Windows API scanner -``` - ---- - -## Dependencies - -**Core crates:** -- `clap` (derive) - CLI parsing -- `tokio` (full) - async runtime -- `serde`/`serde_json` - serialization -- `rusqlite` (bundled) - SQLite queries -- `aes`/`cbc`/`hmac`/`sha2`/`pbkdf2` - crypto primitives -- `zstd` - message decompression -- `chrono` - timestamp formatting -- `anyhow` - error handling -- `dirs` - home directory -- `md5` - table name hashing -- `regex` - Msg_ pattern matching - -**Platform-specific:** -- Unix: `libc` (setsid, signal handling) -- Windows: `windows` crate (process/memory APIs), `interprocess` (named pipes) - ---- - -## Summary - -wx-cli is a well-architected Rust project demonstrating: -- Clean separation of CLI/daemon/crypto/scanner layers -- Async-first daemon with sync-offload for SQLite -- Smart caching strategy (mtime-based invalidation) -- Cross-platform memory scanning for SQLCipher key extraction -- AI-friendly output design (YAML default, JSON optional) -- Comprehensive command coverage for WeChat local data \ No newline at end of file diff --git a/notes/TCP.md b/notes/TCP.md deleted file mode 100644 index 853d275..0000000 --- a/notes/TCP.md +++ /dev/null @@ -1,847 +0,0 @@ -# Communication Layer Analysis & TCP Socket Proposal - -## Current Communication Architecture - -### Layer Overview - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Protocol Layer │ -│ src/ipc.rs │ -│ Request / Response types + JSON serialization │ -│ (Well abstracted - transport-agnostic) │ -└────────────────────────────┬────────────────────────────────┘ - │ -┌────────────────────────────┴────────────────────────────────┐ -│ Server Layer │ -│ src/daemon/server.rs │ -│ Platform-specific listeners + connection handlers │ -│ (POOR abstraction - duplicated logic per platform) │ -└────────────────────────────┬────────────────────────────────┘ - │ -┌────────────────────────────┴────────────────────────────────┐ -│ Client Layer │ -│ src/cli/transport.rs │ -│ Platform-specific connection + send functions │ -│ (POOR abstraction - duplicated logic per platform) │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Abstraction Assessment - -### Protocol Layer (src/ipc.rs) — **HIGH abstraction** - -**Strengths:** -- Pure data types with serde derive -- No transport-specific code -- Clean API: `Request` enum, `Response` struct -- `to_json_line()` helper for serialization -- Transport-agnostic by design - -**Example:** -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "cmd", rename_all = "snake_case")] -pub enum Request { - Ping, - Sessions { limit: usize }, - History { chat: String, limit: usize, ... }, - // ... all commands -} - -pub struct Response { - pub ok: bool, - pub error: Option, - #[serde(flatten)] - pub data: Value, -} -``` - -**Verdict:** This layer is well-designed and TCP-ready. No changes needed. - ---- - -### Server Layer (src/daemon/server.rs) — **LOW abstraction** - -**Current structure:** -```rust -// Top-level entry with #[cfg] branching -pub async fn serve(db, names) -> Result<()> { - #[cfg(unix)] - serve_unix(db, names).await?; - #[cfg(windows)] - serve_windows(db, names).await?; -} - -// Unix implementation (40 lines) -#[cfg(unix)] -async fn serve_unix(db, names) -> Result<()> { - let listener = UnixListener::bind(&sock_path)?; - loop { - let (stream, _) = listener.accept().await?; - tokio::spawn(async { handle_connection_unix(stream, db, names) }); - } -} - -#[cfg(unix)] -async fn handle_connection_unix(stream, db, names) -> Result<()> { - let (reader, mut writer) = stream.into_split(); - let mut lines = BufReader::new(reader).lines(); - let line = lines.next_line().await?; - let req: Request = serde_json::from_str(&line)?; - let resp = dispatch(req, &db, &names).await; - writer.write_all(resp.to_json_line()?.as_bytes()).await?; -} - -// Windows implementation (40 lines) - SAME LOGIC, DIFFERENT TYPES -#[cfg(windows)] -async fn serve_windows(db, names) -> Result<()> { - let listener = ListenerOptions::new().name(name).create_tokio()?; - loop { - let conn = listener.accept().await?; - tokio::spawn(async { handle_connection_windows(conn, db, names) }); - } -} - -#[cfg(windows)] -async fn handle_connection_windows(conn, db, names) -> Result<()> { - let (reader, mut writer) = tokio::io::split(conn); - let mut lines = BufReader::new(reader).lines(); - let line = lines.next_line().await?; - let req: Request = serde_json::from_str(&line)?; - let resp = dispatch(req, &db, &names).await; - writer.write_all(resp.to_json_line()?.as_bytes()).await?; -} -``` - -**Problems:** -1. **Duplicated connection handling**: `handle_connection_unix` and `handle_connection_windows` have identical logic -2. **No abstraction for stream types**: `UnixStream` vs `interprocess::Stream` handled separately -3. **No abstraction for listener types**: `UnixListener` vs `interprocess::Listener` handled separately -4. **#[cfg] branching at function level**: Makes extension difficult -5. **`dispatch()` is shared but buried**: Good pattern, but underutilized - -**Duplication count:** ~30 lines of identical JSON-line protocol handling duplicated per platform - ---- - -### Client Layer (src/cli/transport.rs) — **LOW abstraction** - -**Current structure:** -```rust -// is_alive() with #[cfg] branching -pub fn is_alive() -> bool { - #[cfg(unix)] - { - let stream = UnixStream::connect(&sock_path)?; - // ping logic - } - #[cfg(windows)] - { - let stream = Stream::connect(name)?; - // ping logic (different API) - } -} - -// send() with #[cfg] branching -pub fn send(req: Request) -> Result { - ensure_daemon()?; - #[cfg(unix)] - { send_unix(req) } - #[cfg(windows)] - { send_windows(req) } -} - -#[cfg(unix)] -fn send_unix(req: Request) -> Result { - let stream = UnixStream::connect(&sock_path)?; - stream.write_all(serde_json::to_string(&req)? + "\n"); - let line = BufReader::new(&stream).read_line(); - let resp: Response = serde_json::from_str(&line)?; - Ok(resp) -} - -#[cfg(windows)] -fn send_windows(req: Request) -> Result { - let stream = Stream::connect(name)?; - stream.write_all(serde_json::to_string(&req)? + "\n"); - let line = BufReader::new(stream).read_line(); - let resp: Response = serde_json::from_str(&line)?; - Ok(resp) -} -``` - -**Problems:** -1. **Duplicated request/response handling**: Same JSON-line protocol, different stream types -2. **No abstraction for stream type**: Each platform uses different types -3. **`is_alive()` logic differs**: Windows version doesn't do full ping -4. **#[cfg] branching scattered**: 3 separate locations - -**Duplication count:** ~20 lines of identical protocol handling duplicated per platform - ---- - -## Abstraction Score Summary - -| Layer | Abstraction Level | Duplicated Lines | Extension Difficulty | -|----------------|-------------------|------------------|---------------------| -| Protocol | HIGH | 0 | Easy | -| Server | LOW | ~30 | Hard | -| Client | LOW | ~20 | Hard | - -**Total duplicated code:** ~50 lines of identical JSON-line protocol handling - -**Root cause:** No trait abstraction for `Listener` and `Connection` types - ---- - -## Proposed Architecture for TCP Support - -### Strategy: Trait-Based Abstraction - -Introduce traits for transport primitives, implement for: -1. Unix socket (existing) -2. Windows named pipe (existing) -3. TCP socket (new) - ---- - -### New Trait Definitions - -```rust -// src/transport/traits.rs - -use anyhow::Result; -use tokio::io::{AsyncRead, AsyncWrite}; - -/// Trait for accepting connections (server-side) -pub trait Listener: Send + Sync { - type Connection: AsyncRead + AsyncWrite + Send + Sync + 'static; - - async fn accept(&self) -> Result; - fn addr_desc(&self) -> String; // for logging -} - -/// Trait for connecting to server (client-side) -pub trait Connector: Send + Sync { - type Connection: AsyncRead + AsyncWrite + Send + Sync + 'static; - - async fn connect(&self) -> Result; - fn is_available(&self) -> bool; // quick check before connect -} -``` - ---- - -### New Module Structure - -``` -src/transport/ -├── mod.rs # Public API: send(), handle_connection() -├── traits.rs # Listener + Connector traits -├── unix.rs # UnixListener + UnixConnector -├── windows.rs # PipeListener + PipeConnector -├── tcp.rs # TcpListener + TcpConnector -└── protocol.rs # JSON-line protocol handling (shared) -``` - -**Key change:** Protocol handling moves to `protocol.rs`, shared by all transports - ---- - -### Protocol Handler (Shared Code) - -```rust -// src/transport/protocol.rs - -use anyhow::Result; -use tokio::io::{AsyncRead, AsyncWrite, AsyncBufReadExt, AsyncWriteExt, BufReader}; - -use crate::ipc::{Request, Response}; - -/// Handle a single connection (server-side) -pub async fn handle_connection( - conn: C, - db: Arc, - names: Arc>>, -) -> Result<()> { - let (reader, mut writer) = tokio::io::split(conn); - let mut lines = BufReader::new(reader).lines(); - - let line = match lines.next_line().await? { - Some(l) => l, - None => return Ok(()), - }; - - let req: Request = match serde_json::from_str(&line) { - Ok(r) => r, - Err(e) => { - let resp = Response::err(format!("JSON parse error: {}", e)); - writer.write_all(resp.to_json_line()?.as_bytes()).await?; - return Ok(()); - } - }; - - let resp = dispatch(req, db, names).await; - writer.write_all(resp.to_json_line()?.as_bytes()).await?; - Ok(()) -} - -/// Send request and receive response (client-side) -pub async fn send_over_connection( - conn: C, - req: &Request, -) -> Result { - let (reader, mut writer) = tokio::io::split(conn); - - let req_str = serde_json::to_string(req)? + "\n"; - writer.write_all(req_str.as_bytes()).await?; - - let mut lines = BufReader::new(reader).lines(); - let line = lines.next_line().await? - .ok_or_else(|| anyhow::anyhow!("No response received"))?; - - let resp: Response = serde_json::from_str(&line)?; - if !resp.ok { - anyhow::bail!("{}", resp.error.as_deref().unwrap_or("Unknown error")); - } - Ok(resp) -} -``` - -**This eliminates all 50 lines of duplication.** - ---- - -### Unix Socket Implementation - -```rust -// src/transport/unix.rs - -use anyhow::Result; -use tokio::net::{UnixListener, UnixStream}; - -use super::traits::{Listener, Connector}; - -pub struct UnixSocketListener { - listener: UnixListener, - path: std::path::PathBuf, -} - -impl Listener for UnixSocketListener { - type Connection = UnixStream; - - async fn accept(&self) -> Result { - let (stream, _) = self.listener.accept().await?; - Ok(stream) - } - - fn addr_desc(&self) -> String { - self.path.display().to_string() - } -} - -pub struct UnixSocketConnector { - path: std::path::PathBuf, -} - -impl Connector for UnixSocketConnector { - type Connection = UnixStream; - - async fn connect(&self) -> Result { - UnixStream::connect(&self.path).await? - } - - fn is_available(&self) -> bool { - self.path.exists() - } -} - -// Factory functions -pub fn create_listener(path: &std::path::Path) -> Result { - if path.exists() { - std::fs::remove_file(path)?; - } - let listener = UnixListener::bind(path)?; - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; - Ok(UnixSocketListener { listener, path: path.to_owned() }) -} - -pub fn connector(path: &std::path::Path) -> UnixSocketConnector { - UnixSocketConnector { path: path.to_owned() } -} -``` - ---- - -### Windows Named Pipe Implementation - -```rust -// src/transport/windows.rs - -use anyhow::Result; -use interprocess::local_socket::{ - tokio::prelude::*, - GenericNamespaced, ListenerOptions, -}; - -use super::traits::{Listener, Connector}; - -pub struct PipeListener { - listener: interprocess::local_socket::tokio::Listener, - name: String, -} - -impl Listener for PipeListener { - type Connection = interprocess::local_socket::tokio::Stream; - - async fn accept(&self) -> Result { - self.listener.accept().await? - } - - fn addr_desc(&self) -> String { - format!("\\\\.\\pipe\\{}", self.name) - } -} - -pub struct PipeConnector { - name: String, -} - -impl Connector for PipeConnector { - type Connection = interprocess::local_socket::tokio::Stream; - - async fn connect(&self) -> Result { - let ns_name = self.name.to_ns_name::()?; - Stream::connect(ns_name).await? - } - - fn is_available(&self) -> bool { - // Windows named pipes don't have filesystem presence - // Try a quick connect to check - self.connect().await.is_ok() - } -} - -pub fn create_listener(name: &str) -> Result { - let ns_name = name.to_ns_name::()?; - let listener = ListenerOptions::new().name(ns_name).create_tokio()?; - Ok(PipeListener { listener, name: name.to_owned() }) -} - -pub fn connector(name: &str) -> PipeConnector { - PipeConnector { name: name.to_owned() } -} -``` - ---- - -### TCP Socket Implementation (NEW) - -```rust -// src/transport/tcp.rs - -use anyhow::Result; -use tokio::net::{TcpListener, TcpStream}; - -use super::traits::{Listener, Connector}; - -pub struct TcpSocketListener { - listener: TcpListener, - addr: std::net::SocketAddr, -} - -impl Listener for TcpSocketListener { - type Connection = TcpStream; - - async fn accept(&self) -> Result { - let (stream, addr) = self.listener.accept().await?; - eprintln!("[tcp] connection from {}", addr); - Ok(stream) - } - - fn addr_desc(&self) -> String { - self.addr.to_string() - } -} - -pub struct TcpSocketConnector { - addr: std::net::SocketAddr, -} - -impl Connector for TcpSocketConnector { - type Connection = TcpStream; - - async fn connect(&self) -> Result { - TcpStream::connect(&self.addr).await? - } - - fn is_available(&self) -> bool { - // TCP port check - try quick connect - std::net::TcpStream::connect_timeout(&self.addr, std::time::Duration::from_millis(100)).is_ok() - } -} - -pub async fn create_listener(bind: &str) -> Result { - let listener = TcpListener::bind(bind).await?; - let addr = listener.local_addr()?; - Ok(TcpSocketListener { listener, addr }) -} - -pub fn connector(addr: std::net::SocketAddr) -> TcpSocketConnector { - TcpSocketConnector { addr } -} -``` - ---- - -### Server Refactor (src/daemon/server.rs) - -```rust -// src/daemon/server.rs - -use std::sync::Arc; -use crate::transport::{Listener, handle_connection}; - -pub async fn serve( - db: Arc, - names: Arc>>, -) -> Result<()> { - // Determine transport based on config/env - let listeners: Vec> = build_listeners()?; - - for listener in listeners { - eprintln!("[server] listening on {}", listener.addr_desc()); - let db2 = Arc::clone(&db); - let names2 = Arc::clone(&names); - - tokio::spawn(async move { - loop { - match listener.accept().await { - Ok(conn) => { - let db3 = Arc::clone(&db2); - let names3 = Arc::clone(&names2); - tokio::spawn(async move { - if let Err(e) = handle_connection(conn, db3, names3).await { - eprintln!("[server] connection error: {}", e); - } - }); - } - Err(e) => eprintln!("[server] accept error: {}", e), - } - } - }); - } - - // Keep daemon alive - tokio::signal::ctrl_c().await?; - Ok(()) -} - -fn build_listeners() -> Result>> { - let mut listeners = Vec::new(); - - // Always add local transport (Unix/Pipe) - #[cfg(unix)] - listeners.push(Box::new( - crate::transport::unix::create_listener(&crate::config::sock_path())? - )); - - #[cfg(windows)] - listeners.push(Box::new( - crate::transport::windows::create_listener("wx-cli-daemon")? - )); - - // Optionally add TCP (if configured) - if let Ok(tcp_bind) = std::env::var("WX_TCP_BIND") { - let tcp_listener = crate::transport::tcp::create_listener(&tcp_bind).await?; - eprintln!("[server] TCP enabled on {}", tcp_listener.addr_desc()); - listeners.push(Box::new(tcp_listener)); - } - - Ok(listeners) -} -``` - -**Key changes:** -1. Single `serve()` function, no #[cfg] branching -2. `build_listeners()` constructs appropriate transport(s) -3. Can listen on multiple transports simultaneously (local + TCP) -4. `handle_connection()` from `transport::protocol` is shared - ---- - -### Client Refactor (src/cli/transport.rs) - -```rust -// src/cli/transport.rs (renamed to src/transport/mod.rs) - -use anyhow::Result; -use crate::ipc::{Request, Response}; -use crate::transport::{Connector, send_over_connection}; - -pub async fn send(req: Request) -> Result { - ensure_daemon()?; - - // Try connectors in priority order - let connectors = build_connectors(); - - for connector in connectors { - if connector.is_available() { - let conn = connector.connect().await?; - return send_over_connection(conn, &req).await; - } - } - - anyhow::bail!("No available transport to daemon") -} - -fn build_connectors() -> Vec> { - let mut connectors = Vec::new(); - - // Local transport first (faster, more secure) - #[cfg(unix)] - connectors.push(Box::new( - crate::transport::unix::connector(&crate::config::sock_path()) - )); - - #[cfg(windows)] - connectors.push(Box::new( - crate::transport::windows::connector("wx-cli-daemon") - )); - - // TCP fallback (if configured) - if let Ok(tcp_addr) = std::env::var("WX_TCP_ADDR") { - if let Ok(addr) = tcp_addr.parse() { - connectors.push(Box::new( - crate::transport::tcp::connector(addr) - )); - } - } - - connectors -} - -pub fn ensure_daemon() -> Result<()> { - // Try ping on each connector - for connector in build_connectors() { - if connector.is_available() { - // Try quick ping - if let Ok(conn) = connector.connect().await? { - // Use blocking ping for startup check - // ... (existing logic wrapped) - return Ok(()); - } - } - } - - // No daemon found, start it - start_daemon()?; - - // Wait for any connector to become available - let deadline = std::time::Instant::now() + Duration::from_secs(15); - while std::time::Instant::now() < deadline { - for connector in build_connectors() { - if connector.is_available() { - return Ok(()); - } - } - std::thread::sleep(Duration::from_millis(300)); - } - - anyhow::bail!("Daemon startup timeout") -} -``` - -**Key changes:** -1. Async `send()` using `send_over_connection()` -2. `build_connectors()` returns prioritized list -3. Fallback chain: Unix/Pipe → TCP -4. No #[cfg] branching in main logic - ---- - -## Configuration for TCP - -### Environment Variables - -```bash -# Server: enable TCP listener -WX_TCP_BIND=127.0.0.1:9876 # bind address (default: none) -WX_TCP_BIND=0.0.0.0:9876 # allow external connections (security risk) - -# Client: TCP fallback address -WX_TCP_ADDR=127.0.0.1:9876 # connect address -WX_TCP_ADDR=192.168.1.100:9876 # remote daemon -``` - -### Config File Extension - -```json -// ~/.wx-cli/config.json -{ - "db_dir": "...", - "keys_file": "...", - "tcp": { - "bind": "127.0.0.1:9876", // optional - "allow_remote": false // security flag - } -} -``` - ---- - -## Security Considerations for TCP - -### Risks - -1. **No encryption**: JSON-line protocol sent in plaintext -2. **No authentication**: Anyone can query WeChat data -3. **Data exposure**: Chat history, contacts, etc. visible to network - -### Recommended Safeguards - -```rust -// src/transport/tcp.rs - -pub struct TcpSocketListener { - listener: TcpListener, - addr: SocketAddr, - allowed_hosts: Vec, // CIDR whitelist -} - -impl Listener for TcpSocketListener { - async fn accept(&self) -> Result { - let (stream, addr) = self.listener.accept().await?; - - // Check source IP against whitelist - let ip = addr.ip(); - if !self.allowed_hosts.iter().any(|net| net.contains(&ip)) { - eprintln!("[tcp] rejected connection from {}", addr); - return Err(anyhow::anyhow!("IP not in whitelist")); - } - - Ok(stream) - } -} - -// Config -pub struct TcpConfig { - bind: String, - allow_remote: bool, - allowed_hosts: Vec, // ["127.0.0.1/8", "192.168.1.0/24"] -} -``` - -### Authentication Proposal (Optional) - -```rust -// Add to Request enum -pub enum Request { - Auth { token: String }, // new - Ping, - Sessions { ... }, -} - -// Server checks token before processing -async fn dispatch(req: Request, db: &DbCache, names: &Names, auth: &AuthState) -> Response { - if !auth.is_authenticated() && !req.is_auth_request() { - return Response::err("Not authenticated"); - } - // ... normal dispatch -} -``` - ---- - -## Implementation Roadmap - -### Phase 1: Refactor Existing Code - -1. Create `src/transport/` module -2. Define `Listener` and `Connector` traits -3. Move Unix/Pipe implementations to `unix.rs` / `windows.rs` -4. Extract protocol handling to `protocol.rs` -5. Refactor `server.rs` to use trait -6. Refactor `transport.rs` to use trait - -**Effort:** ~4 hours -**Benefit:** Eliminate 50 lines duplication, cleaner architecture - -### Phase 2: Add TCP Support - -1. Create `tcp.rs` with `TcpSocketListener` / `TcpSocketConnector` -2. Update `build_listeners()` / `build_connectors()` -3. Add config parsing for TCP options -4. Add IP whitelist validation - -**Effort:** ~2 hours -**Benefit:** TCP connectivity for remote clients - -### Phase 3: Security Hardening - -1. Add authentication token support -2. TLS wrapper option (tokio-rustls) -3. Connection logging/audit - -**Effort:** ~3 hours -**Benefit:** Production-safe remote access - ---- - -## Backwards Compatibility - -- Local transport (Unix/Pipe) remains default -- TCP opt-in via config/env (not automatic) -- CLI unchanged (same commands) -- Protocol unchanged (same Request/Response types) - ---- - -## Alternative: Zero-Change TCP Proxy - -If refactoring is not desired, a simpler approach: - -```bash -# Use socat/proxy to expose Unix socket over TCP -socat TCP-LISTEN:9876,reuseaddr,fork UNIX-CONNECT:/home/user/.wx-cli/daemon.sock -``` - -**Pros:** No code changes -**Cons:** Requires external tool, no IP filtering, less integrated - ---- - -## Summary - -| Aspect | Current State | Proposed State | -|---------------------------|-------------------------|-----------------------------| -| Protocol abstraction | HIGH (good) | HIGH (unchanged) | -| Transport abstraction | LOW (platform-specific) | HIGH (trait-based) | -| Duplicated code | ~50 lines | 0 lines | -| Extension difficulty | Hard | Easy | -| TCP support | None | Full | -| Multi-listener support | None | Yes (local + TCP) | - -**Recommended path:** Proceed with Phase 1 refactor, then Phase 2 TCP addition. Phase 3 security can follow based on use case. - ---- - -## Code Impact Summary - -| File | Change Type | Lines Changed | -|--------------------------|--------------------|---------------| -| src/transport/mod.rs | New | ~60 | -| src/transport/traits.rs | New | ~20 | -| src/transport/protocol.rs| New (from existing)| ~40 | -| src/transport/unix.rs | New (refactor) | ~40 | -| src/transport/windows.rs | New (refactor) | ~40 | -| src/transport/tcp.rs | New | ~50 | -| src/daemon/server.rs | Refactor | ~30 (from 90) | -| src/cli/transport.rs | Delete (moved) | 0 | -| src/ipc.rs | Unchanged | 0 | - -**Net change:** +250 new lines, -90 old lines, -50 duplication = +110 total -**Complexity reduction:** Platform branching centralized, extension point clear \ No newline at end of file