2 Commits

Author SHA1 Message Date
f3f7edf6df Bump version 2025-12-27 00:26:01 +00:00
ad2ea77a4f Added xray.exe downloading and autostart 2025-12-27 00:25:33 +00:00
5 changed files with 1366 additions and 30 deletions

613
AGENTS.md Normal file
View File

@@ -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<tokio::runtime::Runtime> = LazyLock::new(|| {
tokio::runtime::Runtime::new().expect("Failed to create tokio runtime")
});
pub static VPN_SERVERS: Mutex<Option<Vec<VpnServer>>> = Mutex::new(None);
pub static XRAY_PROCESSES: LazyLock<Mutex<HashMap<String, XrayRunner>>> = ...;
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<T, String>` (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::<Vec<u16>>()`
- **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, XrayRunner>`
- 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<Mutex<Option<HWND>>>` 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<u16> = "Hello\0".encode_utf16().collect(); // ✅ Correct
let text: Vec<u16> = "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<String, ServerSettings>,
}
```
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)

370
Cargo.lock generated
View File

@@ -2,12 +2,40 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.1" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.4"
@@ -96,6 +124,9 @@ name = "arbitrary"
version = "1.4.2" version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "arg_enum_proc_macro" name = "arg_enum_proc_macro"
@@ -237,6 +268,15 @@ dependencies = [
"core2", "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]] [[package]]
name = "block2" name = "block2"
version = "0.6.2" version = "0.6.2"
@@ -264,6 +304,12 @@ version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "byteorder-lite" name = "byteorder-lite"
version = "0.1.0" version = "0.1.0"
@@ -276,6 +322,25 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 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]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.18.5" version = "0.18.5"
@@ -329,6 +394,16 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 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]] [[package]]
name = "clap" name = "clap"
version = "4.5.53" version = "4.5.53"
@@ -381,6 +456,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -406,6 +487,30 @@ dependencies = [
"memchr", "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]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -455,6 +560,53 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 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]] [[package]]
name = "dirs" name = "dirs"
version = "6.0.0" version = "6.0.0"
@@ -821,6 +973,16 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.16" version = "0.2.16"
@@ -839,9 +1001,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasip2", "wasip2",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1044,6 +1208,15 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.4.0" version = "1.4.0"
@@ -1315,6 +1488,15 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "interpolate_name" name = "interpolate_name"
version = "0.2.4" version = "0.2.4"
@@ -1515,6 +1697,27 @@ dependencies = [
"imgref", "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]] [[package]]
name = "maybe-rayon" name = "maybe-rayon"
version = "0.1.1" version = "0.1.1"
@@ -1646,6 +1849,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-derive" name = "num-derive"
version = "0.4.2" version = "0.4.2"
@@ -1868,6 +2077,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" 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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -1927,6 +2146,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@@ -2438,6 +2663,17 @@ dependencies = [
"serde", "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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -2679,6 +2915,25 @@ dependencies = [
"zune-jpeg 0.4.21", "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]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.2" version = "0.8.2"
@@ -2886,6 +3141,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.22" version = "1.0.22"
@@ -3100,22 +3361,6 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -3500,6 +3745,15 @@ dependencies = [
"pkg-config", "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]] [[package]]
name = "y4m" name = "y4m"
version = "0.8.0" version = "0.8.0"
@@ -3575,6 +3829,20 @@ name = "zeroize"
version = "1.8.2" version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 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]] [[package]]
name = "zerotrie" name = "zerotrie"
@@ -3609,12 +3877,82 @@ dependencies = [
"syn 2.0.111", "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]] [[package]]
name = "zmij" name = "zmij"
version = "0.1.9" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" 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]] [[package]]
name = "zune-core" name = "zune-core"
version = "0.4.12" version = "0.4.12"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "Xray-VPN-Manager" name = "Xray-VPN-Manager"
version = "0.1.0" version = "0.2.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
@@ -10,8 +10,10 @@ reqwest = { version = "0.12", features = ["blocking"] }
base64 = "0.22" base64 = "0.22"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" 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"] } tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros"] }
zip = "2.2"
[build-dependencies] [build-dependencies]
embed-resource = "2.5" embed-resource = "2.5"
@@ -27,4 +29,5 @@ windows = { version = "0.58", features = [
"Win32_UI_Shell", "Win32_UI_Shell",
"Win32_UI_Shell_Common", "Win32_UI_Shell_Common",
"Win32_System_Com", "Win32_System_Com",
"Win32_System_Registry",
] } ] }

View File

@@ -21,6 +21,8 @@ pub struct Config {
pub xray_binary_path: String, pub xray_binary_path: String,
#[serde(default)] #[serde(default)]
pub server_settings: HashMap<String, ServerSettings>, pub server_settings: HashMap<String, ServerSettings>,
#[serde(default)]
pub autostart: bool,
} }
impl Default for Config { impl Default for Config {
@@ -29,6 +31,7 @@ impl Default for Config {
subscription_url: String::new(), subscription_url: String::new(),
xray_binary_path: String::new(), xray_binary_path: String::new(),
server_settings: HashMap::new(), server_settings: HashMap::new(),
autostart: false,
} }
} }
} }
@@ -82,4 +85,63 @@ impl Config {
Ok(()) 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<u16> = 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(())
}
} }

View File

@@ -3,7 +3,7 @@ use windows::{
core::{PCWSTR, w}, core::{PCWSTR, w},
Win32::{ Win32::{
Foundation::{HWND, LPARAM, LRESULT, WPARAM, HINSTANCE, RECT, BOOL}, 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, System::LibraryLoader::GetModuleHandleW,
UI::WindowsAndMessaging::*, UI::WindowsAndMessaging::*,
}, },
@@ -23,11 +23,16 @@ const ID_SAVE_BUTTON: i32 = 1005;
const ID_CANCEL_BUTTON: i32 = 1006; const ID_CANCEL_BUTTON: i32 = 1006;
const ID_XRAY_PATH_EDIT: i32 = 1007; const ID_XRAY_PATH_EDIT: i32 = 1007;
const ID_XRAY_BROWSE_BUTTON: i32 = 1008; 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_CHECKBOX_BASE: i32 = 2000; // 2000, 2001, 2002...
const ID_SERVER_PORT_EDIT_BASE: i32 = 3000; // 3000, 3001, 3002... 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_PROXY_COMBO_BASE: i32 = 4000; // 4000, 4001, 4002...
const ID_SERVER_LABEL_BASE: i32 = 5000; // 5000, 5001, 5002... for "Proxy Port:" labels 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 // Layout constants for consistent formatting
const MARGIN: i32 = 15; const MARGIN: i32 = 15;
const FONT_SIZE: i32 = 32; // Reduced from 40 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)); } 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<u16> = "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<u16> = format!("{}\0", config.xray_binary_path).encode_utf16().collect(); let xray_path_text_wide: Vec<u16> = format!("{}\0", config.xray_binary_path).encode_utf16().collect();
let xray_path_edit = unsafe { let xray_path_edit = unsafe {
CreateWindowExW( CreateWindowExW(
@@ -350,7 +380,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) {
PCWSTR::from_raw(xray_path_text_wide.as_ptr()), PCWSTR::from_raw(xray_path_text_wide.as_ptr()),
WS_CHILD | WS_VISIBLE | WS_BORDER | WINDOW_STYLE(ES_AUTOHSCROLL as u32), WS_CHILD | WS_VISIBLE | WS_BORDER | WINDOW_STYLE(ES_AUTOHSCROLL as u32),
MARGIN + URL_LABEL_WIDTH + 10, MARGIN + URL_LABEL_WIDTH + 10,
row2_y, row3_y,
450, 450,
CONTROL_HEIGHT, CONTROL_HEIGHT,
parent, parent,
@@ -370,7 +400,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) {
PCWSTR::from_raw(browse_btn_text.as_ptr()), PCWSTR::from_raw(browse_btn_text.as_ptr()),
WS_CHILD | WS_VISIBLE | WINDOW_STYLE(BS_PUSHBUTTON as u32), WS_CHILD | WS_VISIBLE | WINDOW_STYLE(BS_PUSHBUTTON as u32),
MARGIN + URL_LABEL_WIDTH + 10 + 450 + 10, MARGIN + URL_LABEL_WIDTH + 10 + 450 + 10,
row2_y, row3_y,
120, 120,
CONTROL_HEIGHT, CONTROL_HEIGHT,
parent, 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)); } unsafe { SendMessageW(btn, WM_SETFONT, WPARAM(hfont.0 as usize), LPARAM(1)); }
} }
// Third row Y position // Fourth row Y position
let row3_y = row2_y + CONTROL_HEIGHT + MARGIN; let row4_y = row3_y + CONTROL_HEIGHT + MARGIN;
// Autostart checkbox
let autostart_text: Vec<u16> = "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:" // Label "VPN Servers:"
let list_label: Vec<u16> = "VPN Servers:\0".encode_utf16().collect(); let list_label: Vec<u16> = "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()), PCWSTR::from_raw(list_label.as_ptr()),
WS_CHILD | WS_VISIBLE, WS_CHILD | WS_VISIBLE,
MARGIN, MARGIN,
row3_y, row5_y,
200, 200,
LABEL_HEIGHT, LABEL_HEIGHT,
parent, parent,
@@ -409,7 +473,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) {
} }
// Server list container Y position // 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 // Get client area size to calculate container height dynamically
let mut client_rect = RECT::default(); 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<u16> = "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 // Handle Save button
else if control_id == ID_SAVE_BUTTON as usize && notification_code == 0 { 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() 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 // Build server_settings HashMap from current servers
use std::collections::HashMap; use std::collections::HashMap;
let mut server_settings = HashMap::new(); let mut server_settings = HashMap::new();
@@ -748,10 +875,16 @@ unsafe extern "system" fn settings_window_proc(
subscription_url, subscription_url,
xray_binary_path, xray_binary_path,
server_settings, server_settings,
autostart,
}; };
match config.save() { match config.save() {
Ok(_) => { 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 // Restart xray servers with new config
crate::restart_xray_servers(); crate::restart_xray_servers();
@@ -799,7 +932,9 @@ unsafe extern "system" fn settings_window_proc(
let row1_y = MARGIN; let row1_y = MARGIN;
let row2_y = row1_y + CONTROL_HEIGHT + MARGIN; let row2_y = row1_y + CONTROL_HEIGHT + MARGIN;
let row3_y = row2_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 container_height = height - container_y - MARGIN - BUTTON_ROW_HEIGHT;
let buttons_y = container_y + container_height + 10; let buttons_y = container_y + container_height + 10;
@@ -853,7 +988,7 @@ unsafe extern "system" fn settings_window_proc(
browse_btn, browse_btn,
None, None,
width - 120 - MARGIN, width - 120 - MARGIN,
row2_y, row3_y,
0, 0, 0, 0,
SWP_NOSIZE | SWP_NOZORDER, SWP_NOSIZE | SWP_NOZORDER,
).ok(); ).ok();
@@ -918,6 +1053,65 @@ unsafe extern "system" fn settings_window_proc(
} }
LRESULT(0) 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<u16> = format!("{}\0", path).encode_utf16().collect();
SetWindowTextW(xray_edit, PCWSTR::from_raw(path_wide.as_ptr())).ok();
}
// Show success message
let msg: Vec<u16> = "Xray binary downloaded successfully!\0".encode_utf16().collect();
let title: Vec<u16> = "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<u16> = format!("Failed to download Xray:\n{}\0", error_msg).encode_utf16().collect();
let title: Vec<u16> = "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<u16> = "Download\0".encode_utf16().collect();
SetWindowTextW(download_btn, PCWSTR::from_raw(download_text.as_ptr())).ok();
}
}
}
LRESULT(0)
}
WM_DESTROY => { WM_DESTROY => {
println!("Settings window destroyed"); println!("Settings window destroyed");
LRESULT(0) 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 let Ok(combo) = unsafe { GetDlgItem(container, ID_SERVER_PROXY_COMBO_BASE + idx as i32) } {
if !combo.is_invalid() { if !combo.is_invalid() {
unsafe { unsafe {
@@ -1208,7 +1402,7 @@ unsafe fn resize_server_list_items(container: HWND) {
right_controls_x + LABEL_WIDTH + 5 + PORT_EDIT_WIDTH + 10, right_controls_x + LABEL_WIDTH + 5 + PORT_EDIT_WIDTH + 10,
y_pos, y_pos,
COMBO_WIDTH, COMBO_WIDTH,
CONTROL_HEIGHT, 200, // Keep dropdown height
SWP_NOZORDER, SWP_NOZORDER,
).ok(); ).ok();
} }
@@ -1225,3 +1419,129 @@ unsafe extern "system" fn destroy_child_window(hwnd: HWND, _: LPARAM) -> BOOL {
unsafe { DestroyWindow(hwnd).ok() }; unsafe { DestroyWindow(hwnd).ok() };
true.into() true.into()
} }
/// Download latest Xray-core binary from GitHub releases
fn download_xray_latest() -> Result<String, String> {
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())
}