diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f33c2af --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,613 @@ +# AGENTS.md - VPN Tray Manager Development Guide + +## Project Overview + +**Xray-VPN-Manager** is a Windows-only system tray application for managing multiple Xray-core VPN servers from a single subscription URL. Built in Rust with native Windows UI (no web/electron), it provides a lightweight way to enable/disable VPN servers with custom proxy ports. + +**Platform:** Windows 10/11 only (uses Windows API extensively) +**Language:** Rust (edition 2024) +**Architecture:** Native Win32 GUI + Tokio async runtime for process management + +--- + +## Essential Commands + +### Build & Run + +```bash +# Development build +cargo build + +# Release build (optimized) +cargo build --release + +# Run (shows console output - #![windows_subsystem = "windows"] commented out in main.rs:1) +cargo run + +# Release run (production mode) +cargo run --release +``` + +### Testing + +No test suite currently exists. Manual testing required: +1. Run app, check tray icon appears +2. Open Settings, enter subscription URL and xray binary path +3. Click Update to fetch servers +4. Enable servers, set ports/proxy types +5. Click Save and verify servers start +6. Check tray menu shows running servers + +### Linting & Formatting + +```bash +# Check code (standard Rust linting) +cargo check + +# Format code +cargo fmt + +# Run Clippy for additional lints +cargo clippy +``` + +--- + +## Project Structure + +``` +VPN-Manager/ +├── src/ +│ ├── main.rs # Entry point, tray icon, Windows message loop +│ ├── config.rs # Config persistence (JSON in %APPDATA%) +│ ├── xray_manager.rs # Xray process lifecycle management +│ ├── vpn/ +│ │ └── mod.rs # Subscription parsing, URI handling +│ └── ui/ +│ ├── mod.rs # UI module exports +│ ├── tray.rs # Tray icon creation, menu rendering +│ └── settings_window.rs # Native Win32 settings window (1200+ LOC) +├── Cargo.toml # Dependencies, Windows features +├── build.rs # Embeds app.manifest via app.rc +├── app.manifest # Windows DPI awareness, compatibility +└── app.rc # Resource compiler input +``` + +### Module Responsibilities + +- **main.rs**: Global state (`TOKIO_RUNTIME`, `MENU_UPDATE_REQUESTED`), server restart logic, Windows message pump +- **config.rs**: `Config` struct, load/save to `%APPDATA%\Xray-VPN-Manager\config.json` +- **xray_manager.rs**: Wraps `v2parser::xray_runner::XrayRunner`, manages server processes in `XRAY_PROCESSES` HashMap +- **vpn/mod.rs**: Fetches subscription URLs (base64-encoded), parses URIs (vless, vmess, trojan, ss, socks), assigns local ports +- **ui/tray.rs**: Creates tray icon (gold star), builds dynamic menu with running servers +- **ui/settings_window.rs**: Complex native Win32 window with custom scrolling, file dialogs, dynamic server list + +--- + +## Configuration & Data Flow + +### Config File + +**Location:** `%APPDATA%\Xray-VPN-Manager\config.json` + +```json +{ + "subscription_url": "https://example.com/sub", + "xray_binary_path": "C:\\path\\to\\xray.exe", + "server_settings": { + "VLESS://server1.com:443": { + "local_port": 1080, + "proxy_type": "SOCKS", + "enabled": true + } + } +} +``` + +**Server Key Format:** `PROTOCOL://address:port` (e.g., `VLESS://server.com:443`) + +### Data Flow + +1. **Startup:** + - Load config from `%APPDATA%` + - Fetch subscription URIs + - Parse and assign ports (preserve saved settings) + - Store in `VPN_SERVERS` global mutex + - Start enabled servers via `xray_manager::start_server()` + +2. **Settings Window:** + - User enters/updates subscription URL + - Click "Update" → fetch and parse in background thread + - Store in `VPN_SERVERS`, post `WM_UPDATE_SERVERS` message + - Rebuild server list UI with checkboxes, port edits, proxy type combos + - Click "Save" → build `Config` from UI state, save to JSON, call `restart_xray_servers()` + +3. **Server Control:** + - `restart_xray_servers()` stops all, starts enabled ones + - Updates tray menu via `request_menu_update()` (sets atomic flag) + - Main loop checks flag and calls `update_tray_menu()` + +--- + +## Code Patterns & Conventions + +### Global State + +Uses `LazyLock` (nightly-stabilized) for lazy static initialization: + +```rust +pub static TOKIO_RUNTIME: LazyLock = LazyLock::new(|| { + tokio::runtime::Runtime::new().expect("Failed to create tokio runtime") +}); + +pub static VPN_SERVERS: Mutex>> = Mutex::new(None); +pub static XRAY_PROCESSES: LazyLock>> = ...; +pub static MENU_UPDATE_REQUESTED: AtomicBool = AtomicBool::new(false); +``` + +### Threading Model + +- **Main thread:** Windows message loop (synchronous, blocking `GetMessageW`) +- **Background threads:** Subscription fetching, server start/stop (spawn via `std::thread::spawn`) +- **Async runtime:** Tokio runtime for xray process management, accessed via `TOKIO_RUNTIME.block_on(async { ... })` + +### Error Handling + +- Functions return `Result` (error messages as strings) +- Silent failures common in networking code (vpn/mod.rs): `Err(_) => {}` without logging +- Print statements (`println!`, `eprintln!`) for debugging (no structured logging) + +### Naming Conventions + +- **Functions:** `snake_case` (Rust standard) +- **Types:** `PascalCase` (`VpnServer`, `ServerSettings`) +- **Constants:** `UPPER_SNAKE_CASE` (`ID_URL_EDIT`, `MARGIN`, `WM_UPDATE_SERVERS`) +- **Control IDs:** Sequential ranges (checkboxes: 2000+, port edits: 3000+, combos: 4000+, labels: 5000+) + +### Windows API Patterns + +Uses `windows` crate (v0.58) with extensive feature flags: + +```rust +#[cfg(windows)] +use windows::{ + Win32::{ + Foundation::{HWND, LPARAM, WPARAM}, + UI::WindowsAndMessaging::*, + }, +}; +``` + +- **String encoding:** UTF-16 conversion everywhere: `"text\0".encode_utf16().collect::>()` +- **Resource management:** No explicit cleanup (relies on OS to clean up on process exit) +- **Unsafe:** Almost all Win32 calls wrapped in `unsafe { }` blocks +- **Error handling:** `.ok()`, `.unwrap()`, `.expect()` common (crashes on critical failures) + +### UI Control Layout + +Constants define consistent spacing: + +```rust +const MARGIN: i32 = 15; +const FONT_SIZE: i32 = 32; +const LABEL_HEIGHT: i32 = 45; +const CONTROL_HEIGHT: i32 = 45; +const ROW_HEIGHT: i32 = 55; +``` + +Dynamic widths calculated from window size in `WM_SIZE` handler. + +--- + +## Dependencies & External Tools + +### Key Rust Dependencies + +- **tray-icon (0.21):** Cross-platform tray icon (uses Windows native API) +- **windows (0.58):** Direct Win32 API bindings (extensive feature list in Cargo.toml) +- **tokio (1.x):** Async runtime for process management (rt-multi-thread, sync, macros) +- **serde/serde_json (1.x):** Config serialization +- **reqwest (0.12):** HTTP client for subscription fetching (blocking feature) +- **base64 (0.22):** Decode subscription content +- **image (0.25):** Image handling (unused in current code?) +- **v2parser (local path):** Custom parser for VPN URIs (path: `../v2-uri-parser`) + +### Critical External Dependency + +**v2parser** is a local crate located at `../v2-uri-parser` (relative to project root). Must exist for project to build. + +Provides: +- `v2parser::parser::get_metadata(uri: &str) -> String` (JSON string) +- `v2parser::parser::create_json_config(uri, socks_port, http_port) -> String` +- `v2parser::xray_runner::XrayRunner` (process management) + +### External Binaries + +**xray-core:** User must download separately from https://github.com/XTLS/Xray-core/releases +Binary path configured in settings window and saved to config. + +--- + +## Architecture Details + +### Async Runtime Usage + +Tokio runtime is created once at startup and used for: +- Starting xray processes: `TOKIO_RUNTIME.block_on(async { xray_manager::start_server(...).await })` +- Stopping processes: `TOKIO_RUNTIME.block_on(async { xray_manager::stop_all_servers().await })` + +**Important:** Main thread blocks on async operations (no true concurrency for process management). + +### Process Management + +Each enabled server spawns an xray process via `v2parser::xray_runner::XrayRunner`: +- Stored in global `XRAY_PROCESSES: HashMap` +- Server key format: `PROTOCOL://address:port` +- Processes cleaned up on `stop_server()` or `stop_all_servers()` +- All processes stopped on app exit (main.rs:154) + +### Tray Menu Updates + +Two-step update mechanism: +1. Call `request_menu_update()` to set `MENU_UPDATE_REQUESTED` atomic flag +2. Main loop checks flag and calls `update_tray_menu(tray_icon, settings_item, quit_item)` +3. Creates new menu with `create_tray_menu_with_servers()` and sets it via `tray_icon.set_menu()` + +Why? `tray-icon` doesn't support callbacks, and menu must be updated from main thread. + +### Settings Window Lifecycle + +- Created on "Settings" menu item click +- HWND stored in `Arc>>` to prevent duplicates +- Brings to front if already open (main.rs:140-151) +- Custom window class "SettingsWindowClass" with white background +- Custom scroll container class "ScrollContainerClass" with manual scroll handling + +--- + +## Common Gotchas + +### 1. Windows-Only Build + +Project **will not compile** on Linux/macOS due to: +- `#[cfg(windows)]` everywhere +- Direct Win32 API calls +- Platform-specific dependencies + +### 2. Edition 2024 Requirement + +`Cargo.toml` specifies `edition = "2024"` (unreleased as of knowledge cutoff). May need Rust nightly or change to `edition = "2021"`. + +### 3. Local Dependency Path + +`v2parser = { path = "../v2-uri-parser" }` must exist at that exact relative path. Not published on crates.io. + +### 4. Windows Subsystem Flag + +`main.rs:1` has `#![windows_subsystem = "windows"]` commented out for debugging. In release: +- Uncomment to hide console window +- Comment to see stdout/stderr + +### 5. Manifest Embedding + +`build.rs` embeds `app.manifest` via `embed-resource` crate. Required for: +- DPI awareness (PerMonitorV2) +- Windows 10/11 compatibility flags +- Non-admin execution (asInvoker) + +### 6. UTF-16 Null Termination + +All Windows API strings **must** be null-terminated: +```rust +let text: Vec = "Hello\0".encode_utf16().collect(); // ✅ Correct +let text: Vec = "Hello".encode_utf16().collect(); // ❌ Crashes or garbage +``` + +### 7. Control IDs Must Be Unique + +Each window control needs unique ID for `GetDlgItem()`: +- Checkboxes: `ID_SERVER_CHECKBOX_BASE + index` (2000+) +- Port edits: `ID_SERVER_PORT_EDIT_BASE + index` (3000+) +- Combos: `ID_SERVER_PROXY_COMBO_BASE + index` (4000+) + +ID collision causes controls to be unreachable. + +### 8. Custom Message Range + +Custom Windows messages start at `WM_USER + 1`: +```rust +const WM_UPDATE_SERVERS: u32 = WM_USER + 1; +``` + +Don't use values below `WM_USER` (conflicts with system messages). + +### 9. Background Thread UI Updates + +Cannot update UI from background threads. Use `PostMessageW()` to marshal to main thread: +```rust +std::thread::spawn(move || { + let servers = fetch_and_process_vpn_list(&url); + unsafe { + PostMessageW(hwnd, WM_UPDATE_SERVERS, WPARAM(0), LPARAM(0)); + } +}); +``` + +### 10. Scroll Container Children + +Server list controls are children of scroll container, not main window. Must use: +```rust +GetDlgItem(container, control_id) // ✅ Correct +GetDlgItem(hwnd, control_id) // ❌ Returns error +``` + +--- + +## Testing Approach + +### Manual Testing Checklist + +1. **Tray Icon:** + - [ ] Icon appears in system tray (gold star) + - [ ] Right-click shows menu (Settings, Exit) + - [ ] Exit cleanly stops all processes + +2. **Settings Window:** + - [ ] Opens on Settings click + - [ ] Focuses if already open (no duplicates) + - [ ] URL and Xray path persist from config + - [ ] Browse button opens file dialog + - [ ] Update fetches servers and populates list + +3. **Server List:** + - [ ] Checkboxes toggle enabled state + - [ ] Port edits accept numbers only + - [ ] Proxy type combo shows SOCKS/HTTP + - [ ] Scroll works with mouse wheel and scrollbar + - [ ] Window resize adjusts layout + +4. **Save Functionality:** + - [ ] Save writes to `%APPDATA%\Xray-VPN-Manager\config.json` + - [ ] Servers restart on save + - [ ] Tray menu updates with running servers + - [ ] Settings persist after app restart + +5. **Error Handling:** + - [ ] Invalid URL shows no error (silent failure) + - [ ] Missing xray binary path causes start failures (check console) + - [ ] Invalid port numbers handled gracefully + +### No Automated Tests + +Currently no unit tests, integration tests, or CI/CD pipeline. All validation is manual. + +--- + +## Adding New Features + +### Adding a New Menu Item + +1. Create `MenuItem` in `main.rs`: + ```rust + let new_item = MenuItem::new("New Feature", true, None); + ``` + +2. Add to menu in `ui/tray.rs`: + ```rust + tray_menu.append(&new_item).unwrap(); + ``` + +3. Handle event in main loop: + ```rust + if event.id == new_item.id() { + // Handle click + } + ``` + +### Adding a Config Field + +1. Add to `Config` struct in `config.rs`: + ```rust + pub struct Config { + pub subscription_url: String, + pub xray_binary_path: String, + pub new_field: String, // Add this + #[serde(default)] + pub server_settings: HashMap, + } + ``` + +2. Update `Default` impl: + ```rust + fn default() -> Self { + Config { + // ... + new_field: String::new(), + // ... + } + } + ``` + +3. Add UI control in `settings_window.rs`: + - Define control ID constant + - Create control in `create_controls()` + - Read value in Save button handler + +### Adding a New VPN Protocol + +1. Update `parse_vpn_uri()` in `vpn/mod.rs`: + ```rust + let is_supported = uri.starts_with("vless://") + || uri.starts_with("newprotocol://"); // Add this + ``` + +2. Ensure `v2parser` crate supports the protocol (external dependency). + +--- + +## Debugging Tips + +### Enable Console Output + +Ensure `#![windows_subsystem = "windows"]` is **commented** in `main.rs:1`. + +### Common Debug Points + +1. **Subscription fetch fails:** + - Check URL is valid and returns base64-encoded content + - Add `println!` in `fetch_and_process_vpn_list()` to see response + +2. **Servers don't start:** + - Verify xray binary path exists + - Check `xray_manager::start_server()` errors in console + - Ensure `v2parser` generates valid config + +3. **Menu doesn't update:** + - Verify `request_menu_update()` called + - Check `MENU_UPDATE_REQUESTED` flag in main loop + - Ensure `VPN_SERVERS` mutex populated + +4. **Settings window controls missing:** + - Check control IDs are unique + - Verify `GetDlgItem()` uses correct parent (container vs. hwnd) + - Look for "Failed to create..." messages + +5. **DPI scaling issues:** + - Verify `app.manifest` embedded (check `build.rs` runs) + - Ensure `SetProcessDpiAwarenessContext` called at startup + +### Useful Print Statements + +```rust +println!("Settings window created: {:?}", hwnd); +println!("URL entered: {}", url); +println!("Started server: {}", server.name); +eprintln!("Failed to start server {}: {}", server.name, e); +``` + +Already present in code for basic debugging. + +--- + +## Performance Considerations + +### UI Thread Blocking + +Main thread blocks on: +- `GetMessageW()` (Windows message pump) +- `TOKIO_RUNTIME.block_on()` (async operations) + +Keep async operations fast to avoid UI freezes. + +### Scroll Performance + +Settings window with many servers (100+) may lag: +- Each server creates 4-5 controls (checkbox, label, edit, combo) +- Scroll handler moves all children on every scroll event +- Consider virtualization if server count exceeds ~50 + +### Subscription Fetch + +Blocking HTTP call in background thread: +```rust +reqwest::blocking::get(url) +``` + +No timeout configured. May hang on slow/unresponsive servers. + +--- + +## Security & Safety + +### Unsafe Code + +Extensive use of `unsafe` for Windows API: +- String pointer conversions (`PCWSTR::from_raw()`) +- Window handles (`HWND` casting) +- Message parameter packing (`WPARAM`, `LPARAM`) + +**Assumption:** Windows API contracts upheld (e.g., null-terminated strings). + +### Process Execution + +Executes arbitrary xray binary from user-configured path. No validation of binary integrity. + +### Network Requests + +Fetches subscription URLs without TLS verification control. Uses `reqwest` defaults (should verify certificates). + +### Config Storage + +Plaintext JSON in `%APPDATA%`. No encryption. Contains: +- Subscription URLs (may include credentials in query params) +- Server addresses/ports +- Local proxy ports + +**Not suitable for highly sensitive credentials.** + +--- + +## Future Improvements + +Based on code analysis: + +1. **Add tests:** Unit tests for config, integration tests for subscription parsing +2. **Logging:** Replace `println!` with structured logging (e.g., `tracing`, `env_logger`) +3. **Error handling:** Return structured errors instead of `String`, display errors in UI +4. **Timeouts:** Add request timeouts to `reqwest::blocking::get()` +5. **Virtualized list:** For 50+ servers, implement virtual scrolling +6. **Tray menu callbacks:** Investigate better tray update mechanism (polling is suboptimal) +7. **CI/CD:** Add GitHub Actions for Windows builds +8. **Installer:** Package as MSI/NSIS installer instead of bare `.exe` +9. **Auto-update:** Check for new versions on startup +10. **Connection testing:** Ping/test servers before enabling + +--- + +## Build Troubleshooting + +### "edition 2024 not found" + +Change `Cargo.toml`: +```toml +edition = "2021" # Change from 2024 +``` + +### "v2parser not found" + +Ensure `../v2-uri-parser` exists relative to project root. Clone or create it separately. + +### "embed-resource failed" + +Ensure Windows SDK installed (required for `rc.exe` resource compiler). + +### Missing Windows features + +If compile fails with missing types, add to `Cargo.toml`: +```toml +[target.'cfg(windows)'.dependencies] +windows = { version = "0.58", features = [ + # Add missing feature here +] } +``` + +--- + +## Summary for AI Agents + +When working in this codebase: + +1. **Platform:** Windows-only, will not compile elsewhere +2. **External dep:** `v2parser` at `../v2-uri-parser` required +3. **Testing:** Manual only, no automated tests +4. **UI:** Native Win32, complex custom controls in `settings_window.rs` +5. **Async:** Tokio runtime used, but main thread blocks on operations +6. **State:** Global mutexes (`VPN_SERVERS`, `XRAY_PROCESSES`) shared across threads +7. **Config:** `%APPDATA%\Xray-VPN-Manager\config.json`, plaintext JSON +8. **Strings:** Always UTF-16 with null terminator for Windows API +9. **IDs:** Control IDs must be unique, use defined constants + index +10. **Updates:** UI updates from background threads via `PostMessageW()` + +**Most complex file:** `ui/settings_window.rs` (1200+ lines, custom scrolling, dynamic layout) +**Most critical function:** `restart_xray_servers()` in `main.rs` (stops/starts all servers) +**Most fragile part:** Windows API unsafe code (crashes if assumptions violated) diff --git a/Cargo.lock b/Cargo.lock index aca8b8a..f0bb040 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,40 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Xray-VPN-Manager" +version = "0.1.0" +dependencies = [ + "base64", + "embed-resource", + "image", + "reqwest", + "serde", + "serde_json", + "tokio", + "tray-icon", + "v2parser", + "windows", + "zip", +] + [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -96,6 +124,9 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arg_enum_proc_macro" @@ -237,6 +268,15 @@ dependencies = [ "core2", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -264,6 +304,12 @@ version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -276,6 +322,25 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -329,6 +394,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.53" @@ -381,6 +456,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation" version = "0.9.4" @@ -406,6 +487,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -455,6 +560,53 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "dirs" version = "6.0.0" @@ -821,6 +973,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -839,9 +1001,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1044,6 +1208,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.4.0" @@ -1315,6 +1488,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -1515,6 +1697,27 @@ dependencies = [ "imgref", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -1646,6 +1849,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.2" @@ -1868,6 +2077,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1927,6 +2146,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2438,6 +2663,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2679,6 +2915,25 @@ dependencies = [ "zune-jpeg 0.4.21", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + [[package]] name = "tinystr" version = "0.8.2" @@ -2886,6 +3141,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -3100,22 +3361,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" -[[package]] -name = "Xray-VPN-Manager" -version = "0.1.0" -dependencies = [ - "base64", - "embed-resource", - "image", - "reqwest", - "serde", - "serde_json", - "tokio", - "tray-icon", - "v2parser", - "windows", -] - [[package]] name = "winapi" version = "0.3.9" @@ -3500,6 +3745,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "y4m" version = "0.8.0" @@ -3575,6 +3829,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] [[package]] name = "zerotrie" @@ -3609,12 +3877,82 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.17", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + [[package]] name = "zmij" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index df175bf..95ca91e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,10 @@ reqwest = { version = "0.12", features = ["blocking"] } base64 = "0.22" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -v2parser = { path = "../v2-uri-parser" } +#v2parser = { path = "../v2-uri-parser" } +v2parser = { git = "https://github.com/house-of-vanity/v2-uri-parser.git" } tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros"] } +zip = "2.2" [build-dependencies] embed-resource = "2.5" @@ -27,4 +29,5 @@ windows = { version = "0.58", features = [ "Win32_UI_Shell", "Win32_UI_Shell_Common", "Win32_System_Com", + "Win32_System_Registry", ] } diff --git a/src/config.rs b/src/config.rs index a5214ed..50da4f2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,6 +21,8 @@ pub struct Config { pub xray_binary_path: String, #[serde(default)] pub server_settings: HashMap, + #[serde(default)] + pub autostart: bool, } impl Default for Config { @@ -29,6 +31,7 @@ impl Default for Config { subscription_url: String::new(), xray_binary_path: String::new(), server_settings: HashMap::new(), + autostart: false, } } } @@ -82,4 +85,63 @@ impl Config { Ok(()) } + + /// Set autostart in Windows registry + pub fn set_autostart(enabled: bool) -> Result<(), String> { + #[cfg(windows)] + { + use windows::{ + core::w, + Win32::System::Registry::{ + RegOpenKeyExW, RegSetValueExW, RegDeleteKeyValueW, + HKEY_CURRENT_USER, KEY_WRITE, REG_SZ, + }, + }; + + let key_path = w!("Software\\Microsoft\\Windows\\CurrentVersion\\Run"); + let value_name = w!("Xray-VPN-Manager"); + + unsafe { + let mut hkey = Default::default(); + let result = RegOpenKeyExW(HKEY_CURRENT_USER, key_path, 0, KEY_WRITE, &mut hkey); + if result.is_err() { + return Err(format!("Failed to open registry key: {:?}", result)); + } + + if enabled { + // Get current executable path + let exe_path = std::env::current_exe() + .map_err(|e| format!("Failed to get exe path: {}", e))?; + let exe_path_str = exe_path.to_string_lossy().to_string(); + + let path_wide: Vec = format!("{}\0", exe_path_str).encode_utf16().collect(); + let data = std::slice::from_raw_parts( + path_wide.as_ptr() as *const u8, + path_wide.len() * 2 + ); + + let result = RegSetValueExW( + hkey, + value_name, + 0, + REG_SZ, + Some(data), + ); + if result.is_err() { + return Err(format!("Failed to set registry value: {:?}", result)); + } + } else { + // Remove from autostart + let _ = RegDeleteKeyValueW(hkey, None, value_name); + } + } + } + + #[cfg(not(windows))] + { + return Err("Autostart only supported on Windows".to_string()); + } + + Ok(()) + } } diff --git a/src/ui/settings_window.rs b/src/ui/settings_window.rs index befe5bd..0202030 100644 --- a/src/ui/settings_window.rs +++ b/src/ui/settings_window.rs @@ -3,7 +3,7 @@ use windows::{ core::{PCWSTR, w}, Win32::{ Foundation::{HWND, LPARAM, LRESULT, WPARAM, HINSTANCE, RECT, BOOL}, - Graphics::Gdi::{UpdateWindow, HBRUSH, SetBkMode, TRANSPARENT, HDC, GetStockObject, WHITE_BRUSH}, + Graphics::Gdi::{UpdateWindow, HBRUSH, SetBkMode, TRANSPARENT, HDC, GetStockObject, WHITE_BRUSH, InvalidateRect}, System::LibraryLoader::GetModuleHandleW, UI::WindowsAndMessaging::*, }, @@ -23,11 +23,16 @@ const ID_SAVE_BUTTON: i32 = 1005; const ID_CANCEL_BUTTON: i32 = 1006; const ID_XRAY_PATH_EDIT: i32 = 1007; const ID_XRAY_BROWSE_BUTTON: i32 = 1008; +const ID_XRAY_DOWNLOAD_BUTTON: i32 = 1009; +const ID_AUTOSTART_CHECKBOX: i32 = 1010; const ID_SERVER_CHECKBOX_BASE: i32 = 2000; // 2000, 2001, 2002... const ID_SERVER_PORT_EDIT_BASE: i32 = 3000; // 3000, 3001, 3002... const ID_SERVER_PROXY_COMBO_BASE: i32 = 4000; // 4000, 4001, 4002... const ID_SERVER_LABEL_BASE: i32 = 5000; // 5000, 5001, 5002... for "Proxy Port:" labels +// Custom Windows message for download completion +const WM_DOWNLOAD_COMPLETE: u32 = WM_USER + 2; + // Layout constants for consistent formatting const MARGIN: i32 = 15; const FONT_SIZE: i32 = 32; // Reduced from 40 @@ -341,7 +346,32 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) { unsafe { SendMessageW(lbl, WM_SETFONT, WPARAM(hfont.0 as usize), LPARAM(1)); } } - // Xray binary path edit control + // Download button (above the path field) - same width as URL edit control + let download_btn_text: Vec = "Download Xray Automatically\0".encode_utf16().collect(); + let download_btn = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE::default(), + w!("BUTTON"), + PCWSTR::from_raw(download_btn_text.as_ptr()), + WS_CHILD | WS_VISIBLE | WINDOW_STYLE(BS_PUSHBUTTON as u32), + MARGIN + URL_LABEL_WIDTH + 10, + row2_y, + 450, + CONTROL_HEIGHT, + parent, + HMENU(ID_XRAY_DOWNLOAD_BUTTON as _), + hinstance, + None, + ).ok() + }; + if let Some(btn) = download_btn { + unsafe { SendMessageW(btn, WM_SETFONT, WPARAM(hfont.0 as usize), LPARAM(1)); } + } + + // Third row Y position (for path field) + let row3_y = row2_y + CONTROL_HEIGHT + MARGIN; + + // Xray binary path edit control - same width as URL edit control let xray_path_text_wide: Vec = format!("{}\0", config.xray_binary_path).encode_utf16().collect(); let xray_path_edit = unsafe { CreateWindowExW( @@ -350,7 +380,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) { PCWSTR::from_raw(xray_path_text_wide.as_ptr()), WS_CHILD | WS_VISIBLE | WS_BORDER | WINDOW_STYLE(ES_AUTOHSCROLL as u32), MARGIN + URL_LABEL_WIDTH + 10, - row2_y, + row3_y, 450, CONTROL_HEIGHT, parent, @@ -370,7 +400,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) { PCWSTR::from_raw(browse_btn_text.as_ptr()), WS_CHILD | WS_VISIBLE | WINDOW_STYLE(BS_PUSHBUTTON as u32), MARGIN + URL_LABEL_WIDTH + 10 + 450 + 10, - row2_y, + row3_y, 120, CONTROL_HEIGHT, parent, @@ -383,8 +413,42 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) { unsafe { SendMessageW(btn, WM_SETFONT, WPARAM(hfont.0 as usize), LPARAM(1)); } } - // Third row Y position - let row3_y = row2_y + CONTROL_HEIGHT + MARGIN; + // Fourth row Y position + let row4_y = row3_y + CONTROL_HEIGHT + MARGIN; + + // Autostart checkbox + let autostart_text: Vec = "Start automatically on Windows startup\0".encode_utf16().collect(); + let autostart_checkbox = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE::default(), + w!("BUTTON"), + PCWSTR::from_raw(autostart_text.as_ptr()), + WS_CHILD | WS_VISIBLE | WINDOW_STYLE(BS_AUTOCHECKBOX as u32), + MARGIN + URL_LABEL_WIDTH + 10, + row4_y, + 450, + CONTROL_HEIGHT, + parent, + HMENU(ID_AUTOSTART_CHECKBOX as _), + hinstance, + None, + ).ok() + }; + if let Some(cb) = autostart_checkbox { + unsafe { + SendMessageW(cb, WM_SETFONT, WPARAM(hfont.0 as usize), LPARAM(1)); + // Set checkbox state based on config + SendMessageW( + cb, + BM_SETCHECK, + WPARAM(if config.autostart { 1 } else { 0 }), + LPARAM(0), + ); + } + } + + // Fifth row Y position + let row5_y = row4_y + CONTROL_HEIGHT + MARGIN; // Label "VPN Servers:" let list_label: Vec = "VPN Servers:\0".encode_utf16().collect(); @@ -395,7 +459,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) { PCWSTR::from_raw(list_label.as_ptr()), WS_CHILD | WS_VISIBLE, MARGIN, - row3_y, + row5_y, 200, LABEL_HEIGHT, parent, @@ -409,7 +473,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) { } // Server list container Y position - let container_y = row3_y + LABEL_HEIGHT + 10; + let container_y = row5_y + LABEL_HEIGHT + 10; // Get client area size to calculate container height dynamically let mut client_rect = RECT::default(); @@ -694,6 +758,59 @@ unsafe extern "system" fn settings_window_proc( } } } + // Handle Download button for Xray binary + else if control_id == ID_XRAY_DOWNLOAD_BUTTON as usize && notification_code == 0 { + println!("Download button clicked!"); + + // Disable download button during download + if let Ok(download_btn) = unsafe { GetDlgItem(hwnd, ID_XRAY_DOWNLOAD_BUTTON) } { + if !download_btn.is_invalid() { + unsafe { + // Disable by setting WS_DISABLED style + let style = GetWindowLongW(download_btn, GWL_STYLE); + SetWindowLongW(download_btn, GWL_STYLE, style | WS_DISABLED.0 as i32); + let _ = InvalidateRect(download_btn, None, true); + + let downloading_text: Vec = "Downloading...\0".encode_utf16().collect(); + SetWindowTextW(download_btn, PCWSTR::from_raw(downloading_text.as_ptr())).ok(); + } + } + } + + // Spawn background thread for download + let hwnd_raw = hwnd.0 as isize; + std::thread::spawn(move || { + match download_xray_latest() { + Ok(xray_exe_path) => { + println!("Downloaded Xray to: {}", xray_exe_path); + + // Update UI on main thread + unsafe { + let hwnd = HWND(hwnd_raw as *mut _); + + // Allocate string on heap for passing to main thread + let path_box = Box::new(xray_exe_path); + let path_ptr = Box::into_raw(path_box); + + let _ = PostMessageW(hwnd, WM_DOWNLOAD_COMPLETE, WPARAM(1), LPARAM(path_ptr as isize)); + } + } + Err(e) => { + eprintln!("Failed to download Xray: {}", e); + + // Send error message + unsafe { + let hwnd = HWND(hwnd_raw as *mut _); + + let error_box = Box::new(e); + let error_ptr = Box::into_raw(error_box); + + let _ = PostMessageW(hwnd, WM_DOWNLOAD_COMPLETE, WPARAM(0), LPARAM(error_ptr as isize)); + } + } + } + }); + } // Handle Save button else if control_id == ID_SAVE_BUTTON as usize && notification_code == 0 { @@ -725,6 +842,16 @@ unsafe extern "system" fn settings_window_proc( String::new() }; + // Get autostart checkbox state + let autostart = unsafe { + if let Ok(checkbox) = GetDlgItem(hwnd, ID_AUTOSTART_CHECKBOX) { + let state = SendMessageW(checkbox, BM_GETCHECK, WPARAM(0), LPARAM(0)); + state.0 == 1 + } else { + false + } + }; + // Build server_settings HashMap from current servers use std::collections::HashMap; let mut server_settings = HashMap::new(); @@ -748,10 +875,16 @@ unsafe extern "system" fn settings_window_proc( subscription_url, xray_binary_path, server_settings, + autostart, }; match config.save() { Ok(_) => { + // Apply autostart setting to registry + if let Err(e) = crate::config::Config::set_autostart(autostart) { + eprintln!("Failed to set autostart: {}", e); + } + // Restart xray servers with new config crate::restart_xray_servers(); @@ -799,7 +932,9 @@ unsafe extern "system" fn settings_window_proc( let row1_y = MARGIN; let row2_y = row1_y + CONTROL_HEIGHT + MARGIN; let row3_y = row2_y + CONTROL_HEIGHT + MARGIN; - let container_y = row3_y + LABEL_HEIGHT + 10; + let row4_y = row3_y + CONTROL_HEIGHT + MARGIN; + let row5_y = row4_y + CONTROL_HEIGHT + MARGIN; + let container_y = row5_y + LABEL_HEIGHT + 10; let container_height = height - container_y - MARGIN - BUTTON_ROW_HEIGHT; let buttons_y = container_y + container_height + 10; @@ -853,7 +988,7 @@ unsafe extern "system" fn settings_window_proc( browse_btn, None, width - 120 - MARGIN, - row2_y, + row3_y, 0, 0, SWP_NOSIZE | SWP_NOZORDER, ).ok(); @@ -918,6 +1053,65 @@ unsafe extern "system" fn settings_window_proc( } LRESULT(0) } + _ if msg == WM_DOWNLOAD_COMPLETE => { + // Custom message: download complete + let success = wparam.0 == 1; + let data_ptr = lparam.0 as *mut String; + + if success { + // Success - update path field + unsafe { + let path = Box::from_raw(data_ptr); + + // Update edit control + if let Ok(xray_edit) = GetDlgItem(hwnd, ID_XRAY_PATH_EDIT) { + let path_wide: Vec = format!("{}\0", path).encode_utf16().collect(); + SetWindowTextW(xray_edit, PCWSTR::from_raw(path_wide.as_ptr())).ok(); + } + + // Show success message + let msg: Vec = "Xray binary downloaded successfully!\0".encode_utf16().collect(); + let title: Vec = "Success\0".encode_utf16().collect(); + MessageBoxW( + hwnd, + PCWSTR::from_raw(msg.as_ptr()), + PCWSTR::from_raw(title.as_ptr()), + MB_OK | MB_ICONINFORMATION, + ); + } + } else { + // Error - show error message + unsafe { + let error_msg = Box::from_raw(data_ptr); + + let msg: Vec = format!("Failed to download Xray:\n{}\0", error_msg).encode_utf16().collect(); + let title: Vec = "Error\0".encode_utf16().collect(); + MessageBoxW( + hwnd, + PCWSTR::from_raw(msg.as_ptr()), + PCWSTR::from_raw(title.as_ptr()), + MB_OK | MB_ICONERROR, + ); + } + } + + // Re-enable download button + if let Ok(download_btn) = unsafe { GetDlgItem(hwnd, ID_XRAY_DOWNLOAD_BUTTON) } { + if !download_btn.is_invalid() { + unsafe { + // Re-enable by removing WS_DISABLED style + let style = GetWindowLongW(download_btn, GWL_STYLE); + SetWindowLongW(download_btn, GWL_STYLE, style & !(WS_DISABLED.0 as i32)); + let _ = InvalidateRect(download_btn, None, true); + + let download_text: Vec = "Download\0".encode_utf16().collect(); + SetWindowTextW(download_btn, PCWSTR::from_raw(download_text.as_ptr())).ok(); + } + } + } + + LRESULT(0) + } WM_DESTROY => { println!("Settings window destroyed"); LRESULT(0) @@ -1198,7 +1392,7 @@ unsafe fn resize_server_list_items(container: HWND) { } } - // Reposition combo + // Reposition combo (keep dropdown height at 200) if let Ok(combo) = unsafe { GetDlgItem(container, ID_SERVER_PROXY_COMBO_BASE + idx as i32) } { if !combo.is_invalid() { unsafe { @@ -1208,7 +1402,7 @@ unsafe fn resize_server_list_items(container: HWND) { right_controls_x + LABEL_WIDTH + 5 + PORT_EDIT_WIDTH + 10, y_pos, COMBO_WIDTH, - CONTROL_HEIGHT, + 200, // Keep dropdown height SWP_NOZORDER, ).ok(); } @@ -1225,3 +1419,129 @@ unsafe extern "system" fn destroy_child_window(hwnd: HWND, _: LPARAM) -> BOOL { unsafe { DestroyWindow(hwnd).ok() }; true.into() } + +/// Download latest Xray-core binary from GitHub releases +fn download_xray_latest() -> Result { + use std::fs; + use std::io::Write; + + println!("Starting Xray download..."); + + // Get config directory + let config_dir = crate::config::Config::get_config_path() + .map_err(|e| format!("Failed to get config path: {}", e))? + .parent() + .ok_or("Failed to get config directory")? + .to_path_buf(); + + // Create xray subdirectory + let xray_dir = config_dir.join("xray"); + fs::create_dir_all(&xray_dir) + .map_err(|e| format!("Failed to create xray directory: {}", e))?; + + // Fetch latest release page to get actual version + println!("Fetching latest release info..."); + let client = reqwest::blocking::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|e| format!("Failed to create HTTP client: {}", e))?; + + let response = client + .get("https://github.com/XTLS/Xray-core/releases/latest") + .send() + .map_err(|e| format!("Failed to fetch latest release: {}", e))?; + + // Get redirect location to determine version + let location = response + .headers() + .get("location") + .ok_or("No redirect location found")? + .to_str() + .map_err(|_| "Invalid redirect location")?; + + println!("Redirect location: {}", location); + + // Extract version from redirect URL (e.g., /XTLS/Xray-core/releases/tag/v25.12.8) + let version = location + .split('/') + .last() + .ok_or("Failed to parse version from redirect")?; + + println!("Latest version: {}", version); + + // Construct download URL + let download_url = format!( + "https://github.com/XTLS/Xray-core/releases/download/{}/Xray-windows-64.zip", + version + ); + + println!("Downloading from: {}", download_url); + + // Download zip file + let zip_data = reqwest::blocking::get(&download_url) + .map_err(|e| format!("Failed to download zip: {}", e))? + .bytes() + .map_err(|e| format!("Failed to read zip data: {}", e))?; + + println!("Downloaded {} bytes", zip_data.len()); + + // Save zip temporarily + let zip_path = xray_dir.join("xray.zip"); + let mut zip_file = fs::File::create(&zip_path) + .map_err(|e| format!("Failed to create zip file: {}", e))?; + zip_file.write_all(&zip_data) + .map_err(|e| format!("Failed to write zip file: {}", e))?; + + println!("Extracting zip..."); + + // Extract zip + let zip_file = fs::File::open(&zip_path) + .map_err(|e| format!("Failed to open zip file: {}", e))?; + + let mut archive = zip::ZipArchive::new(zip_file) + .map_err(|e| format!("Failed to open zip archive: {}", e))?; + + // Extract all files + for i in 0..archive.len() { + let mut file = archive.by_index(i) + .map_err(|e| format!("Failed to read zip entry: {}", e))?; + + let outpath = match file.enclosed_name() { + Some(path) => xray_dir.join(path), + None => continue, + }; + + if file.name().ends_with('/') { + // Directory + fs::create_dir_all(&outpath) + .map_err(|e| format!("Failed to create directory: {}", e))?; + } else { + // File + if let Some(parent) = outpath.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create parent directory: {}", e))?; + } + + let mut outfile = fs::File::create(&outpath) + .map_err(|e| format!("Failed to create file: {}", e))?; + + std::io::copy(&mut file, &mut outfile) + .map_err(|e| format!("Failed to extract file: {}", e))?; + + println!("Extracted: {}", outpath.display()); + } + } + + // Delete zip file + let _ = fs::remove_file(&zip_path); + + // Find xray.exe in extracted files + let xray_exe = xray_dir.join("xray.exe"); + if !xray_exe.exists() { + return Err("xray.exe not found in extracted files".to_string()); + } + + println!("Xray extracted to: {}", xray_exe.display()); + + Ok(xray_exe.to_string_lossy().to_string()) +}