first commit

This commit is contained in:
Ethanfly 2026-01-04 18:15:07 +08:00
commit cc6054b3f7
74 changed files with 17395 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# Rust
/target/
**/*.rs.bk
Cargo.lock
# Node.js
node_modules/
dist/
.cache/
# Environment
.env
*.db
*.db-journal
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Build artifacts
*.exe
*.dll
*.so
*.dylib
# Tauri
/crates/client-tauri/src-tauri/target/
/crates/client-tauri/ui/dist/
# Logs
*.log
logs/

88
Cargo.toml Normal file
View File

@ -0,0 +1,88 @@
[workspace]
resolver = "2"
members = [
"crates/common",
"crates/server",
"crates/client-core",
"crates/client-tauri",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["EasyRemote Team"]
license = "MIT"
repository = "https://github.com/easyremote/easyremote"
[workspace.dependencies]
# Async runtime
tokio = { version = "1.35", features = ["full"] }
tokio-util = { version = "0.7", features = ["codec"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
bincode = "1.3"
# Web framework
axum = { version = "0.7", features = ["ws", "macros"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "fs", "trace"] }
reqwest = { version = "0.11", features = ["json"] }
# Database
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "postgres", "chrono", "uuid"] }
sea-orm = { version = "0.12", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-native-tls"] }
# Authentication
jsonwebtoken = "9.2"
argon2 = "0.5"
uuid = { version = "1.6", features = ["v4", "serde"] }
# Networking
quinn = "0.10"
webrtc = "0.9"
stun = "0.5"
turn = "0.6"
# Encryption
ring = "0.17"
rustls = "0.22"
rcgen = "0.12"
# Screen capture & encoding
scrap = "0.5"
x264 = "0.5"
vpx-encode = "1.0"
# Image processing
image = "0.24"
turbojpeg = "1.0"
# Input simulation
enigo = "0.2"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Error handling
anyhow = "1.0"
thiserror = "1.0"
# Utils
chrono = { version = "0.4", features = ["serde"] }
rand = "0.8"
base64 = "0.21"
bytes = "1.5"
futures = "0.3"
async-trait = "0.1"
# Config
config = "0.14"
dotenvy = "0.15"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1

277
README.md Normal file
View File

@ -0,0 +1,277 @@
# EasyRemote - 远程桌面控制软件
EasyRemote 是一个基于 Rust 开发的远程桌面控制软件,支持 P2P 直连和服务器中转两种连接方式,提供高性能的远程控制体验。
## ✨ 功能特性
### 客户端功能
- 🔐 **设备ID + 验证码** 连接方式,类似 TeamViewer/向日葵
- 👥 **账号登录** 支持,同步设备列表和控制历史
- 🖥️ **高性能屏幕捕获**,支持多显示器
- ⌨️ **完整输入控制**,包括鼠标、键盘、滚轮
- 📋 **剪贴板同步**
- 📊 **控制历史记录**
- ⚙️ **质量设置**,可调节帧率、分辨率、图像质量
### 服务端功能
- 🌐 **Web 管理后台**,管理用户、设备、会话
- 📡 **信令服务器**,支持 WebSocket
- 🔄 **P2P 穿透**,优先直连,备选中转
- 🛰️ **内置 STUN 服务器**,支持 NAT 穿透
- 👮 **管理员功能**:强制下线、结束会话
- 🖥️ **浏览器远程控制**(管理员)
### 技术特点
- 🦀 **纯 Rust 实现**,高性能、内存安全
- 🔒 **端到端加密**AES-256-GCM
- 🚀 **P2P 优先**,低延迟
- 📦 **跨平台**,支持 Windows、macOS、Linux
## 🏗️ 项目结构
```
easyremote/
├── Cargo.toml # Workspace 配置
├── crates/
│ ├── common/ # 共享库(协议、类型、加密)
│ ├── server/ # 服务端
│ │ └── static/ # Web 管理后台
│ ├── client-core/ # 客户端核心(屏幕捕获、输入控制)
│ └── client-tauri/ # Tauri 桌面客户端
│ └── ui/ # Vue3 前端
└── README.md
```
## 🚀 快速开始
### 环境要求
- Rust 1.70+
- Node.js 18+ (客户端前端)
- SQLite (服务端数据库)
### 构建服务端
```bash
# 编译服务端
cargo build --release -p easyremote-server
# 运行服务端
./target/release/easyremote-server
```
服务端默认运行在 `http://localhost:8080`,管理后台访问 `http://localhost:8080/`
### 构建客户端
```bash
# 进入客户端目录
cd crates/client-tauri
# 安装前端依赖
cd ui && npm install && cd ..
# 开发模式运行
cargo tauri dev
# 构建发布版本
cargo tauri build
```
## ⚙️ 配置
### 服务端配置
创建 `.env` 文件:
```env
# 服务器配置
HOST=0.0.0.0
PORT=8080
# STUN 服务器配置
STUN_PORT=3478 # STUN 服务端口UDP
ENABLE_LOCAL_STUN=true # 启用内置 STUN 服务
# PUBLIC_IP=your.public.ip # 公网 IP可选用于生成正确的 STUN URL
# 额外的 STUN 服务器(可选,会添加到本地 STUN 后面)
# STUN_SERVERS=stun:stun.l.google.com:19302
# 数据库配置
DATABASE_URL=sqlite:easyremote.db?mode=rwc
# JWT 配置
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_EXPIRY=86400
# TURN 服务器(可选,用于无法 P2P 直连时中转)
# TURN_SERVER=turn:your-turn-server:3478
# TURN_USERNAME=username
# TURN_PASSWORD=password
```
### STUN 服务说明
EasyRemote 服务端内置了 STUN (Session Traversal Utilities for NAT) 服务器,用于帮助客户端发现其公网 IP 地址,实现 P2P 穿透连接。
**配置说明:**
- `ENABLE_LOCAL_STUN=true`:启用本地 STUN 服务(默认开启)
- `STUN_PORT=3478`STUN 服务监听的 UDP 端口(默认 3478标准端口
- `PUBLIC_IP`:如果服务器有公网 IP设置此项可让客户端正确连接
**防火墙配置:**
如果使用防火墙,需要开放以下端口:
- TCP 8080HTTP/WebSocket 服务
- UDP 3478STUN 服务
**客户端获取 ICE 配置:**
客户端可以通过 API 获取 ICE 服务器配置:
```bash
curl http://localhost:8080/api/ice-servers
# 返回:{"stun_servers":["stun:localhost:3478"],"turn_server":null}
```
### 客户端配置
客户端配置存储在:
- Windows: `%APPDATA%/easyremote/config.json`
- macOS: `~/Library/Application Support/easyremote/config.json`
- Linux: `~/.config/easyremote/config.json`
```json
{
"server_url": "ws://localhost:8080",
"device_name": "My Computer",
"quality": {
"frame_rate": 30,
"resolution_scale": 1.0,
"image_quality": 80,
"hardware_acceleration": true
},
"auto_start": false,
"launch_on_boot": false
}
```
## 📖 使用说明
### 远程协助(被控制端)
1. 启动客户端
2. 开启「允许他人远程协助」开关
3. 将设备ID和验证码告知控制方
### 远程控制(控制端)
1. 启动客户端
2. 切换到「远控」标签
3. 输入对方的设备ID和验证码
4. 点击「连接」
### 账号登录
1. 注册/登录账号
2. 设备自动绑定到账号
3. 可在「远控」标签查看已登录的所有设备
4. 在「历史」标签查看控制记录
### 管理后台
1. 创建管理员账号(首次需要手动在数据库中设置 role='admin'
2. 访问 `http://服务器地址:8080/`
3. 使用管理员账号登录
4. 可管理用户、设备、会话,以及直接远程控制
## 🔧 开发
### 运行测试
```bash
cargo test
```
### 代码检查
```bash
cargo clippy
```
### 格式化代码
```bash
cargo fmt
```
## 📄 API 文档
### 认证接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/auth/register` | POST | 用户注册 |
| `/api/auth/login` | POST | 用户登录 |
| `/api/auth/logout` | POST | 退出登录 |
| `/api/auth/refresh` | POST | 刷新令牌 |
### 用户接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/users/me` | GET | 获取当前用户 |
| `/api/users/me` | POST | 更新用户信息 |
### 设备接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/devices` | GET | 获取设备列表 |
| `/api/devices/register` | POST | 注册设备 |
| `/api/devices/:id` | GET | 获取设备详情 |
| `/api/devices/:id` | DELETE | 移除设备 |
| `/api/devices/:id/offline` | POST | 强制下线 |
### ICE 配置接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/ice-servers` | GET | 获取 ICE 服务器配置STUN/TURN|
### 会话接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/sessions` | GET | 获取活跃会话 |
| `/api/sessions/history` | GET | 获取历史记录 |
| `/api/sessions/:id` | GET | 获取会话详情 |
| `/api/sessions/:id/end` | POST | 结束会话 |
### WebSocket 接口
| 接口 | 说明 |
|------|------|
| `/ws/signal?device_id=xxx` | 信令WebSocket |
| `/ws/remote/:device_id` | 浏览器远程控制WebSocket |
## 🛣️ 路线图
- [x] 基础架构
- [x] 用户认证
- [x] 设备管理
- [x] 信令服务
- [x] 客户端GUI
- [x] 管理后台
- [x] 内置 STUN 服务器
- [ ] P2P连接优化
- [ ] TURN 中转服务
- [ ] 文件传输
- [ ] 音频传输
- [ ] 多显示器选择
- [ ] 移动端适配
## 📜 许可证
MIT License
## 🤝 贡献
欢迎提交 Issue 和 Pull Request

View File

@ -0,0 +1,48 @@
[package]
name = "easyremote-client-core"
version.workspace = true
edition.workspace = true
[dependencies]
easyremote-common = { path = "../common" }
# Async
tokio = { workspace = true }
tokio-util = { workspace = true }
futures = { workspace = true }
async-trait = { workspace = true }
# Serialization
serde = { workspace = true }
serde_json = { workspace = true }
bincode = { workspace = true }
uuid = { workspace = true }
# Networking
quinn = { workspace = true }
rustls = { workspace = true }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
reqwest = { workspace = true }
# Screen capture
scrap = { workspace = true }
# Input
enigo = { workspace = true }
# Image
image = { workspace = true }
# Logging
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
# Utils
chrono = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
rand = { workspace = true }
base64 = { workspace = true }
bytes = { workspace = true }
dirs = "5.0"
hostname = "0.3"

View File

@ -0,0 +1,146 @@
//! 屏幕捕获模块
use anyhow::Result;
use scrap::{Capturer, Display};
use std::io::ErrorKind::WouldBlock;
use std::time::Duration;
/// 屏幕捕获器
pub struct ScreenCapturer {
capturer: Capturer,
width: u32,
height: u32,
}
impl ScreenCapturer {
/// 创建新的屏幕捕获器
pub fn new(display_index: usize) -> Result<Self> {
let displays = Display::all()?;
if display_index >= displays.len() {
anyhow::bail!("Invalid display index: {}", display_index);
}
let display = displays.into_iter().nth(display_index).unwrap();
let width = display.width() as u32;
let height = display.height() as u32;
let capturer = Capturer::new(display)?;
Ok(Self {
capturer,
width,
height,
})
}
/// 获取屏幕宽度
pub fn width(&self) -> u32 {
self.width
}
/// 获取屏幕高度
pub fn height(&self) -> u32 {
self.height
}
/// 捕获一帧
pub fn capture_frame(&mut self) -> Result<Option<Vec<u8>>> {
match self.capturer.frame() {
Ok(frame) => {
// scrap 返回的是 BGRA 格式
let data = frame.to_vec();
Ok(Some(data))
}
Err(ref e) if e.kind() == WouldBlock => {
// 没有新帧,稍后重试
Ok(None)
}
Err(e) => Err(e.into()),
}
}
/// 捕获并转换为 RGBA
pub fn capture_rgba(&mut self) -> Result<Option<Vec<u8>>> {
if let Some(bgra) = self.capture_frame()? {
let mut rgba = bgra;
// BGRA -> RGBA
for chunk in rgba.chunks_exact_mut(4) {
chunk.swap(0, 2);
}
Ok(Some(rgba))
} else {
Ok(None)
}
}
}
/// 获取所有显示器信息
pub fn get_displays() -> Result<Vec<DisplayInfo>> {
let displays = Display::all()?;
Ok(displays
.into_iter()
.enumerate()
.map(|(i, d)| DisplayInfo {
index: i,
width: d.width() as u32,
height: d.height() as u32,
is_primary: i == 0, // scrap 没有提供 is_primary默认第一个为主显示器
})
.collect())
}
/// 显示器信息
#[derive(Debug, Clone)]
pub struct DisplayInfo {
pub index: usize,
pub width: u32,
pub height: u32,
pub is_primary: bool,
}
/// 帧率控制器
pub struct FrameRateLimiter {
target_interval: Duration,
last_frame: std::time::Instant,
}
impl FrameRateLimiter {
pub fn new(fps: u32) -> Self {
Self {
target_interval: Duration::from_secs_f64(1.0 / fps as f64),
last_frame: std::time::Instant::now(),
}
}
/// 等待到下一帧时间
pub fn wait(&mut self) {
let elapsed = self.last_frame.elapsed();
if elapsed < self.target_interval {
std::thread::sleep(self.target_interval - elapsed);
}
self.last_frame = std::time::Instant::now();
}
/// 更新目标帧率
pub fn set_fps(&mut self, fps: u32) {
self.target_interval = Duration::from_secs_f64(1.0 / fps as f64);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_displays() {
let displays = get_displays().unwrap();
assert!(!displays.is_empty());
println!("Found {} displays", displays.len());
for d in &displays {
println!(
"Display {}: {}x{} (primary: {})",
d.index, d.width, d.height, d.is_primary
);
}
}
}

View File

@ -0,0 +1,220 @@
//! 编解码模块
use anyhow::Result;
use image::{ImageBuffer, Rgba, RgbaImage};
/// JPEG 编码器
pub struct JpegEncoder {
quality: u32,
}
impl JpegEncoder {
pub fn new(quality: u32) -> Self {
Self {
quality: quality.clamp(1, 100),
}
}
/// 编码 RGBA 数据为 JPEG
pub fn encode(&self, rgba: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
let img: RgbaImage = ImageBuffer::from_raw(width, height, rgba.to_vec())
.ok_or_else(|| anyhow::anyhow!("Failed to create image buffer"))?;
let mut jpeg_data = Vec::new();
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
&mut jpeg_data,
self.quality as u8,
);
encoder.encode(
img.as_raw(),
width,
height,
image::ColorType::Rgba8,
)?;
Ok(jpeg_data)
}
/// 设置质量
pub fn set_quality(&mut self, quality: u32) {
self.quality = quality.clamp(1, 100);
}
}
/// JPEG 解码器
pub struct JpegDecoder;
impl JpegDecoder {
/// 解码 JPEG 为 RGBA
pub fn decode(jpeg_data: &[u8]) -> Result<(Vec<u8>, u32, u32)> {
let img = image::load_from_memory_with_format(jpeg_data, image::ImageFormat::Jpeg)?;
let rgba = img.to_rgba8();
let (width, height) = rgba.dimensions();
Ok((rgba.into_raw(), width, height))
}
}
/// 图像缩放器
pub struct ImageScaler;
impl ImageScaler {
/// 缩放 RGBA 图像
pub fn scale(
rgba: &[u8],
src_width: u32,
src_height: u32,
scale: f32,
) -> Result<(Vec<u8>, u32, u32)> {
if (scale - 1.0).abs() < 0.01 {
return Ok((rgba.to_vec(), src_width, src_height));
}
let new_width = (src_width as f32 * scale) as u32;
let new_height = (src_height as f32 * scale) as u32;
let img: RgbaImage = ImageBuffer::from_raw(src_width, src_height, rgba.to_vec())
.ok_or_else(|| anyhow::anyhow!("Failed to create image buffer"))?;
let resized = image::imageops::resize(
&img,
new_width,
new_height,
image::imageops::FilterType::Triangle,
);
Ok((resized.into_raw(), new_width, new_height))
}
}
/// 帧差异计算
pub struct FrameDiffer {
last_frame: Option<Vec<u8>>,
width: u32,
height: u32,
}
impl FrameDiffer {
pub fn new(width: u32, height: u32) -> Self {
Self {
last_frame: None,
width,
height,
}
}
/// 计算与上一帧的差异区域
/// 返回差异区域的边界框 (x, y, width, height)
pub fn compute_diff(&mut self, current: &[u8]) -> Option<DiffRegion> {
let result = if let Some(ref last) = self.last_frame {
self.find_diff_region(last, current)
} else {
// 第一帧,返回整个区域
Some(DiffRegion {
x: 0,
y: 0,
width: self.width,
height: self.height,
is_full_frame: true,
})
};
self.last_frame = Some(current.to_vec());
result
}
fn find_diff_region(&self, last: &[u8], current: &[u8]) -> Option<DiffRegion> {
let mut min_x = self.width;
let mut min_y = self.height;
let mut max_x = 0u32;
let mut max_y = 0u32;
let mut has_diff = false;
// 每4个字节一组RGBA
for y in 0..self.height {
for x in 0..self.width {
let idx = ((y * self.width + x) * 4) as usize;
if idx + 3 < last.len() && idx + 3 < current.len() {
// 简单比较:如果任何通道差异超过阈值,认为有变化
let diff = (last[idx] as i32 - current[idx] as i32).abs()
+ (last[idx + 1] as i32 - current[idx + 1] as i32).abs()
+ (last[idx + 2] as i32 - current[idx + 2] as i32).abs();
if diff > 30 {
has_diff = true;
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
}
}
}
if has_diff {
// 添加一些边距
let margin = 8;
min_x = min_x.saturating_sub(margin);
min_y = min_y.saturating_sub(margin);
max_x = (max_x + margin).min(self.width - 1);
max_y = (max_y + margin).min(self.height - 1);
Some(DiffRegion {
x: min_x,
y: min_y,
width: max_x - min_x + 1,
height: max_y - min_y + 1,
is_full_frame: false,
})
} else {
None
}
}
/// 重置状态
pub fn reset(&mut self) {
self.last_frame = None;
}
}
/// 差异区域
#[derive(Debug, Clone)]
pub struct DiffRegion {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
pub is_full_frame: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_jpeg_encode_decode() {
let width = 100;
let height = 100;
let mut rgba = vec![0u8; (width * height * 4) as usize];
// 创建测试图像
for y in 0..height {
for x in 0..width {
let idx = ((y * width + x) * 4) as usize;
rgba[idx] = (x * 255 / width) as u8; // R
rgba[idx + 1] = (y * 255 / height) as u8; // G
rgba[idx + 2] = 128; // B
rgba[idx + 3] = 255; // A
}
}
let encoder = JpegEncoder::new(80);
let jpeg = encoder.encode(&rgba, width, height).unwrap();
assert!(!jpeg.is_empty());
let (decoded, w, h) = JpegDecoder::decode(&jpeg).unwrap();
assert_eq!(w, width);
assert_eq!(h, height);
assert_eq!(decoded.len(), rgba.len());
}
}

View File

@ -0,0 +1,93 @@
//! 客户端配置
use serde::{Deserialize, Serialize};
/// 客户端配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
/// 服务器地址
pub server_url: String,
/// 设备名称
pub device_name: String,
/// 质量设置
pub quality: QualityConfig,
/// 是否自动启动
pub auto_start: bool,
/// 是否开机启动
pub launch_on_boot: bool,
}
/// 质量配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityConfig {
/// 帧率
pub frame_rate: u32,
/// 分辨率缩放
pub resolution_scale: f32,
/// 图像质量 (1-100)
pub image_quality: u32,
/// 是否启用硬件加速
pub hardware_acceleration: bool,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
server_url: "ws://localhost:8080".to_string(),
device_name: get_default_device_name(),
quality: QualityConfig::default(),
auto_start: false,
launch_on_boot: false,
}
}
}
impl Default for QualityConfig {
fn default() -> Self {
Self {
frame_rate: 30,
resolution_scale: 1.0,
image_quality: 80,
hardware_acceleration: true,
}
}
}
fn get_default_device_name() -> String {
hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "My Device".to_string())
}
impl ClientConfig {
/// 从文件加载配置
pub fn load() -> Self {
let config_path = Self::config_path();
if config_path.exists() {
if let Ok(content) = std::fs::read_to_string(&config_path) {
if let Ok(config) = serde_json::from_str(&content) {
return config;
}
}
}
Self::default()
}
/// 保存配置到文件
pub fn save(&self) -> anyhow::Result<()> {
let config_path = Self::config_path();
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = serde_json::to_string_pretty(self)?;
std::fs::write(config_path, content)?;
Ok(())
}
fn config_path() -> std::path::PathBuf {
dirs::config_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("easyremote")
.join("config.json")
}
}

View File

@ -0,0 +1,456 @@
//! P2P 连接模块
use anyhow::Result;
use bytes::Bytes;
use easyremote_common::protocol::{FrameData, FrameFormat, InputEvent};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::UdpSocket;
use tokio::sync::{mpsc, RwLock};
/// 连接状态
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionState {
/// 断开连接
Disconnected,
/// 正在连接
Connecting,
/// 已连接
Connected,
/// 连接失败
Failed(String),
}
/// P2P 连接管理器
pub struct P2PConnection {
state: Arc<RwLock<ConnectionState>>,
local_addr: Option<SocketAddr>,
remote_addr: Option<SocketAddr>,
socket: Option<Arc<UdpSocket>>,
frame_tx: Option<mpsc::Sender<FrameData>>,
input_tx: Option<mpsc::Sender<InputEvent>>,
}
impl P2PConnection {
pub fn new() -> Self {
Self {
state: Arc::new(RwLock::new(ConnectionState::Disconnected)),
local_addr: None,
remote_addr: None,
socket: None,
frame_tx: None,
input_tx: None,
}
}
/// 获取连接状态
pub async fn state(&self) -> ConnectionState {
self.state.read().await.clone()
}
/// 初始化本地套接字
pub async fn init_socket(&mut self) -> Result<SocketAddr> {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
let local_addr = socket.local_addr()?;
self.local_addr = Some(local_addr);
self.socket = Some(Arc::new(socket));
Ok(local_addr)
}
/// 尝试连接到远程地址
pub async fn connect(&mut self, remote_addr: SocketAddr) -> Result<()> {
*self.state.write().await = ConnectionState::Connecting;
if let Some(socket) = &self.socket {
socket.connect(remote_addr).await?;
self.remote_addr = Some(remote_addr);
*self.state.write().await = ConnectionState::Connected;
} else {
*self.state.write().await = ConnectionState::Failed("Socket not initialized".into());
anyhow::bail!("Socket not initialized");
}
Ok(())
}
/// 发送帧数据
pub async fn send_frame(&self, frame: &FrameData) -> Result<()> {
if let Some(socket) = &self.socket {
let data = bincode::serialize(frame)?;
// 如果数据太大,需要分片发送
if data.len() > 65000 {
self.send_fragmented(socket, &data).await?;
} else {
socket.send(&data).await?;
}
}
Ok(())
}
/// 发送输入事件
pub async fn send_input(&self, event: &InputEvent) -> Result<()> {
if let Some(socket) = &self.socket {
let data = bincode::serialize(event)?;
socket.send(&data).await?;
}
Ok(())
}
/// 分片发送大数据
async fn send_fragmented(&self, socket: &UdpSocket, data: &[u8]) -> Result<()> {
const MAX_FRAGMENT_SIZE: usize = 60000;
let total_fragments = (data.len() + MAX_FRAGMENT_SIZE - 1) / MAX_FRAGMENT_SIZE;
for (i, chunk) in data.chunks(MAX_FRAGMENT_SIZE).enumerate() {
let fragment = FragmentHeader {
fragment_id: i as u16,
total_fragments: total_fragments as u16,
data: chunk.to_vec(),
};
let fragment_data = bincode::serialize(&fragment)?;
socket.send(&fragment_data).await?;
}
Ok(())
}
/// 开始接收数据
pub async fn start_receiving(
&self,
on_frame: impl Fn(FrameData) + Send + 'static,
on_input: impl Fn(InputEvent) + Send + 'static,
) -> Result<()> {
let socket = self.socket.clone().ok_or_else(|| anyhow::anyhow!("Socket not initialized"))?;
let state = self.state.clone();
tokio::spawn(async move {
let mut buf = vec![0u8; 100000];
let mut fragment_buffer: Vec<Option<Vec<u8>>> = Vec::new();
let mut expected_fragments = 0u16;
loop {
if *state.read().await != ConnectionState::Connected {
break;
}
match socket.recv(&mut buf).await {
Ok(len) => {
let data = &buf[..len];
// 尝试解析为分片
if let Ok(fragment) = bincode::deserialize::<FragmentHeader>(data) {
if fragment.total_fragments > 1 {
// 处理分片
if fragment_buffer.is_empty() {
fragment_buffer = vec![None; fragment.total_fragments as usize];
expected_fragments = fragment.total_fragments;
}
if fragment.fragment_id < expected_fragments {
fragment_buffer[fragment.fragment_id as usize] =
Some(fragment.data);
}
// 检查是否收到所有分片
if fragment_buffer.iter().all(|f| f.is_some()) {
let complete_data: Vec<u8> = fragment_buffer
.iter()
.filter_map(|f| f.clone())
.flatten()
.collect();
if let Ok(frame) = bincode::deserialize::<FrameData>(&complete_data) {
on_frame(frame);
}
fragment_buffer.clear();
}
continue;
}
}
// 尝试解析为帧数据
if let Ok(frame) = bincode::deserialize::<FrameData>(data) {
on_frame(frame);
continue;
}
// 尝试解析为输入事件
if let Ok(input) = bincode::deserialize::<InputEvent>(data) {
on_input(input);
}
}
Err(e) => {
tracing::warn!("Receive error: {}", e);
}
}
}
});
Ok(())
}
/// 断开连接
pub async fn disconnect(&mut self) {
*self.state.write().await = ConnectionState::Disconnected;
self.socket = None;
self.remote_addr = None;
}
}
/// 分片头
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct FragmentHeader {
fragment_id: u16,
total_fragments: u16,
data: Vec<u8>,
}
/// ICE 服务器配置
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct IceServersConfig {
pub stun_servers: Vec<String>,
pub turn_server: Option<TurnConfig>,
}
/// TURN 服务器配置
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TurnConfig {
pub url: String,
pub username: String,
pub credential: String,
}
/// NAT穿透辅助
pub struct NatTraversal;
impl NatTraversal {
/// 从服务器获取 ICE 服务器配置
pub async fn fetch_ice_servers(server_url: &str) -> Result<IceServersConfig> {
// 将 ws:// 或 wss:// 转换为 http:// 或 https://
let http_url = server_url
.replace("ws://", "http://")
.replace("wss://", "https://");
let url = format!("{}/api/ice-servers", http_url.trim_end_matches('/'));
let response = reqwest::get(&url).await?;
let config: IceServersConfig = response.json().await?;
Ok(config)
}
/// 获取本地候选地址
pub async fn get_local_candidates() -> Result<Vec<String>> {
let mut candidates = Vec::new();
// 获取所有本地网络接口
if let Ok(socket) = UdpSocket::bind("0.0.0.0:0").await {
if let Ok(addr) = socket.local_addr() {
candidates.push(format!("host {}", addr));
}
}
Ok(candidates)
}
/// 获取完整的 ICE 候选(包括本地和公网地址)
pub async fn get_all_candidates(stun_servers: &[String]) -> Result<Vec<IceCandidate>> {
let mut candidates = Vec::new();
// 添加本地地址候选
let local_candidates = Self::get_local_candidates().await?;
for candidate in local_candidates {
candidates.push(IceCandidate::new(candidate));
}
// 使用 STUN 服务器获取公网地址
for stun_url in stun_servers {
match Self::get_public_addr_from_url(stun_url).await {
Ok(addr) => {
candidates.push(IceCandidate::new(format!("srflx {}", addr)));
tracing::info!("Got public address from STUN {}: {}", stun_url, addr);
break; // 成功获取一个公网地址即可
}
Err(e) => {
tracing::warn!("Failed to get public address from {}: {}", stun_url, e);
}
}
}
Ok(candidates)
}
/// 从 STUN URL 解析并获取公网地址
pub async fn get_public_addr_from_url(stun_url: &str) -> Result<SocketAddr> {
// 解析 STUN URL格式: stun:host:port 或 stun:host
let url = stun_url.trim_start_matches("stun:");
let parts: Vec<&str> = url.split(':').collect();
let (host, port) = match parts.len() {
1 => (parts[0], 3478u16), // 默认 STUN 端口
2 => (parts[0], parts[1].parse().unwrap_or(3478)),
_ => anyhow::bail!("Invalid STUN URL format: {}", stun_url),
};
Self::get_public_addr(host, port).await
}
/// 使用 STUN 服务器获取公网地址
pub async fn get_public_addr(stun_host: &str, stun_port: u16) -> Result<SocketAddr> {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
// 解析 STUN 服务器地址
let stun_addr = tokio::net::lookup_host(format!("{}:{}", stun_host, stun_port))
.await?
.next()
.ok_or_else(|| anyhow::anyhow!("Cannot resolve STUN server: {}", stun_host))?;
// 设置超时
socket.connect(stun_addr).await?;
// 发送 STUN Binding 请求
let binding_request = create_stun_binding_request();
socket.send(&binding_request).await?;
// 接收响应(带超时)
let mut buf = [0u8; 1024];
let recv_result = tokio::time::timeout(
std::time::Duration::from_secs(5),
socket.recv(&mut buf)
).await;
match recv_result {
Ok(Ok(len)) => parse_stun_response(&buf[..len]),
Ok(Err(e)) => anyhow::bail!("STUN receive error: {}", e),
Err(_) => anyhow::bail!("STUN request timeout"),
}
}
}
/// STUN Magic Cookie (RFC 5389)
const STUN_MAGIC_COOKIE: u32 = 0x2112A442;
/// STUN Attribute Types
const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020;
fn create_stun_binding_request() -> Vec<u8> {
use rand::Rng;
let mut request = Vec::with_capacity(20);
// Message Type: Binding Request (0x0001)
request.extend_from_slice(&0x0001u16.to_be_bytes());
// Message Length: 0 (no attributes)
request.extend_from_slice(&0x0000u16.to_be_bytes());
// Magic Cookie (RFC 5389)
request.extend_from_slice(&STUN_MAGIC_COOKIE.to_be_bytes());
// Transaction ID (12 bytes random)
let mut rng = rand::thread_rng();
let transaction_id: [u8; 12] = rng.gen();
request.extend_from_slice(&transaction_id);
request
}
fn parse_stun_response(data: &[u8]) -> Result<SocketAddr> {
// 验证最小长度 (20 bytes header)
if data.len() < 20 {
anyhow::bail!("Invalid STUN response: too short");
}
// 验证 Magic Cookie
let magic = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
if magic != STUN_MAGIC_COOKIE {
anyhow::bail!("Invalid STUN magic cookie");
}
// 解析消息长度
let msg_len = u16::from_be_bytes([data[2], data[3]]) as usize;
if data.len() < 20 + msg_len {
anyhow::bail!("Invalid STUN response: truncated");
}
// 遍历属性查找 XOR-MAPPED-ADDRESS
let mut offset = 20;
while offset + 4 <= 20 + msg_len {
let attr_type = u16::from_be_bytes([data[offset], data[offset + 1]]);
let attr_len = u16::from_be_bytes([data[offset + 2], data[offset + 3]]) as usize;
if attr_type == ATTR_XOR_MAPPED_ADDRESS {
// XOR-MAPPED-ADDRESS 格式:
// 1 byte: reserved
// 1 byte: family (0x01=IPv4, 0x02=IPv6)
// 2 bytes: XOR'd port
// 4 bytes (IPv4) or 16 bytes (IPv6): XOR'd address
if offset + 4 + attr_len > data.len() {
anyhow::bail!("Invalid XOR-MAPPED-ADDRESS attribute");
}
let family = data[offset + 5];
// XOR'd port
let xor_port = u16::from_be_bytes([data[offset + 6], data[offset + 7]]);
let port = xor_port ^ ((STUN_MAGIC_COOKIE >> 16) as u16);
match family {
0x01 => {
// IPv4
let magic_bytes = STUN_MAGIC_COOKIE.to_be_bytes();
let ip = std::net::Ipv4Addr::new(
data[offset + 8] ^ magic_bytes[0],
data[offset + 9] ^ magic_bytes[1],
data[offset + 10] ^ magic_bytes[2],
data[offset + 11] ^ magic_bytes[3],
);
return Ok(SocketAddr::new(std::net::IpAddr::V4(ip), port));
}
0x02 => {
// IPv6 (需要 Transaction ID 进行 XOR)
let magic_bytes = STUN_MAGIC_COOKIE.to_be_bytes();
let transaction_id = &data[8..20];
let mut ip_bytes = [0u8; 16];
// 前 4 bytes 与 magic cookie XOR
for i in 0..4 {
ip_bytes[i] = data[offset + 8 + i] ^ magic_bytes[i];
}
// 后 12 bytes 与 transaction ID XOR
for i in 4..16 {
ip_bytes[i] = data[offset + 8 + i] ^ transaction_id[i - 4];
}
let ip = std::net::Ipv6Addr::from(ip_bytes);
return Ok(SocketAddr::new(std::net::IpAddr::V6(ip), port));
}
_ => {
anyhow::bail!("Unknown address family: {}", family);
}
}
}
// 移动到下一个属性 (4字节对齐)
offset += 4 + attr_len + (4 - (attr_len % 4)) % 4;
}
anyhow::bail!("XOR-MAPPED-ADDRESS not found in STUN response")
}
/// ICE 候选
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct IceCandidate {
pub candidate: String,
pub sdp_mid: Option<String>,
pub sdp_mline_index: Option<u32>,
}
impl IceCandidate {
pub fn new(candidate: String) -> Self {
Self {
candidate,
sdp_mid: None,
sdp_mline_index: Some(0),
}
}
}

View File

@ -0,0 +1,284 @@
//! 输入控制模块
use anyhow::Result;
use enigo::{Enigo, Key, Keyboard, Mouse, Direction, Settings, Coordinate};
use easyremote_common::protocol::{InputEvent, MouseButton as ProtoMouseButton, Modifiers};
/// 鼠标按钮
#[derive(Debug, Clone, Copy)]
pub enum MouseButton {
Left,
Right,
Middle,
}
/// 输入控制器
pub struct InputController {
enigo: Enigo,
screen_width: i32,
screen_height: i32,
}
impl InputController {
/// 创建新的输入控制器(带屏幕尺寸)
pub fn with_screen_size(screen_width: u32, screen_height: u32) -> Self {
Self {
enigo: Enigo::new(&Settings::default()).unwrap(),
screen_width: screen_width as i32,
screen_height: screen_height as i32,
}
}
/// 创建新的输入控制器(默认屏幕尺寸)
pub fn new() -> Result<Self> {
Ok(Self {
enigo: Enigo::new(&Settings::default())?,
screen_width: 1920,
screen_height: 1080,
})
}
/// 移动鼠标
pub fn move_mouse(&mut self, x: i32, y: i32) -> Result<()> {
self.enigo.move_mouse(x, y, Coordinate::Abs)?;
Ok(())
}
/// 点击鼠标
pub fn click(&mut self, button: MouseButton) -> Result<()> {
self.enigo.button(convert_simple_mouse_button(button), Direction::Click)?;
Ok(())
}
/// 鼠标按下
pub fn mouse_down(&mut self, button: MouseButton) -> Result<()> {
self.enigo.button(convert_simple_mouse_button(button), Direction::Press)?;
Ok(())
}
/// 鼠标释放
pub fn mouse_up(&mut self, button: MouseButton) -> Result<()> {
self.enigo.button(convert_simple_mouse_button(button), Direction::Release)?;
Ok(())
}
/// 滚动
pub fn scroll(&mut self, delta_x: i32, delta_y: i32) -> Result<()> {
if delta_y != 0 {
self.enigo.scroll(delta_y, enigo::Axis::Vertical)?;
}
if delta_x != 0 {
self.enigo.scroll(delta_x, enigo::Axis::Horizontal)?;
}
Ok(())
}
/// 按键按下
pub fn key_down(&mut self, key: &str) -> Result<()> {
if let Some(k) = parse_key(key) {
self.enigo.key(k, Direction::Press)?;
}
Ok(())
}
/// 按键释放
pub fn key_up(&mut self, key: &str) -> Result<()> {
if let Some(k) = parse_key(key) {
self.enigo.key(k, Direction::Release)?;
}
Ok(())
}
/// 处理输入事件
pub fn handle_event(&mut self, event: InputEvent) -> Result<()> {
match event {
InputEvent::MouseMove { x, y } => {
let _ = self.enigo.move_mouse(x, y, Coordinate::Abs);
}
InputEvent::MouseDown { button, x, y } => {
let _ = self.enigo.move_mouse(x, y, Coordinate::Abs);
let _ = self.enigo.button(convert_mouse_button(button), Direction::Press);
}
InputEvent::MouseUp { button, x, y } => {
let _ = self.enigo.move_mouse(x, y, Coordinate::Abs);
let _ = self.enigo.button(convert_mouse_button(button), Direction::Release);
}
InputEvent::MouseScroll { delta_x, delta_y } => {
if delta_y != 0 {
let _ = self.enigo.scroll(delta_y, enigo::Axis::Vertical);
}
if delta_x != 0 {
let _ = self.enigo.scroll(delta_x, enigo::Axis::Horizontal);
}
}
InputEvent::KeyDown { key, modifiers } => {
self.handle_modifiers_down(&modifiers);
if let Some(k) = parse_key(&key) {
let _ = self.enigo.key(k, Direction::Press);
}
}
InputEvent::KeyUp { key, modifiers } => {
if let Some(k) = parse_key(&key) {
let _ = self.enigo.key(k, Direction::Release);
}
self.handle_modifiers_up(&modifiers);
}
InputEvent::TextInput { text } => {
let _ = self.enigo.text(&text);
}
}
Ok(())
}
fn handle_modifiers_down(&mut self, modifiers: &Modifiers) {
if modifiers.ctrl {
let _ = self.enigo.key(Key::Control, Direction::Press);
}
if modifiers.alt {
let _ = self.enigo.key(Key::Alt, Direction::Press);
}
if modifiers.shift {
let _ = self.enigo.key(Key::Shift, Direction::Press);
}
if modifiers.meta {
let _ = self.enigo.key(Key::Meta, Direction::Press);
}
}
fn handle_modifiers_up(&mut self, modifiers: &Modifiers) {
if modifiers.meta {
let _ = self.enigo.key(Key::Meta, Direction::Release);
}
if modifiers.shift {
let _ = self.enigo.key(Key::Shift, Direction::Release);
}
if modifiers.alt {
let _ = self.enigo.key(Key::Alt, Direction::Release);
}
if modifiers.ctrl {
let _ = self.enigo.key(Key::Control, Direction::Release);
}
}
/// 更新屏幕尺寸
pub fn set_screen_size(&mut self, width: u32, height: u32) {
self.screen_width = width as i32;
self.screen_height = height as i32;
}
}
fn convert_mouse_button(button: ProtoMouseButton) -> enigo::Button {
match button {
ProtoMouseButton::Left => enigo::Button::Left,
ProtoMouseButton::Right => enigo::Button::Right,
ProtoMouseButton::Middle => enigo::Button::Middle,
ProtoMouseButton::Back => enigo::Button::Back,
ProtoMouseButton::Forward => enigo::Button::Forward,
}
}
fn convert_simple_mouse_button(button: MouseButton) -> enigo::Button {
match button {
MouseButton::Left => enigo::Button::Left,
MouseButton::Right => enigo::Button::Right,
MouseButton::Middle => enigo::Button::Middle,
}
}
fn parse_key(key: &str) -> Option<Key> {
// 处理常见的键名
match key.to_lowercase().as_str() {
// 功能键
"escape" | "esc" => Some(Key::Escape),
"f1" => Some(Key::F1),
"f2" => Some(Key::F2),
"f3" => Some(Key::F3),
"f4" => Some(Key::F4),
"f5" => Some(Key::F5),
"f6" => Some(Key::F6),
"f7" => Some(Key::F7),
"f8" => Some(Key::F8),
"f9" => Some(Key::F9),
"f10" => Some(Key::F10),
"f11" => Some(Key::F11),
"f12" => Some(Key::F12),
// 控制键
"tab" => Some(Key::Tab),
"capslock" => Some(Key::CapsLock),
"shift" => Some(Key::Shift),
"control" | "ctrl" => Some(Key::Control),
"alt" => Some(Key::Alt),
"meta" | "win" | "cmd" | "command" => Some(Key::Meta),
"space" | " " => Some(Key::Space),
"enter" | "return" => Some(Key::Return),
"backspace" => Some(Key::Backspace),
"delete" | "del" => Some(Key::Delete),
"home" => Some(Key::Home),
"end" => Some(Key::End),
"pageup" | "pgup" => Some(Key::PageUp),
"pagedown" | "pgdn" => Some(Key::PageDown),
// 方向键
"arrowup" | "up" => Some(Key::UpArrow),
"arrowdown" | "down" => Some(Key::DownArrow),
"arrowleft" | "left" => Some(Key::LeftArrow),
"arrowright" | "right" => Some(Key::RightArrow),
// 单字符 - 使用 Unicode
s if s.len() == 1 => {
let c = s.chars().next().unwrap();
Some(Key::Unicode(c))
}
_ => None,
}
}
/// 生成输入事件
pub struct InputEventGenerator;
impl InputEventGenerator {
/// 生成鼠标移动事件
pub fn mouse_move(x: i32, y: i32) -> InputEvent {
InputEvent::MouseMove { x, y }
}
/// 生成鼠标按下事件
pub fn mouse_down(button: ProtoMouseButton, x: i32, y: i32) -> InputEvent {
InputEvent::MouseDown { button, x, y }
}
/// 生成鼠标释放事件
pub fn mouse_up(button: ProtoMouseButton, x: i32, y: i32) -> InputEvent {
InputEvent::MouseUp { button, x, y }
}
/// 生成鼠标点击事件(按下+释放)
pub fn mouse_click(button: ProtoMouseButton, x: i32, y: i32) -> Vec<InputEvent> {
vec![
InputEvent::MouseDown { button, x, y },
InputEvent::MouseUp { button, x, y },
]
}
/// 生成鼠标滚轮事件
pub fn mouse_scroll(delta_x: i32, delta_y: i32) -> InputEvent {
InputEvent::MouseScroll { delta_x, delta_y }
}
/// 生成键盘按下事件
pub fn key_down(key: String, modifiers: Modifiers) -> InputEvent {
InputEvent::KeyDown { key, modifiers }
}
/// 生成键盘释放事件
pub fn key_up(key: String, modifiers: Modifiers) -> InputEvent {
InputEvent::KeyUp { key, modifiers }
}
/// 生成文本输入事件
pub fn text_input(text: String) -> InputEvent {
InputEvent::TextInput { text }
}
}

View File

@ -0,0 +1,13 @@
//! EasyRemote Client Core Library
pub mod capture;
pub mod input;
pub mod connection;
pub mod signal;
pub mod codec;
pub mod config;
pub use config::ClientConfig;
pub use signal::{SignalClient, SignalMessage};
pub use capture::ScreenCapturer;
pub use input::{InputController, MouseButton};

View File

@ -0,0 +1,338 @@
//! 信令客户端
use anyhow::Result;
use futures::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio::sync::{mpsc, RwLock};
use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
/// 信令消息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SignalMessage {
/// 注册设备
#[serde(rename = "register")]
Register {
device_id: String,
verification_code: String,
},
/// 心跳
#[serde(rename = "heartbeat")]
Heartbeat { device_id: String },
/// 心跳响应
#[serde(rename = "heartbeat_ack")]
HeartbeatAck,
/// 连接请求
#[serde(rename = "connect_request")]
ConnectRequest {
session_id: String,
from_device: String,
to_device: String,
verification_code: String,
},
/// 连接响应
#[serde(rename = "connect_response")]
ConnectResponse {
session_id: String,
accepted: bool,
reason: Option<String>,
},
/// SDP Offer
#[serde(rename = "offer")]
Offer {
session_id: String,
from_device: String,
to_device: String,
sdp: String,
},
/// SDP Answer
#[serde(rename = "answer")]
Answer {
session_id: String,
from_device: String,
to_device: String,
sdp: String,
},
/// ICE Candidate
#[serde(rename = "candidate")]
Candidate {
session_id: String,
from_device: String,
to_device: String,
candidate: String,
sdp_mid: Option<String>,
sdp_mline_index: Option<u32>,
},
/// 会话结束
#[serde(rename = "session_end")]
SessionEnd { session_id: String },
/// 强制下线
#[serde(rename = "force_offline")]
ForceOffline { device_id: String },
/// 错误
#[serde(rename = "error")]
Error { code: u32, message: String },
/// 设置允许远程
#[serde(rename = "set_allow_remote")]
SetAllowRemote { device_id: String, allow: bool },
/// 刷新验证码
#[serde(rename = "refresh_code")]
RefreshCode { device_id: String },
/// 验证码已刷新
#[serde(rename = "code_refreshed")]
CodeRefreshed {
device_id: String,
verification_code: String,
},
/// 屏幕帧数据
#[serde(rename = "screen_frame")]
ScreenFrame {
session_id: String,
from_device: String,
to_device: String,
width: u32,
height: u32,
data: String, // Base64 encoded JPEG
},
/// 鼠标事件
#[serde(rename = "mouse_event")]
MouseEvent {
session_id: String,
from_device: String,
to_device: String,
x: f64,
y: f64,
event_type: String, // "move", "click", "down", "up", "scroll"
button: Option<u8>,
delta: Option<f64>,
},
/// 键盘事件
#[serde(rename = "keyboard_event")]
KeyboardEvent {
session_id: String,
from_device: String,
to_device: String,
key: String,
event_type: String, // "down", "up"
},
}
/// 信令客户端
pub struct SignalClient {
device_id: String,
server_url: String,
tx: Option<mpsc::Sender<SignalMessage>>,
connected: Arc<RwLock<bool>>,
}
impl SignalClient {
pub fn new(device_id: String, server_url: String) -> Self {
Self {
device_id,
server_url,
tx: None,
connected: Arc::new(RwLock::new(false)),
}
}
/// 连接到信令服务器
pub async fn connect(
&mut self,
on_message: impl Fn(SignalMessage) + Send + 'static,
) -> Result<()> {
// 将 http:// 转换为 ws://https:// 转换为 wss://
let ws_url = self.server_url
.replace("http://", "ws://")
.replace("https://", "wss://");
let url = format!(
"{}/ws/signal?device_id={}",
ws_url, self.device_id
);
tracing::info!("Connecting to WebSocket: {}", url);
let (ws_stream, _) = connect_async(&url).await?;
let (mut write, mut read) = ws_stream.split();
let (tx, mut rx) = mpsc::channel::<SignalMessage>(32);
self.tx = Some(tx);
*self.connected.write().await = true;
let connected = self.connected.clone();
// 发送任务
tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
let text = serde_json::to_string(&msg).unwrap();
if write.send(Message::Text(text)).await.is_err() {
break;
}
}
});
// 接收任务
tokio::spawn(async move {
while let Some(Ok(msg)) = read.next().await {
if let Message::Text(text) = msg {
if let Ok(signal_msg) = serde_json::from_str::<SignalMessage>(&text) {
on_message(signal_msg);
}
}
}
*connected.write().await = false;
});
Ok(())
}
/// 发送消息
pub async fn send(&self, msg: SignalMessage) -> Result<()> {
if let Some(tx) = &self.tx {
tx.send(msg).await?;
}
Ok(())
}
/// 发送心跳
pub async fn send_heartbeat(&self) -> Result<()> {
self.send(SignalMessage::Heartbeat {
device_id: self.device_id.clone(),
})
.await
}
/// 发送连接请求
pub async fn request_connect(
&self,
session_id: String,
to_device: String,
verification_code: String,
) -> Result<()> {
self.send(SignalMessage::ConnectRequest {
session_id,
from_device: self.device_id.clone(),
to_device,
verification_code,
})
.await
}
/// 发送连接响应
pub async fn respond_connect(
&self,
session_id: String,
accepted: bool,
reason: Option<String>,
) -> Result<()> {
self.send(SignalMessage::ConnectResponse {
session_id,
accepted,
reason,
})
.await
}
/// 发送 SDP Offer
pub async fn send_offer(
&self,
session_id: String,
to_device: String,
sdp: String,
) -> Result<()> {
self.send(SignalMessage::Offer {
session_id,
from_device: self.device_id.clone(),
to_device,
sdp,
})
.await
}
/// 发送 SDP Answer
pub async fn send_answer(
&self,
session_id: String,
to_device: String,
sdp: String,
) -> Result<()> {
self.send(SignalMessage::Answer {
session_id,
from_device: self.device_id.clone(),
to_device,
sdp,
})
.await
}
/// 发送 ICE Candidate
pub async fn send_candidate(
&self,
session_id: String,
to_device: String,
candidate: String,
sdp_mid: Option<String>,
sdp_mline_index: Option<u32>,
) -> Result<()> {
self.send(SignalMessage::Candidate {
session_id,
from_device: self.device_id.clone(),
to_device,
candidate,
sdp_mid,
sdp_mline_index,
})
.await
}
/// 设置允许远程
pub async fn set_allow_remote(&self, allow: bool) -> Result<()> {
self.send(SignalMessage::SetAllowRemote {
device_id: self.device_id.clone(),
allow,
})
.await
}
/// 刷新验证码
pub async fn refresh_code(&self) -> Result<()> {
self.send(SignalMessage::RefreshCode {
device_id: self.device_id.clone(),
})
.await
}
/// 结束会话
pub async fn end_session(&self, session_id: String) -> Result<()> {
self.send(SignalMessage::SessionEnd { session_id }).await
}
/// 发送屏幕帧
pub async fn send_frame(
&self,
session_id: String,
to_device: String,
width: u32,
height: u32,
jpeg_data: &[u8],
) -> Result<()> {
let data = BASE64.encode(jpeg_data);
self.send(SignalMessage::ScreenFrame {
session_id,
from_device: self.device_id.clone(),
to_device,
width,
height,
data,
})
.await
}
/// 是否已连接
pub async fn is_connected(&self) -> bool {
*self.connected.read().await
}
}

View File

@ -0,0 +1,50 @@
[package]
name = "easyremote-client"
version.workspace = true
edition.workspace = true
[build-dependencies]
tauri-build = { version = "1.5", features = [] }
[dependencies]
easyremote-common = { path = "../common" }
easyremote-client-core = { path = "../client-core" }
# Tauri
tauri = { version = "1.5", features = ["shell-open", "dialog-all", "clipboard-all", "window-all"] }
# Windows single instance
[target.'cfg(windows)'.dependencies]
windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_System_Threading",
"Win32_Security",
"Win32_UI_WindowsAndMessaging"
] }
# Async
tokio = { workspace = true }
futures = { workspace = true }
# HTTP Client
reqwest = { version = "0.11", features = ["json"] }
# Serialization
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }
# Logging
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
# Utils
chrono = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
dirs = "5.0"
image = { workspace = true }
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,760 @@
//! Tauri 命令
use crate::state::{AppState, ConnectionState, CurrentUser, DeviceInfo, HistoryItem, ConnectRequest};
use easyremote_client_core::{ClientConfig, SignalClient, SignalMessage, ScreenCapturer};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::State;
use tokio::sync::RwLock;
type AppStateHandle = Arc<RwLock<AppState>>;
/// 全局信令客户端
static SIGNAL_CLIENT: tokio::sync::OnceCell<Arc<RwLock<Option<SignalClient>>>> = tokio::sync::OnceCell::const_new();
/// 强制下线标志
static FORCE_OFFLINE_FLAG: AtomicBool = AtomicBool::new(false);
/// 当前活跃的屏幕流会话
static ACTIVE_SESSION: tokio::sync::OnceCell<Arc<RwLock<Option<ActiveScreenSession>>>> = tokio::sync::OnceCell::const_new();
/// 活跃屏幕会话
struct ActiveScreenSession {
session_id: String,
controller_device: String,
stop_flag: Arc<AtomicBool>,
}
async fn get_active_session() -> &'static Arc<RwLock<Option<ActiveScreenSession>>> {
ACTIVE_SESSION.get_or_init(|| async {
Arc::new(RwLock::new(None))
}).await
}
/// 获取信令客户端
async fn get_signal_client() -> &'static Arc<RwLock<Option<SignalClient>>> {
SIGNAL_CLIENT.get_or_init(|| async {
Arc::new(RwLock::new(None))
}).await
}
/// 检查是否需要强制下线
pub fn check_force_offline() -> bool {
FORCE_OFFLINE_FLAG.swap(false, Ordering::SeqCst)
}
/// 设置强制下线标志并清除登录信息
fn set_force_offline() {
FORCE_OFFLINE_FLAG.store(true, Ordering::SeqCst);
// 清除本地保存的登录信息
let auth_path = dirs::data_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("easyremote")
.join("auth.json");
let _ = std::fs::remove_file(&auth_path);
tracing::info!("已清除登录信息");
}
/// 启动屏幕流
async fn start_screen_streaming(session_id: String, controller_device: String, my_device_id: String) {
tracing::info!("启动屏幕流: session={}, controller={}", session_id, controller_device);
// 检查是否已有活跃会话
let active_session = get_active_session().await;
{
let mut session = active_session.write().await;
if session.is_some() {
tracing::warn!("已有活跃屏幕流会话,停止旧会话");
if let Some(s) = session.as_ref() {
s.stop_flag.store(true, Ordering::SeqCst);
}
}
let stop_flag = Arc::new(AtomicBool::new(false));
*session = Some(ActiveScreenSession {
session_id: session_id.clone(),
controller_device: controller_device.clone(),
stop_flag: stop_flag.clone(),
});
}
// 获取信令客户端
let signal_client = get_signal_client().await;
// 启动屏幕捕获线程
let session_id_capture = session_id.clone();
let controller_device_capture = controller_device.clone();
let my_device_id_capture = my_device_id.clone();
let signal_client_clone = signal_client.clone();
let active_session_clone = active_session.clone();
tokio::task::spawn_blocking(move || {
// 创建屏幕捕获器
let mut capturer = match ScreenCapturer::new(0) {
Ok(c) => c,
Err(e) => {
tracing::error!("创建屏幕捕获器失败: {}", e);
return;
}
};
let width = capturer.width();
let height = capturer.height();
tracing::info!("屏幕分辨率: {}x{}", width, height);
let mut frame_count = 0u64;
let start_time = std::time::Instant::now();
loop {
// 检查是否需要停止
let should_stop = {
let rt = tokio::runtime::Handle::current();
rt.block_on(async {
let session = active_session_clone.read().await;
session.as_ref().map(|s| s.stop_flag.load(Ordering::SeqCst)).unwrap_or(true)
})
};
if should_stop {
tracing::info!("屏幕流停止");
break;
}
// 捕获帧
match capturer.capture_rgba() {
Ok(Some(rgba_data)) => {
// 使用 image crate 压缩为 JPEG
let img = image::RgbaImage::from_raw(width, height, rgba_data);
if let Some(img) = img {
let mut jpeg_data = Vec::new();
let mut cursor = std::io::Cursor::new(&mut jpeg_data);
// 降低分辨率和质量以减少带宽
let scaled = image::imageops::resize(&img, width / 2, height / 2, image::imageops::FilterType::Nearest);
if scaled.write_to(&mut cursor, image::ImageFormat::Jpeg).is_ok() {
let jpeg_len = jpeg_data.len();
// 发送帧
let rt = tokio::runtime::Handle::current();
let session_id_send = session_id_capture.clone();
let controller_device_send = controller_device_capture.clone();
let signal_client_send = signal_client_clone.clone();
let scaled_width = width / 2;
let scaled_height = height / 2;
rt.block_on(async move {
let client = signal_client_send.read().await;
if let Some(c) = client.as_ref() {
let _ = c.send_frame(
session_id_send,
controller_device_send,
scaled_width,
scaled_height,
&jpeg_data,
).await;
}
});
frame_count += 1;
if frame_count % 30 == 0 {
let fps = frame_count as f64 / start_time.elapsed().as_secs_f64();
tracing::debug!("屏幕流帧率: {:.1} fps, 帧大小: {} KB", fps, jpeg_len / 1024);
}
}
}
}
Ok(None) => {
// 没有新帧
}
Err(e) => {
tracing::error!("捕获屏幕失败: {}", e);
break;
}
}
// 控制帧率 (约 15 fps)
std::thread::sleep(std::time::Duration::from_millis(66));
}
// 清理会话
let rt = tokio::runtime::Handle::current();
rt.block_on(async {
let mut session = active_session_clone.write().await;
*session = None;
});
});
}
/// 处理鼠标输入
fn handle_mouse_input(x: f64, y: f64, event_type: &str, button: Option<u8>, delta: Option<f64>) {
use easyremote_client_core::InputController;
// 获取屏幕缩放比例 (因为我们发送的是缩放后的帧)
let scale = 2.0;
let actual_x = (x * scale) as i32;
let actual_y = (y * scale) as i32;
if let Ok(mut controller) = InputController::new() {
match event_type {
"move" => {
let _ = controller.move_mouse(actual_x, actual_y);
}
"click" => {
let btn = match button {
Some(0) => easyremote_client_core::MouseButton::Left,
Some(2) => easyremote_client_core::MouseButton::Right,
Some(1) => easyremote_client_core::MouseButton::Middle,
_ => easyremote_client_core::MouseButton::Left,
};
let _ = controller.click(btn);
}
"down" => {
let btn = match button {
Some(0) => easyremote_client_core::MouseButton::Left,
Some(2) => easyremote_client_core::MouseButton::Right,
Some(1) => easyremote_client_core::MouseButton::Middle,
_ => easyremote_client_core::MouseButton::Left,
};
let _ = controller.mouse_down(btn);
}
"up" => {
let btn = match button {
Some(0) => easyremote_client_core::MouseButton::Left,
Some(2) => easyremote_client_core::MouseButton::Right,
Some(1) => easyremote_client_core::MouseButton::Middle,
_ => easyremote_client_core::MouseButton::Left,
};
let _ = controller.mouse_up(btn);
}
"scroll" => {
if let Some(d) = delta {
let _ = controller.scroll(0, -(d as i32) / 3);
}
}
_ => {}
}
}
}
/// 处理键盘输入
fn handle_keyboard_input(key: &str, event_type: &str) {
use easyremote_client_core::InputController;
if let Ok(mut controller) = InputController::new() {
match event_type {
"down" => {
let _ = controller.key_down(key);
}
"up" => {
let _ = controller.key_up(key);
}
_ => {}
}
}
}
/// 获取操作系统类型
fn get_os_type() -> &'static str {
if cfg!(target_os = "windows") {
"Windows"
} else if cfg!(target_os = "macos") {
"macOS"
} else {
"Linux"
}
}
/// 获取操作系统版本
fn get_os_version() -> String {
std::env::consts::OS.to_string()
}
/// 向服务端注册设备
async fn register_device_to_server(
server_url: &str,
device_id: &str,
device_name: &str,
auth_token: Option<&str>,
) -> Result<(), String> {
let client = reqwest::Client::new();
let http_url = server_url.replace("ws://", "http://").replace("wss://", "https://");
let mut request = client
.post(format!("{}/api/devices/register", http_url))
.json(&serde_json::json!({
"device_id": device_id,
"name": device_name,
"os_type": get_os_type(),
"os_version": get_os_version()
}));
// 如果已登录,附带 token 以绑定设备到用户
if let Some(token) = auth_token {
request = request.header("Authorization", format!("Bearer {}", token));
}
let response = request.send().await.map_err(|e| e.to_string())?;
if !response.status().is_success() {
let error: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
return Err(error["error"].as_str().unwrap_or("设备注册失败").to_string());
}
tracing::info!("设备已注册到服务端");
Ok(())
}
/// 连接到信令服务器
async fn connect_to_signal_server(device_id: String, server_url: String) -> Result<(), String> {
let signal_client_holder = get_signal_client().await;
// 如果已连接,先断开
{
let mut holder = signal_client_holder.write().await;
*holder = None;
}
// 创建新的信令客户端
let mut signal_client = SignalClient::new(device_id.clone(), server_url.clone());
// 连接到信令服务器
let device_id_clone = device_id.clone();
let device_id_for_callback = device_id.clone();
match signal_client.connect(move |msg| {
let device_id_inner = device_id_for_callback.clone();
match msg {
SignalMessage::HeartbeatAck => {
tracing::debug!("收到心跳响应");
}
SignalMessage::ForceOffline { device_id } => {
tracing::info!("收到强制下线命令: device_id={}", device_id);
// 设置强制下线标志
set_force_offline();
}
SignalMessage::ConnectRequest { session_id, from_device, to_device, .. } => {
tracing::info!("收到连接请求: session_id={}, from={}, to={}", session_id, from_device, to_device);
// 启动屏幕流
let session_id_clone = session_id.clone();
let from_device_clone = from_device.clone();
let device_id_stream = device_id_inner.clone();
tokio::spawn(async move {
start_screen_streaming(session_id_clone, from_device_clone, device_id_stream).await;
});
}
SignalMessage::MouseEvent { x, y, event_type, button, delta, .. } => {
// 处理鼠标事件
handle_mouse_input(x, y, &event_type, button, delta);
}
SignalMessage::KeyboardEvent { key, event_type, .. } => {
// 处理键盘事件
handle_keyboard_input(&key, &event_type);
}
_ => {
tracing::debug!("收到信令消息: {:?}", msg);
}
}
}).await {
Ok(_) => {
tracing::info!("已连接到信令服务器");
// 存储客户端
let mut holder = signal_client_holder.write().await;
*holder = Some(signal_client);
// 启动心跳任务
let holder_clone = signal_client_holder.clone();
let device_id_for_heartbeat = device_id.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
loop {
interval.tick().await;
let holder = holder_clone.read().await;
if let Some(client) = holder.as_ref() {
if client.is_connected().await {
if let Err(e) = client.send_heartbeat().await {
tracing::warn!("发送心跳失败: {}", e);
break;
}
tracing::debug!("发送心跳");
} else {
tracing::info!("信令连接已断开");
break;
}
} else {
break;
}
}
});
Ok(())
}
Err(e) => {
tracing::warn!("连接信令服务器失败: {}", e);
Err(format!("连接信令服务器失败: {}", e))
}
}
}
/// 初始化 - 验证 token、注册设备并连接信令服务器
#[tauri::command]
pub async fn initialize(state: State<'_, AppStateHandle>) -> Result<bool, String> {
let mut app_state = state.write().await;
let mut logged_in = false;
// 如果有保存的 token验证是否仍然有效
if let Some(token) = &app_state.auth_token {
let client = reqwest::Client::new();
let server_url = app_state.config.server_url.replace("ws://", "http://").replace("wss://", "https://");
let response = client
.get(format!("{}/api/users/me", server_url))
.header("Authorization", format!("Bearer {}", token))
.send()
.await;
match response {
Ok(resp) if resp.status().is_success() => {
tracing::info!("Token 验证成功,用户已登录");
logged_in = true;
}
_ => {
tracing::info!("Token 已过期,需要重新登录");
app_state.current_user = None;
app_state.auth_token = None;
app_state.clear_auth_data();
}
}
}
// 获取必要信息
let device_id = app_state.device_id.0.clone();
let device_name = app_state.config.device_name.clone();
let server_url = app_state.config.server_url.clone();
let token = app_state.auth_token.clone();
drop(app_state); // 释放锁
// 注册设备到服务端
let _ = register_device_to_server(
&server_url,
&device_id,
&device_name,
token.as_deref(),
).await;
// 连接到信令服务器WebSocket- 这会使设备显示为在线
let _ = connect_to_signal_server(device_id, server_url).await;
Ok(logged_in)
}
/// 获取设备信息
#[tauri::command]
pub async fn get_device_info(state: State<'_, AppStateHandle>) -> Result<DeviceInfo, String> {
let state = state.read().await;
Ok(DeviceInfo {
device_id: state.device_id.0.clone(),
device_id_formatted: state.device_id.formatted(),
verification_code: state.verification_code.0.clone(),
allow_remote: state.allow_remote,
device_name: state.config.device_name.clone(),
os_type: get_os_type().to_string(),
})
}
/// 刷新验证码
#[tauri::command]
pub async fn refresh_verification_code(state: State<'_, AppStateHandle>) -> Result<String, String> {
let mut state = state.write().await;
Ok(state.refresh_code())
}
/// 设置是否允许远程控制
#[tauri::command]
pub async fn set_allow_remote(state: State<'_, AppStateHandle>, allow: bool) -> Result<(), String> {
let mut state = state.write().await;
state.allow_remote = allow;
Ok(())
}
/// 用户登录
#[tauri::command]
pub async fn login(
state: State<'_, AppStateHandle>,
username: String,
password: String,
) -> Result<CurrentUser, String> {
let mut app_state = state.write().await;
// 构建登录请求
let client = reqwest::Client::new();
let server_url = app_state.config.server_url.replace("ws://", "http://").replace("wss://", "https://");
let response = client
.post(format!("{}/api/auth/login", server_url))
.json(&serde_json::json!({
"username": username,
"password": password,
"device_id": app_state.device_id.0
}))
.send()
.await
.map_err(|e| e.to_string())?;
if !response.status().is_success() {
let error: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
return Err(error["error"].as_str().unwrap_or("登录失败").to_string());
}
let result: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
let data = &result["data"];
let user = CurrentUser {
id: data["user"]["id"].as_str().unwrap_or("").to_string(),
username: data["user"]["username"].as_str().unwrap_or("").to_string(),
email: data["user"]["email"].as_str().map(|s| s.to_string()),
};
let token = data["token"].as_str().unwrap_or("").to_string();
app_state.set_user(user.clone(), token.clone());
// 登录成功后注册设备到服务端(绑定用户)
let device_id = app_state.device_id.0.clone();
let device_name = app_state.config.device_name.clone();
drop(app_state);
let _ = register_device_to_server(&server_url, &device_id, &device_name, Some(&token)).await;
Ok(user)
}
/// 用户注册
#[tauri::command]
pub async fn register(
state: State<'_, AppStateHandle>,
username: String,
password: String,
email: Option<String>,
) -> Result<CurrentUser, String> {
let mut app_state = state.write().await;
let client = reqwest::Client::new();
let server_url = &app_state.config.server_url.replace("ws://", "http://").replace("wss://", "https://");
let response = client
.post(format!("{}/api/auth/register", server_url))
.json(&serde_json::json!({
"username": username,
"password": password,
"email": email
}))
.send()
.await
.map_err(|e| e.to_string())?;
if !response.status().is_success() {
let error: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
return Err(error["error"].as_str().unwrap_or("注册失败").to_string());
}
let result: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
let data = &result["data"];
let user = CurrentUser {
id: data["user"]["id"].as_str().unwrap_or("").to_string(),
username: data["user"]["username"].as_str().unwrap_or("").to_string(),
email: data["user"]["email"].as_str().map(|s| s.to_string()),
};
let token = data["token"].as_str().unwrap_or("").to_string();
app_state.set_user(user.clone(), token);
Ok(user)
}
/// 用户退出登录
#[tauri::command]
pub async fn logout(state: State<'_, AppStateHandle>) -> Result<(), String> {
let mut state = state.write().await;
state.logout();
Ok(())
}
/// 获取当前用户
#[tauri::command]
pub async fn get_current_user(state: State<'_, AppStateHandle>) -> Result<Option<CurrentUser>, String> {
let state = state.read().await;
Ok(state.current_user.clone())
}
/// 获取用户设备列表
#[tauri::command]
pub async fn get_devices(state: State<'_, AppStateHandle>) -> Result<Vec<serde_json::Value>, String> {
let app_state = state.read().await;
let token = app_state.auth_token.as_ref()
.ok_or("未登录")?;
let client = reqwest::Client::new();
let server_url = &app_state.config.server_url.replace("ws://", "http://").replace("wss://", "https://");
let response = client
.get(format!("{}/api/devices", server_url))
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| e.to_string())?;
if !response.status().is_success() {
return Err("获取设备列表失败".to_string());
}
let result: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
let devices = result["data"].as_array()
.map(|arr| arr.clone())
.unwrap_or_default();
Ok(devices)
}
/// 移除设备
#[tauri::command]
pub async fn remove_device(
state: State<'_, AppStateHandle>,
device_id: String,
) -> Result<(), String> {
let app_state = state.read().await;
let token = app_state.auth_token.as_ref()
.ok_or("未登录")?;
let client = reqwest::Client::new();
let server_url = &app_state.config.server_url.replace("ws://", "http://").replace("wss://", "https://");
let response = client
.delete(format!("{}/api/devices/{}", server_url, device_id))
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| e.to_string())?;
if !response.status().is_success() {
return Err("移除设备失败".to_string());
}
Ok(())
}
/// 连接到目标设备
#[tauri::command]
pub async fn connect_to_device(
state: State<'_, AppStateHandle>,
request: ConnectRequest,
) -> Result<(), String> {
let mut app_state = state.write().await;
app_state.connection_state = ConnectionState::Connecting;
// TODO: 实现实际的P2P连接逻辑
// 这里需要:
// 1. 连接到信令服务器
// 2. 发送连接请求
// 3. 进行ICE候选交换
// 4. 建立P2P连接
// 模拟连接过程
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
app_state.connection_state = ConnectionState::Connected {
target_device_id: request.device_id.clone(),
target_device_name: format!("设备 {}", request.device_id),
connection_type: "P2P".to_string(),
};
Ok(())
}
/// 断开连接
#[tauri::command]
pub async fn disconnect(state: State<'_, AppStateHandle>) -> Result<(), String> {
let mut state = state.write().await;
state.connection_state = ConnectionState::Disconnected;
state.current_session_id = None;
Ok(())
}
/// 获取连接状态
#[tauri::command]
pub async fn get_connection_state(state: State<'_, AppStateHandle>) -> Result<ConnectionState, String> {
let state = state.read().await;
Ok(state.connection_state.clone())
}
/// 获取历史记录
#[tauri::command]
pub async fn get_history(
state: State<'_, AppStateHandle>,
page: Option<u32>,
limit: Option<u32>,
) -> Result<Vec<HistoryItem>, String> {
let app_state = state.read().await;
let token = app_state.auth_token.as_ref()
.ok_or("未登录")?;
let client = reqwest::Client::new();
let server_url = &app_state.config.server_url.replace("ws://", "http://").replace("wss://", "https://");
let response = client
.get(format!("{}/api/sessions/history", server_url))
.query(&[("page", page.unwrap_or(1)), ("limit", limit.unwrap_or(20))])
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| e.to_string())?;
if !response.status().is_success() {
return Err("获取历史记录失败".to_string());
}
let result: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
let items = result["data"]["items"].as_array()
.map(|arr| {
arr.iter().filter_map(|item| {
Some(HistoryItem {
id: item["id"].as_str()?.to_string(),
device_id: item["controlled_device_id"].as_str()?.to_string(),
device_name: item["controlled_device_name"].as_str()?.to_string(),
started_at: item["started_at"].as_str()?.to_string(),
ended_at: item["ended_at"].as_str().map(|s| s.to_string()),
duration: item["duration"].as_str().map(|s| s.to_string()),
connection_type: item["connection_type"].as_str()?.to_string(),
})
}).collect()
})
.unwrap_or_default();
Ok(items)
}
/// 获取配置
#[tauri::command]
pub async fn get_config(state: State<'_, AppStateHandle>) -> Result<ClientConfig, String> {
let state = state.read().await;
Ok(state.config.clone())
}
/// 保存配置
#[tauri::command]
pub async fn save_config(
state: State<'_, AppStateHandle>,
config: ClientConfig,
) -> Result<(), String> {
let mut state = state.write().await;
state.config = config.clone();
config.save().map_err(|e| e.to_string())?;
Ok(())
}

View File

@ -0,0 +1,195 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod commands;
mod state;
use state::AppState;
use std::sync::Arc;
use tauri::Manager;
use tokio::sync::RwLock;
#[cfg(windows)]
mod single_instance {
use windows::core::PCWSTR;
use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, LPARAM, GetLastError, ERROR_ALREADY_EXISTS};
use windows::Win32::System::Threading::CreateMutexW;
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetWindowTextW, IsWindowVisible, SetForegroundWindow, ShowWindow, SW_RESTORE,
};
static MUTEX_NAME: &str = "Global\\EasyRemoteClientMutex";
static WINDOW_TITLE: &str = "EasyRemote";
pub struct SingleInstance {
_handle: HANDLE,
}
impl SingleInstance {
pub fn new() -> Option<Self> {
unsafe {
let mutex_name: Vec<u16> = MUTEX_NAME.encode_utf16().chain(std::iter::once(0)).collect();
let handle = CreateMutexW(None, true, PCWSTR(mutex_name.as_ptr())).ok()?;
if GetLastError() == ERROR_ALREADY_EXISTS {
// 已有实例在运行,尝试激活它
activate_existing_window();
let _ = CloseHandle(handle);
return None;
}
Some(Self { _handle: handle })
}
}
}
fn activate_existing_window() {
unsafe {
let _ = EnumWindows(Some(enum_window_callback), LPARAM(0));
}
}
unsafe extern "system" fn enum_window_callback(hwnd: HWND, _: LPARAM) -> windows::Win32::Foundation::BOOL {
if !IsWindowVisible(hwnd).as_bool() {
return true.into();
}
let mut title = [0u16; 256];
let len = GetWindowTextW(hwnd, &mut title);
if len > 0 {
let title_str = String::from_utf16_lossy(&title[..len as usize]);
if title_str.contains(WINDOW_TITLE) {
// 找到窗口,恢复并聚焦
let _ = ShowWindow(hwnd, SW_RESTORE);
let _ = SetForegroundWindow(hwnd);
return false.into(); // 停止枚举
}
}
true.into()
}
}
#[cfg(not(windows))]
mod single_instance {
use std::fs::File;
use std::path::PathBuf;
pub struct SingleInstance {
_lock_file: File,
}
impl SingleInstance {
pub fn new() -> Option<Self> {
let lock_path = get_lock_path();
// 尝试以独占方式打开锁文件
match File::options()
.write(true)
.create(true)
.truncate(true)
.open(&lock_path)
{
Ok(file) => {
// 尝试获取文件锁
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let result = unsafe {
libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB)
};
if result != 0 {
eprintln!("EasyRemote 已在运行中");
return None;
}
}
Some(Self { _lock_file: file })
}
Err(_) => {
eprintln!("EasyRemote 已在运行中");
None
}
}
}
}
fn get_lock_path() -> PathBuf {
dirs::runtime_dir()
.or_else(dirs::cache_dir)
.unwrap_or_else(std::env::temp_dir)
.join("easyremote.lock")
}
}
fn main() {
// 单例检查 - 确保只运行一个实例
let _instance = match single_instance::SingleInstance::new() {
Some(instance) => instance,
None => {
// 已有实例在运行,退出
eprintln!("EasyRemote 客户端已在运行中,将激活现有窗口");
return;
}
};
// 初始化日志
tracing_subscriber::fmt::init();
// 创建应用状态
let state = Arc::new(RwLock::new(AppState::new()));
tauri::Builder::default()
.setup(|app| {
// 设置窗口标题以便单例检测
if let Some(window) = app.get_window("main") {
let _ = window.set_title("EasyRemote");
// 启动强制下线检测任务
let window_clone = window.clone();
std::thread::spawn(move || {
loop {
std::thread::sleep(std::time::Duration::from_millis(500));
// 检查是否需要强制下线
if commands::check_force_offline() {
tracing::info!("执行强制下线,关闭应用");
// 关闭窗口并退出
let _ = window_clone.close();
break;
}
}
});
}
Ok(())
})
.manage(state)
.invoke_handler(tauri::generate_handler![
// 初始化
commands::initialize,
// 设备信息
commands::get_device_info,
commands::refresh_verification_code,
commands::set_allow_remote,
// 账号
commands::login,
commands::logout,
commands::register,
commands::get_current_user,
// 设备管理
commands::get_devices,
commands::remove_device,
// 连接
commands::connect_to_device,
commands::disconnect,
commands::get_connection_state,
// 历史记录
commands::get_history,
// 配置
commands::get_config,
commands::save_config,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -0,0 +1,221 @@
//! 应用状态管理
use easyremote_common::types::{DeviceId, VerificationCode};
use easyremote_client_core::ClientConfig;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// 应用状态
pub struct AppState {
/// 配置
pub config: ClientConfig,
/// 当前设备ID
pub device_id: DeviceId,
/// 验证码
pub verification_code: VerificationCode,
/// 是否允许远程控制
pub allow_remote: bool,
/// 当前登录用户
pub current_user: Option<CurrentUser>,
/// 认证令牌
pub auth_token: Option<String>,
/// 连接状态
pub connection_state: ConnectionState,
/// 当前会话ID
pub current_session_id: Option<String>,
}
/// 当前用户信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CurrentUser {
pub id: String,
pub username: String,
pub email: Option<String>,
}
/// 连接状态
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ConnectionState {
/// 断开连接
Disconnected,
/// 正在连接
Connecting,
/// 已连接到设备
Connected {
target_device_id: String,
target_device_name: String,
connection_type: String,
},
/// 被控制中
BeingControlled {
controller_device_id: String,
},
}
/// 持久化的认证数据
#[derive(Debug, Serialize, Deserialize)]
struct AuthData {
server_url: String,
token: String,
user: CurrentUser,
}
impl AppState {
pub fn new() -> Self {
// 尝试加载已保存的设备ID
let (device_id, verification_code) = Self::load_or_create_device_id();
let config = ClientConfig::load();
// 尝试加载已保存的登录信息
let (current_user, auth_token) = Self::load_auth_data(&config.server_url);
Self {
config,
device_id,
verification_code,
allow_remote: false,
current_user,
auth_token,
connection_state: ConnectionState::Disconnected,
current_session_id: None,
}
}
fn get_data_dir() -> PathBuf {
dirs::data_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("easyremote")
}
fn load_or_create_device_id() -> (DeviceId, VerificationCode) {
let data_path = Self::get_data_dir().join("device.json");
if data_path.exists() {
if let Ok(content) = std::fs::read_to_string(&data_path) {
if let Ok(data) = serde_json::from_str::<DeviceData>(&content) {
return (DeviceId(data.device_id), VerificationCode::generate());
}
}
}
// 创建新的设备ID
let device_id = DeviceId::generate();
let verification_code = VerificationCode::generate();
// 保存设备ID
if let Some(parent) = data_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let data = DeviceData {
device_id: device_id.0.clone(),
};
let _ = std::fs::write(&data_path, serde_json::to_string(&data).unwrap());
(device_id, verification_code)
}
/// 加载认证数据(如果服务器地址匹配)
fn load_auth_data(server_url: &str) -> (Option<CurrentUser>, Option<String>) {
let auth_path = Self::get_data_dir().join("auth.json");
if auth_path.exists() {
if let Ok(content) = std::fs::read_to_string(&auth_path) {
if let Ok(auth_data) = serde_json::from_str::<AuthData>(&content) {
// 只有当服务器地址匹配时才恢复登录状态
if auth_data.server_url == server_url {
tracing::info!("已恢复登录状态: {}", auth_data.user.username);
return (Some(auth_data.user), Some(auth_data.token));
} else {
tracing::info!("服务器地址已变更,需要重新登录");
}
}
}
}
(None, None)
}
/// 保存认证数据
pub fn save_auth_data(&self) -> Result<(), std::io::Error> {
if let (Some(user), Some(token)) = (&self.current_user, &self.auth_token) {
let auth_path = Self::get_data_dir().join("auth.json");
if let Some(parent) = auth_path.parent() {
std::fs::create_dir_all(parent)?;
}
let auth_data = AuthData {
server_url: self.config.server_url.clone(),
token: token.clone(),
user: user.clone(),
};
std::fs::write(&auth_path, serde_json::to_string(&auth_data).unwrap())?;
tracing::info!("登录信息已保存");
}
Ok(())
}
/// 清除认证数据
pub fn clear_auth_data(&self) {
let auth_path = Self::get_data_dir().join("auth.json");
let _ = std::fs::remove_file(&auth_path);
tracing::info!("登录信息已清除");
}
/// 刷新验证码
pub fn refresh_code(&mut self) -> String {
self.verification_code.refresh();
self.verification_code.0.clone()
}
/// 设置登录用户并保存
pub fn set_user(&mut self, user: CurrentUser, token: String) {
self.current_user = Some(user);
self.auth_token = Some(token);
// 保存登录信息
let _ = self.save_auth_data();
}
/// 退出登录并清除保存的数据
pub fn logout(&mut self) {
self.current_user = None;
self.auth_token = None;
self.clear_auth_data();
}
}
#[derive(Debug, Serialize, Deserialize)]
struct DeviceData {
device_id: String,
}
/// 设备信息响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceInfo {
pub device_id: String,
pub device_id_formatted: String,
pub verification_code: String,
pub allow_remote: bool,
pub device_name: String,
pub os_type: String,
}
/// 连接设备请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectRequest {
pub device_id: String,
pub verification_code: String,
}
/// 历史记录项
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryItem {
pub id: String,
pub device_id: String,
pub device_name: String,
pub started_at: String,
pub ended_at: Option<String>,
pub duration: Option<String>,
pub connection_type: String,
}

View File

@ -0,0 +1,81 @@
{
"build": {
"beforeDevCommand": "",
"beforeBuildCommand": "",
"devPath": "http://localhost:5173",
"distDir": "ui/dist"
},
"package": {
"productName": "EasyRemote",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"open": true
},
"window": {
"all": true
},
"dialog": {
"all": true
},
"clipboard": {
"all": true
}
},
"bundle": {
"active": true,
"category": "Utility",
"copyright": "Copyright © 2024 EasyRemote Team",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.easyremote.app",
"longDescription": "A remote desktop control application",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "Remote Desktop Control",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"height": 700,
"resizable": true,
"title": "EasyRemote - 远程协助",
"width": 480,
"center": true,
"decorations": true,
"minWidth": 400,
"minHeight": 600
}
]
}
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EasyRemote - 远程协助</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1367
crates/client-tauri/ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
{
"name": "easyremote-ui",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"@tauri-apps/api": "^1.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import './styles/main.css'
createApp(App).mount('#app')

View File

@ -0,0 +1,473 @@
/* EasyRemote 主题样式 */
:root {
/* 主色调 - 深蓝科技感 */
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-light: #60a5fa;
--primary-bg: rgba(59, 130, 246, 0.1);
/* 背景色 */
--bg-primary: #0a0f1a;
--bg-secondary: #111827;
--bg-tertiary: #1f2937;
--bg-card: #161f2d;
/* 文字色 */
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--text-muted: #6b7280;
/* 边框 */
--border-color: #2d3748;
--border-light: #4b5563;
/* 状态色 */
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--info: #06b6d4;
/* 阴影 */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.6);
/* 圆角 */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
/* 字体 */
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
height: 100%;
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-light);
}
/* 主容器 */
.app-container {
display: flex;
flex-direction: column;
height: 100%;
background: linear-gradient(180deg, var(--bg-primary) 0%, #050810 100%);
}
/* 头部 */
.app-header {
padding: 14px 20px;
background: rgba(22, 31, 45, 0.9);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.app-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
/* 主内容区 */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
overflow-x: hidden;
}
.tab-content {
animation: fadeIn 0.25s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 卡片 */
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
margin-bottom: 16px;
transition: all 0.2s ease;
}
.card:hover {
border-color: var(--border-light);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
.card-subtitle {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
line-height: 1.5;
}
/* 开关 */
.switch {
position: relative;
width: 44px;
height: 24px;
background: var(--bg-tertiary);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid var(--border-color);
}
.switch.active {
background: var(--primary);
border-color: var(--primary);
}
.switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
background: var(--text-primary);
border-radius: 50%;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
}
.switch.active::after {
left: 22px;
}
/* 输入框 */
.input-group {
margin-bottom: 16px;
}
.input-label {
display: block;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 8px;
font-weight: 500;
}
.input {
width: 100%;
padding: 12px 14px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 14px;
transition: all 0.2s ease;
}
.input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-bg);
}
.input::placeholder {
color: var(--text-muted);
}
/* 按钮 */
.btn {
padding: 10px 20px;
border: none;
border-radius: var(--radius-md);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover:not(:disabled) {
background: var(--border-color);
border-color: var(--border-light);
}
.btn-icon {
padding: 8px;
background: transparent;
color: var(--text-secondary);
border-radius: var(--radius-sm);
}
.btn-icon:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-block {
width: 100%;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
/* 状态指示器 */
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-dot.online {
background: var(--success);
box-shadow: 0 0 8px var(--success);
}
.status-dot.offline {
background: var(--text-muted);
}
/* 历史记录列表 */
.history-item {
display: flex;
align-items: center;
padding: 14px;
background: var(--bg-tertiary);
border-radius: var(--radius-md);
margin-bottom: 10px;
transition: all 0.2s ease;
}
.history-item:last-child {
margin-bottom: 0;
}
.history-item:hover {
background: var(--border-color);
}
.history-icon {
width: 38px;
height: 38px;
background: var(--primary-bg);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
margin-right: 14px;
color: var(--primary);
flex-shrink: 0;
}
.history-info {
flex: 1;
min-width: 0;
}
.history-name {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-meta {
font-size: 11px;
color: var(--text-secondary);
margin-top: 3px;
}
.history-action {
color: var(--primary);
cursor: pointer;
font-size: 13px;
background: none;
border: none;
}
/* 用户菜单 */
.user-menu {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
color: white;
}
.user-name {
font-size: 13px;
color: var(--text-primary);
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-secondary);
}
.empty-icon {
font-size: 40px;
margin-bottom: 12px;
opacity: 0.6;
}
.empty-text {
font-size: 13px;
}
/* 加载动画 */
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 分割线 */
.divider {
height: 1px;
background: var(--border-color);
margin: 20px 0;
}
/* 响应式 */
@media (max-width: 480px) {
.app-header {
padding: 12px 16px;
}
.main-content {
padding: 16px;
}
.card {
padding: 16px;
}
}
/* 动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

View File

@ -0,0 +1,63 @@
// Tauri 命令类型定义
export interface DeviceInfo {
device_id: string;
device_id_formatted: string;
verification_code: string;
allow_remote: boolean;
device_name: string;
os_type: string;
}
export interface CurrentUser {
id: string;
username: string;
email: string | null;
}
export interface Device {
id: string;
device_id: string;
name: string;
os_type: string;
os_version: string;
is_online: boolean;
allow_remote: boolean;
last_seen: string;
}
export interface HistoryItem {
id: string;
device_id: string;
device_name: string;
started_at: string;
ended_at: string | null;
duration: string | null;
connection_type: string;
}
export interface ConnectRequest {
device_id: string;
verification_code: string;
}
export type ConnectionState =
| { type: 'Disconnected' }
| { type: 'Connecting' }
| { type: 'Connected'; target_device_id: string; target_device_name: string; connection_type: string }
| { type: 'BeingControlled'; controller_device_id: string };
export interface ClientConfig {
server_url: string;
device_name: string;
quality: QualityConfig;
auto_start: boolean;
launch_on_boot: boolean;
}
export interface QualityConfig {
frame_rate: number;
resolution_scale: number;
image_quality: number;
hardware_acceleration: boolean;
}

View File

@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
clearScreen: false,
server: {
port: 5173,
strictPort: true,
},
envPrefix: ['VITE_', 'TAURI_'],
build: {
target: ['es2021', 'chrome100', 'safari13'],
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
sourcemap: !!process.env.TAURI_DEBUG,
},
})

18
crates/common/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "easyremote-common"
version.workspace = true
edition.workspace = true
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
bincode = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
ring = { workspace = true }
base64 = { workspace = true }
rand = { workspace = true }
bytes = { workspace = true }
hostname = "0.3"

178
crates/common/src/crypto.rs Normal file
View File

@ -0,0 +1,178 @@
//! 加密工具
use crate::{Error, Result};
use ring::aead::{Aad, BoundKey, Nonce, NonceSequence, SealingKey, OpeningKey, UnboundKey, AES_256_GCM};
use ring::rand::{SecureRandom, SystemRandom};
/// 密钥长度
pub const KEY_SIZE: usize = 32;
/// Nonce长度
pub const NONCE_SIZE: usize = 12;
/// 认证标签长度
pub const TAG_SIZE: usize = 16;
/// 生成随机密钥
pub fn generate_key() -> [u8; KEY_SIZE] {
let rng = SystemRandom::new();
let mut key = [0u8; KEY_SIZE];
rng.fill(&mut key).expect("Failed to generate key");
key
}
/// 生成随机Nonce
pub fn generate_nonce() -> [u8; NONCE_SIZE] {
let rng = SystemRandom::new();
let mut nonce = [0u8; NONCE_SIZE];
rng.fill(&mut nonce).expect("Failed to generate nonce");
nonce
}
/// 计数器Nonce序列
struct CounterNonceSequence {
counter: u64,
prefix: [u8; 4],
}
impl CounterNonceSequence {
fn new() -> Self {
let rng = SystemRandom::new();
let mut prefix = [0u8; 4];
rng.fill(&mut prefix).expect("Failed to generate prefix");
Self { counter: 0, prefix }
}
}
impl NonceSequence for CounterNonceSequence {
fn advance(&mut self) -> std::result::Result<Nonce, ring::error::Unspecified> {
let mut nonce = [0u8; NONCE_SIZE];
nonce[..4].copy_from_slice(&self.prefix);
nonce[4..].copy_from_slice(&self.counter.to_be_bytes());
self.counter += 1;
Nonce::try_assume_unique_for_key(&nonce)
}
}
/// 加密器
pub struct Encryptor {
sealing_key: SealingKey<CounterNonceSequence>,
}
impl Encryptor {
/// 创建新的加密器
pub fn new(key: &[u8; KEY_SIZE]) -> Result<Self> {
let unbound_key = UnboundKey::new(&AES_256_GCM, key)
.map_err(|_| Error::CryptoError("Invalid key".into()))?;
let sealing_key = SealingKey::new(unbound_key, CounterNonceSequence::new());
Ok(Self { sealing_key })
}
/// 加密数据
pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>> {
let mut in_out = plaintext.to_vec();
// 预留TAG空间
in_out.extend_from_slice(&[0u8; TAG_SIZE]);
self.sealing_key
.seal_in_place_append_tag(Aad::empty(), &mut in_out)
.map_err(|_| Error::CryptoError("Encryption failed".into()))?;
Ok(in_out)
}
}
/// 解密器
pub struct Decryptor {
opening_key: OpeningKey<CounterNonceSequence>,
}
impl Decryptor {
/// 创建新的解密器
pub fn new(key: &[u8; KEY_SIZE]) -> Result<Self> {
let unbound_key = UnboundKey::new(&AES_256_GCM, key)
.map_err(|_| Error::CryptoError("Invalid key".into()))?;
let opening_key = OpeningKey::new(unbound_key, CounterNonceSequence::new());
Ok(Self { opening_key })
}
/// 解密数据
pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>> {
let mut in_out = ciphertext.to_vec();
let plaintext = self.opening_key
.open_in_place(Aad::empty(), &mut in_out)
.map_err(|_| Error::CryptoError("Decryption failed".into()))?;
Ok(plaintext.to_vec())
}
}
/// 密码哈希
pub mod password {
use super::*;
use ring::pbkdf2;
use std::num::NonZeroU32;
const CREDENTIAL_LEN: usize = 32;
const ITERATIONS: u32 = 100_000;
/// 哈希密码
pub fn hash_password(password: &str, salt: &[u8]) -> [u8; CREDENTIAL_LEN] {
let mut hash = [0u8; CREDENTIAL_LEN];
pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA256,
NonZeroU32::new(ITERATIONS).unwrap(),
salt,
password.as_bytes(),
&mut hash,
);
hash
}
/// 验证密码
pub fn verify_password(password: &str, salt: &[u8], hash: &[u8]) -> bool {
pbkdf2::verify(
pbkdf2::PBKDF2_HMAC_SHA256,
NonZeroU32::new(ITERATIONS).unwrap(),
salt,
password.as_bytes(),
hash,
)
.is_ok()
}
/// 生成盐值
pub fn generate_salt() -> [u8; 16] {
let rng = SystemRandom::new();
let mut salt = [0u8; 16];
rng.fill(&mut salt).expect("Failed to generate salt");
salt
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt() {
let key = generate_key();
let mut encryptor = Encryptor::new(&key).unwrap();
let mut decryptor = Decryptor::new(&key).unwrap();
let plaintext = b"Hello, World!";
let ciphertext = encryptor.encrypt(plaintext).unwrap();
let decrypted = decryptor.decrypt(&ciphertext).unwrap();
assert_eq!(plaintext.as_slice(), decrypted.as_slice());
}
#[test]
fn test_password_hash() {
let password = "my_secure_password";
let salt = password::generate_salt();
let hash = password::hash_password(password, &salt);
assert!(password::verify_password(password, &salt, &hash));
assert!(!password::verify_password("wrong_password", &salt, &hash));
}
}

View File

@ -0,0 +1,59 @@
//! 错误类型定义
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("认证失败: {0}")]
AuthError(String),
#[error("连接错误: {0}")]
ConnectionError(String),
#[error("协议错误: {0}")]
ProtocolError(String),
#[error("加密错误: {0}")]
CryptoError(String),
#[error("设备未找到: {0}")]
DeviceNotFound(String),
#[error("权限被拒绝: {0}")]
PermissionDenied(String),
#[error("会话过期")]
SessionExpired,
#[error("网络错误: {0}")]
NetworkError(String),
#[error("编码错误: {0}")]
EncodingError(String),
#[error("数据库错误: {0}")]
DatabaseError(String),
#[error("内部错误: {0}")]
InternalError(String),
#[error("IO错误: {0}")]
IoError(#[from] std::io::Error),
#[error("序列化错误: {0}")]
SerializationError(String),
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::SerializationError(e.to_string())
}
}
impl From<bincode::Error> for Error {
fn from(e: bincode::Error) -> Self {
Error::SerializationError(e.to_string())
}
}

11
crates/common/src/lib.rs Normal file
View File

@ -0,0 +1,11 @@
//! EasyRemote 共享库
//! 包含协议定义、加密工具、通用类型等
pub mod protocol;
pub mod crypto;
pub mod types;
pub mod error;
pub mod utils;
pub use error::{Error, Result};
pub use types::*;

View File

@ -0,0 +1,302 @@
//! 通信协议定义
use crate::types::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// 消息类型标识
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[repr(u8)]
pub enum MessageType {
// 认证相关 (0x00 - 0x0F)
AuthRequest = 0x00,
AuthResponse = 0x01,
AuthChallenge = 0x02,
AuthVerify = 0x03,
// 信令相关 (0x10 - 0x1F)
SignalOffer = 0x10,
SignalAnswer = 0x11,
SignalCandidate = 0x12,
SignalReady = 0x13,
// 连接相关 (0x20 - 0x2F)
ConnectRequest = 0x20,
ConnectAccept = 0x21,
ConnectReject = 0x22,
Disconnect = 0x23,
Heartbeat = 0x24,
HeartbeatAck = 0x25,
// 数据传输 (0x30 - 0x3F)
FrameData = 0x30,
FrameAck = 0x31,
InputEvent = 0x32,
AudioData = 0x33,
ClipboardData = 0x34,
FileTransfer = 0x35,
// 控制相关 (0x40 - 0x4F)
QualityRequest = 0x40,
QualityResponse = 0x41,
ScreenInfo = 0x42,
SessionControl = 0x43,
// 错误 (0xF0 - 0xFF)
Error = 0xF0,
}
/// 协议消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtocolMessage {
/// 消息ID
pub id: u64,
/// 消息类型
pub msg_type: MessageType,
/// 消息负载
pub payload: Vec<u8>,
/// 时间戳
pub timestamp: i64,
}
impl ProtocolMessage {
pub fn new(msg_type: MessageType, payload: Vec<u8>) -> Self {
use chrono::Utc;
Self {
id: rand::random(),
msg_type,
payload,
timestamp: Utc::now().timestamp_millis(),
}
}
pub fn encode(&self) -> crate::Result<Vec<u8>> {
bincode::serialize(self).map_err(Into::into)
}
pub fn decode(data: &[u8]) -> crate::Result<Self> {
bincode::deserialize(data).map_err(Into::into)
}
}
// ==================== 认证消息 ====================
/// 认证请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthRequest {
pub device_id: DeviceId,
pub verification_code: String,
}
/// 认证响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthResponse {
pub success: bool,
pub session_token: Option<String>,
pub error: Option<String>,
}
// ==================== 信令消息 ====================
/// SDP Offer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignalOffer {
pub session_id: Uuid,
pub from_device: DeviceId,
pub to_device: DeviceId,
pub sdp: String,
}
/// SDP Answer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignalAnswer {
pub session_id: Uuid,
pub from_device: DeviceId,
pub to_device: DeviceId,
pub sdp: String,
}
/// ICE Candidate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignalCandidate {
pub session_id: Uuid,
pub from_device: DeviceId,
pub to_device: DeviceId,
pub candidate: String,
pub sdp_mid: Option<String>,
pub sdp_mline_index: Option<u32>,
}
// ==================== 连接消息 ====================
/// 连接请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectRequest {
pub session_id: Uuid,
pub from_device: DeviceId,
pub to_device: DeviceId,
pub verification_code: String,
pub quality_settings: QualitySettings,
}
/// 连接接受
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectAccept {
pub session_id: Uuid,
pub screen_width: u32,
pub screen_height: u32,
pub connection_type: ConnectionType,
}
/// 连接拒绝
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectReject {
pub session_id: Uuid,
pub reason: String,
}
// ==================== 数据传输消息 ====================
/// 视频帧数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrameData {
/// 帧序号
pub frame_id: u64,
/// 是否为关键帧
pub is_keyframe: bool,
/// 帧宽度
pub width: u32,
/// 帧高度
pub height: u32,
/// 编码格式
pub format: FrameFormat,
/// 帧数据
pub data: Vec<u8>,
/// 时间戳
pub timestamp: i64,
}
/// 帧编码格式
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum FrameFormat {
/// JPEG
Jpeg,
/// VP8
Vp8,
/// VP9
Vp9,
/// H264
H264,
/// H265
H265,
/// Raw RGBA
RawRgba,
}
/// 输入事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InputEvent {
/// 鼠标移动
MouseMove { x: i32, y: i32 },
/// 鼠标按下
MouseDown { button: MouseButton, x: i32, y: i32 },
/// 鼠标释放
MouseUp { button: MouseButton, x: i32, y: i32 },
/// 鼠标滚轮
MouseScroll { delta_x: i32, delta_y: i32 },
/// 键盘按下
KeyDown { key: String, modifiers: Modifiers },
/// 键盘释放
KeyUp { key: String, modifiers: Modifiers },
/// 文本输入
TextInput { text: String },
}
/// 鼠标按钮
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum MouseButton {
Left,
Right,
Middle,
Back,
Forward,
}
/// 修饰键状态
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct Modifiers {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub meta: bool,
}
/// 剪贴板数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClipboardData {
pub format: ClipboardFormat,
pub data: Vec<u8>,
}
/// 剪贴板格式
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum ClipboardFormat {
Text,
Html,
Image,
Files,
}
// ==================== 控制消息 ====================
/// 质量调整请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityRequest {
pub settings: QualitySettings,
}
/// 屏幕信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScreenInfo {
pub monitors: Vec<MonitorInfo>,
pub primary_index: usize,
}
/// 显示器信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitorInfo {
pub index: usize,
pub name: String,
pub width: u32,
pub height: u32,
pub x: i32,
pub y: i32,
pub is_primary: bool,
pub scale_factor: f32,
}
/// 会话控制命令
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SessionControl {
/// 请求控制权
RequestControl,
/// 释放控制权
ReleaseControl,
/// 切换显示器
SwitchMonitor { index: usize },
/// 锁定屏幕
LockScreen,
/// 重启
Restart,
/// 关机
Shutdown,
}
// ==================== 错误消息 ====================
/// 错误消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorMessage {
pub code: u32,
pub message: String,
}

220
crates/common/src/types.rs Normal file
View File

@ -0,0 +1,220 @@
//! 通用类型定义
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// 设备ID (9位数字格式如 732 940 272)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct DeviceId(pub String);
impl DeviceId {
/// 生成新的设备ID
pub fn generate() -> Self {
use rand::Rng;
let mut rng = rand::thread_rng();
let id: u32 = rng.gen_range(100_000_000..999_999_999);
Self(format!("{}", id))
}
/// 格式化显示 (如: 732 940 272)
pub fn formatted(&self) -> String {
let s = &self.0;
if s.len() == 9 {
format!("{} {} {}", &s[0..3], &s[3..6], &s[6..9])
} else {
s.clone()
}
}
/// 从字符串解析
pub fn from_string(s: &str) -> Self {
// 移除空格
Self(s.replace(' ', ""))
}
}
impl std::fmt::Display for DeviceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.formatted())
}
}
/// 验证码 (8位随机字符)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationCode(pub String);
impl VerificationCode {
/// 生成新的验证码
pub fn generate() -> Self {
use rand::Rng;
let mut rng = rand::thread_rng();
let code: String = (0..8)
.map(|_| {
let idx = rng.gen_range(0..36);
if idx < 10 {
(b'0' + idx) as char
} else {
(b'a' + idx - 10) as char
}
})
.collect();
Self(code)
}
/// 刷新验证码
pub fn refresh(&mut self) {
*self = Self::generate();
}
}
impl std::fmt::Display for VerificationCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// 用户信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: Uuid,
pub username: String,
pub email: Option<String>,
pub created_at: DateTime<Utc>,
pub last_login: Option<DateTime<Utc>>,
}
/// 设备信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Device {
pub id: Uuid,
pub device_id: DeviceId,
pub name: String,
pub os_type: OsType,
pub os_version: String,
pub user_id: Option<Uuid>,
pub is_online: bool,
pub allow_remote: bool,
pub last_seen: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
/// 操作系统类型
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum OsType {
Windows,
MacOS,
Linux,
Android,
IOS,
Unknown,
}
impl OsType {
pub fn current() -> Self {
#[cfg(target_os = "windows")]
return OsType::Windows;
#[cfg(target_os = "macos")]
return OsType::MacOS;
#[cfg(target_os = "linux")]
return OsType::Linux;
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
return OsType::Unknown;
}
}
/// 连接会话
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: Uuid,
pub controller_device_id: DeviceId,
pub controlled_device_id: DeviceId,
pub started_at: DateTime<Utc>,
pub ended_at: Option<DateTime<Utc>>,
pub connection_type: ConnectionType,
pub status: SessionStatus,
}
/// 连接类型
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ConnectionType {
/// P2P直连
P2P,
/// 服务器中转
Relay,
}
/// 会话状态
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SessionStatus {
/// 连接中
Connecting,
/// 已连接
Connected,
/// 已断开
Disconnected,
/// 连接失败
Failed,
}
/// 控制历史记录
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ControlHistory {
pub id: Uuid,
pub user_id: Option<Uuid>,
pub controller_device_id: DeviceId,
pub controlled_device_id: DeviceId,
pub controlled_device_name: String,
pub started_at: DateTime<Utc>,
pub ended_at: Option<DateTime<Utc>>,
pub duration_seconds: Option<i64>,
pub connection_type: ConnectionType,
}
/// 质量设置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualitySettings {
/// 帧率 (fps)
pub frame_rate: u32,
/// 分辨率缩放比例 (0.25 - 1.0)
pub resolution_scale: f32,
/// 图像质量 (1-100)
pub image_quality: u32,
/// 是否启用硬件加速
pub hardware_acceleration: bool,
}
impl Default for QualitySettings {
fn default() -> Self {
Self {
frame_rate: 30,
resolution_scale: 1.0,
image_quality: 80,
hardware_acceleration: true,
}
}
}
/// 登录请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
pub device_id: DeviceId,
}
/// 登录响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginResponse {
pub token: String,
pub user: User,
pub devices: Vec<Device>,
}
/// 注册请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterRequest {
pub username: String,
pub password: String,
pub email: Option<String>,
}

104
crates/common/src/utils.rs Normal file
View File

@ -0,0 +1,104 @@
//! 工具函数
use chrono::{DateTime, Utc};
/// 获取系统信息
pub fn get_system_info() -> SystemInfo {
SystemInfo {
os_name: std::env::consts::OS.to_string(),
os_version: get_os_version(),
hostname: get_hostname(),
cpu_cores: num_cpus(),
}
}
/// 系统信息
#[derive(Debug, Clone)]
pub struct SystemInfo {
pub os_name: String,
pub os_version: String,
pub hostname: String,
pub cpu_cores: usize,
}
/// 获取操作系统版本
fn get_os_version() -> String {
#[cfg(target_os = "windows")]
{
"Windows 10/11".to_string()
}
#[cfg(target_os = "macos")]
{
"macOS".to_string()
}
#[cfg(target_os = "linux")]
{
"Linux".to_string()
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
{
"Unknown".to_string()
}
}
/// 获取主机名
fn get_hostname() -> String {
hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "Unknown".to_string())
}
/// 获取CPU核心数
fn num_cpus() -> usize {
std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(1)
}
/// 格式化持续时间
pub fn format_duration(seconds: i64) -> String {
let hours = seconds / 3600;
let minutes = (seconds % 3600) / 60;
let secs = seconds % 60;
if hours > 0 {
format!("{:02}:{:02}:{:02}", hours, minutes, secs)
} else {
format!("{:02}:{:02}", minutes, secs)
}
}
/// 格式化时间
pub fn format_datetime(dt: &DateTime<Utc>) -> String {
dt.format("%Y-%m-%d %H:%M:%S").to_string()
}
/// 格式化文件大小
pub fn format_file_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
/// 生成随机字符串
pub fn random_string(len: usize) -> String {
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = rand::thread_rng();
(0..len)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect()
}

55
crates/server/Cargo.toml Normal file
View File

@ -0,0 +1,55 @@
[package]
name = "easyremote-server"
version.workspace = true
edition.workspace = true
[[bin]]
name = "easyremote-server"
path = "src/main.rs"
[dependencies]
easyremote-common = { path = "../common" }
# Async
tokio = { workspace = true }
tokio-util = { workspace = true }
futures = { workspace = true }
async-trait = { workspace = true }
# Web
axum = { workspace = true }
tower = { workspace = true }
tower-http = { workspace = true }
# Database
sqlx = { workspace = true }
# Auth
jsonwebtoken = { workspace = true }
argon2 = { workspace = true }
# Serialization
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }
# Networking
quinn = { workspace = true }
rustls = { workspace = true }
rcgen = { workspace = true }
stun = { workspace = true }
# Logging
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
# Utils
chrono = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
rand = { workspace = true }
base64 = { workspace = true }
bytes = { workspace = true }
config = { workspace = true }
dotenvy = { workspace = true }
hex = "0.4"

177
crates/server/src/config.rs Normal file
View File

@ -0,0 +1,177 @@
//! 服务端配置
use anyhow::Result;
use serde::{Deserialize, Serialize};
/// 默认 STUN 端口
pub const DEFAULT_STUN_PORT: u16 = 3478;
/// 默认 TURN 端口
pub const DEFAULT_TURN_PORT: u16 = 3479;
/// 默认 TURN 用户名
pub const DEFAULT_TURN_USERNAME: &str = "easyremote";
/// 默认 TURN 密码
pub const DEFAULT_TURN_PASSWORD: &str = "easyremote123";
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
/// 服务器主机
pub host: String,
/// 服务器端口
pub port: u16,
/// STUN 服务器端口
pub stun_port: u16,
/// TURN 服务器端口
pub turn_port: u16,
/// 公网 IP用于生成 STUN/TURN URL
pub public_ip: Option<String>,
/// 数据库URL
pub database_url: String,
/// JWT密钥
pub jwt_secret: String,
/// JWT过期时间(秒)
pub jwt_expiry: i64,
/// STUN服务器地址列表
pub stun_servers: Vec<String>,
/// 是否启用本地 STUN 服务
pub enable_local_stun: bool,
/// 是否启用本地 TURN 服务
pub enable_local_turn: bool,
/// TURN服务器地址如果不使用本地
pub turn_server: Option<String>,
/// TURN用户名
pub turn_username: String,
/// TURN密码
pub turn_password: String,
/// TURN Realm
pub turn_realm: String,
}
/// ICE 服务器配置(返回给客户端)
#[derive(Debug, Clone, Serialize)]
pub struct IceServersConfig {
pub stun_servers: Vec<String>,
pub turn_server: Option<TurnConfig>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TurnConfig {
pub url: String,
pub username: String,
pub credential: String,
}
impl Config {
pub fn from_env() -> Result<Self> {
let enable_local_stun = std::env::var("ENABLE_LOCAL_STUN")
.unwrap_or_else(|_| "true".to_string())
.parse()
.unwrap_or(true);
let enable_local_turn = std::env::var("ENABLE_LOCAL_TURN")
.unwrap_or_else(|_| "true".to_string())
.parse()
.unwrap_or(true);
let stun_port: u16 = std::env::var("STUN_PORT")
.unwrap_or_else(|_| DEFAULT_STUN_PORT.to_string())
.parse()
.unwrap_or(DEFAULT_STUN_PORT);
let turn_port: u16 = std::env::var("TURN_PORT")
.unwrap_or_else(|_| DEFAULT_TURN_PORT.to_string())
.parse()
.unwrap_or(DEFAULT_TURN_PORT);
let public_ip = std::env::var("PUBLIC_IP").ok();
let turn_username = std::env::var("TURN_USERNAME")
.unwrap_or_else(|_| DEFAULT_TURN_USERNAME.to_string());
let turn_password = std::env::var("TURN_PASSWORD")
.unwrap_or_else(|_| DEFAULT_TURN_PASSWORD.to_string());
let turn_realm = std::env::var("TURN_REALM")
.unwrap_or_else(|_| "easyremote".to_string());
// 构建 STUN 服务器列表
let mut stun_servers: Vec<String> = Vec::new();
// 如果启用本地 STUN总是添加到列表最前面
if enable_local_stun {
let host = public_ip.as_deref().unwrap_or("localhost");
stun_servers.push(format!("stun:{}:{}", host, stun_port));
}
// 如果设置了额外的 STUN 服务器,添加到列表
if let Ok(extra_stun) = std::env::var("STUN_SERVERS") {
for server in extra_stun.split(',') {
let s = server.trim().to_string();
if !s.is_empty() && !stun_servers.contains(&s) {
stun_servers.push(s);
}
}
}
// 如果列表为空,添加 Google STUN 作为备用
if stun_servers.is_empty() {
stun_servers.push("stun:stun.l.google.com:19302".to_string());
stun_servers.push("stun:stun1.l.google.com:19302".to_string());
}
// TURN 服务器配置
let turn_server = if enable_local_turn {
None // 使用本地 TURNURL 在 get_ice_servers 中动态生成
} else {
std::env::var("TURN_SERVER").ok()
};
Ok(Self {
host: std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
port: std::env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse()?,
stun_port,
turn_port,
public_ip,
database_url: std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "sqlite:easyremote.db?mode=rwc".to_string()),
jwt_secret: std::env::var("JWT_SECRET")
.unwrap_or_else(|_| "your-super-secret-jwt-key-change-in-production".to_string()),
jwt_expiry: std::env::var("JWT_EXPIRY")
.unwrap_or_else(|_| "86400".to_string())
.parse()?,
stun_servers,
enable_local_stun,
enable_local_turn,
turn_server,
turn_username,
turn_password,
turn_realm,
})
}
/// 获取 ICE 服务器配置
pub fn get_ice_servers(&self) -> IceServersConfig {
let turn_server = if self.enable_local_turn {
// 使用本地 TURN 服务器
let host = self.public_ip.as_deref().unwrap_or("localhost");
Some(TurnConfig {
url: format!("turn:{}:{}", host, self.turn_port),
username: self.turn_username.clone(),
credential: self.turn_password.clone(),
})
} else if let Some(url) = &self.turn_server {
// 使用外部 TURN 服务器
Some(TurnConfig {
url: url.clone(),
username: self.turn_username.clone(),
credential: self.turn_password.clone(),
})
} else {
None
};
IceServersConfig {
stun_servers: self.stun_servers.clone(),
turn_server,
}
}
}

120
crates/server/src/db.rs Normal file
View File

@ -0,0 +1,120 @@
//! 数据库管理
use anyhow::Result;
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite};
pub type DbPool = Pool<Sqlite>;
#[derive(Clone)]
pub struct Database {
pub pool: DbPool,
}
impl Database {
pub async fn new(database_url: &str) -> Result<Self> {
let pool = SqlitePoolOptions::new()
.max_connections(10)
.connect(database_url)
.await?;
Ok(Self { pool })
}
pub async fn migrate(&self) -> Result<()> {
// 创建用户表
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
email TEXT,
role TEXT NOT NULL DEFAULT 'user',
created_at TEXT NOT NULL,
last_login TEXT
)
"#,
)
.execute(&self.pool)
.await?;
// 创建设备表
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS devices (
id TEXT PRIMARY KEY,
device_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
os_type TEXT NOT NULL,
os_version TEXT NOT NULL,
user_id TEXT,
is_online INTEGER NOT NULL DEFAULT 0,
allow_remote INTEGER NOT NULL DEFAULT 0,
verification_code TEXT,
last_seen TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
)
"#,
)
.execute(&self.pool)
.await?;
// 创建会话表
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
controller_device_id TEXT NOT NULL,
controlled_device_id TEXT NOT NULL,
started_at TEXT NOT NULL,
ended_at TEXT,
connection_type TEXT NOT NULL,
status TEXT NOT NULL,
FOREIGN KEY (controller_device_id) REFERENCES devices(device_id),
FOREIGN KEY (controlled_device_id) REFERENCES devices(device_id)
)
"#,
)
.execute(&self.pool)
.await?;
// 创建控制历史表
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS control_history (
id TEXT PRIMARY KEY,
user_id TEXT,
controller_device_id TEXT NOT NULL,
controlled_device_id TEXT NOT NULL,
controlled_device_name TEXT NOT NULL,
started_at TEXT NOT NULL,
ended_at TEXT,
duration_seconds INTEGER,
connection_type TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
)
"#,
)
.execute(&self.pool)
.await?;
// 创建索引
sqlx::query("CREATE INDEX IF NOT EXISTS idx_devices_user_id ON devices(user_id)")
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_devices_device_id ON devices(device_id)")
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_sessions_controller ON sessions(controller_device_id)")
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_history_user_id ON control_history(user_id)")
.execute(&self.pool)
.await?;
tracing::info!("Database migrations completed");
Ok(())
}
}

View File

@ -0,0 +1,275 @@
//! 管理员处理器
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use serde::Serialize;
use std::sync::Arc;
use crate::models::{
ApiResponse, DeviceResponse, PaginatedResponse, PaginationParams, SessionResponse,
StatsResponse, UserResponse,
};
use crate::services::{
auth::UserRepository,
device::DeviceRepository,
session::SessionRepository,
AppState,
};
use super::{api_error, AdminUser};
/// 服务器配置响应
#[derive(Debug, Serialize)]
pub struct ServerConfigResponse {
pub version: String,
pub http_port: u16,
pub stun_port: u16,
pub stun_enabled: bool,
pub stun_servers: Vec<String>,
pub turn_port: u16,
pub turn_enabled: bool,
pub turn_server: Option<String>,
pub turn_username: String,
pub turn_realm: String,
pub jwt_expiry_hours: i64,
pub database_type: String,
}
/// 获取所有用户
pub async fn list_users(
State(state): State<Arc<AppState>>,
_admin: AdminUser,
Query(params): Query<PaginationParams>,
) -> impl IntoResponse {
let repo = UserRepository::new(&state.db);
match repo.find_all(params.offset(), params.limit()).await {
Ok((users, total)) => {
let items: Vec<UserResponse> = users.into_iter().map(Into::into).collect();
let total_pages = ((total as f64) / (params.limit() as f64)).ceil() as u32;
let response = PaginatedResponse {
items,
total,
page: params.page.unwrap_or(1),
limit: params.limit(),
total_pages,
};
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 删除用户
pub async fn delete_user(
State(state): State<Arc<AppState>>,
_admin: AdminUser,
Path(user_id): Path<String>,
) -> impl IntoResponse {
let repo = UserRepository::new(&state.db);
match repo.delete(&user_id).await {
Ok(_) => (StatusCode::OK, Json(ApiResponse::ok("用户已删除"))).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 获取所有设备(包含绑定用户信息)
pub async fn list_all_devices(
State(state): State<Arc<AppState>>,
_admin: AdminUser,
Query(params): Query<PaginationParams>,
) -> impl IntoResponse {
let repo = DeviceRepository::new(&state.db);
match repo.find_all_with_username(params.offset(), params.limit()).await {
Ok((devices, total)) => {
let items: Vec<DeviceResponse> = devices.into_iter().map(Into::into).collect();
let total_pages = ((total as f64) / (params.limit() as f64)).ceil() as u32;
let response = PaginatedResponse {
items,
total,
page: params.page.unwrap_or(1),
limit: params.limit(),
total_pages,
};
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 获取所有会话
pub async fn list_all_sessions(
State(state): State<Arc<AppState>>,
_admin: AdminUser,
Query(params): Query<PaginationParams>,
) -> impl IntoResponse {
let repo = SessionRepository::new(&state.db);
match repo.find_all(params.offset(), params.limit()).await {
Ok((sessions, total)) => {
let items: Vec<SessionResponse> = sessions.into_iter().map(Into::into).collect();
let total_pages = ((total as f64) / (params.limit() as f64)).ceil() as u32;
let response = PaginatedResponse {
items,
total,
page: params.page.unwrap_or(1),
limit: params.limit(),
total_pages,
};
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 获取统计信息
pub async fn get_stats(
State(state): State<Arc<AppState>>,
_admin: AdminUser,
) -> impl IntoResponse {
let user_repo = UserRepository::new(&state.db);
let device_repo = DeviceRepository::new(&state.db);
let session_repo = SessionRepository::new(&state.db);
let total_users = user_repo.find_all(0, 1).await.map(|(_, t)| t).unwrap_or(0);
let (_, total_devices) = device_repo.find_all(0, 1).await.unwrap_or((vec![], 0));
let online_devices = device_repo.count_online().await.unwrap_or(0);
let active_sessions = session_repo.count_active().await.unwrap_or(0);
let (_, total_sessions) = session_repo.find_all(0, 1).await.unwrap_or((vec![], 0));
let response = StatsResponse {
total_users,
total_devices,
online_devices,
active_sessions,
total_sessions,
};
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
/// 获取服务器配置
pub async fn get_server_config(
State(state): State<Arc<AppState>>,
_admin: AdminUser,
) -> impl IntoResponse {
let config = &state.config;
// 获取 TURN 服务器地址
let turn_server = if config.enable_local_turn {
let host = config.public_ip.as_deref().unwrap_or("localhost");
Some(format!("turn:{}:{}", host, config.turn_port))
} else {
config.turn_server.clone()
};
let response = ServerConfigResponse {
version: env!("CARGO_PKG_VERSION").to_string(),
http_port: config.port,
stun_port: config.stun_port,
stun_enabled: config.enable_local_stun,
stun_servers: config.stun_servers.clone(),
turn_port: config.turn_port,
turn_enabled: config.enable_local_turn,
turn_server,
turn_username: config.turn_username.clone(),
turn_realm: config.turn_realm.clone(),
jwt_expiry_hours: config.jwt_expiry / 3600,
database_type: "SQLite".to_string(),
};
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
/// 环境配置响应
#[derive(Debug, Serialize)]
pub struct EnvConfigResponse {
pub content: String,
}
/// 环境配置请求
#[derive(Debug, serde::Deserialize)]
pub struct EnvConfigRequest {
pub content: String,
}
/// 获取环境配置文件
pub async fn get_env_config(
_admin: AdminUser,
) -> impl IntoResponse {
let env_path = std::path::Path::new(".env");
let content = if env_path.exists() {
std::fs::read_to_string(env_path).unwrap_or_default()
} else {
// 返回默认配置模板
generate_default_env_config()
};
let response = EnvConfigResponse { content };
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
/// 保存环境配置文件
pub async fn save_env_config(
_admin: AdminUser,
Json(req): Json<EnvConfigRequest>,
) -> impl IntoResponse {
let env_path = std::path::Path::new(".env");
match std::fs::write(env_path, &req.content) {
Ok(_) => {
tracing::info!("Environment config saved");
(StatusCode::OK, Json(ApiResponse::ok("配置已保存"))).into_response()
}
Err(e) => {
tracing::error!("Failed to save env config: {}", e);
api_error(StatusCode::INTERNAL_SERVER_ERROR, format!("保存失败: {}", e))
}
}
}
/// Generate default environment config
fn generate_default_env_config() -> String {
r#"# EasyRemote Server Configuration
# Server Settings
HOST=0.0.0.0
PORT=8080
# STUN Server Settings
ENABLE_LOCAL_STUN=true
STUN_PORT=3478
# TURN Server Settings
ENABLE_LOCAL_TURN=true
TURN_PORT=3479
TURN_USERNAME=easyremote
TURN_PASSWORD=easyremote123
TURN_REALM=easyremote
# Public IP (optional, for generating correct STUN/TURN URLs)
# PUBLIC_IP=your.public.ip
# Database Settings
DATABASE_URL=sqlite:easyremote.db?mode=rwc
# JWT Settings
# JWT_SECRET=your-secret-key-here
JWT_EXPIRY=86400
# Log Level
RUST_LOG=info,tower_http=debug
"#.to_string()
}

View File

@ -0,0 +1,141 @@
//! 认证处理器
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
Json,
};
use std::sync::Arc;
use crate::models::{
ApiResponse, AuthResponse, LoginRequest, RegisterRequest,
};
use crate::services::{
auth::{AuthService, UserRepository},
device::DeviceRepository,
AppState,
};
use super::{api_error, AuthUser};
/// 用户注册
pub async fn register(
State(state): State<Arc<AppState>>,
Json(req): Json<RegisterRequest>,
) -> impl IntoResponse {
// 验证输入
if req.username.len() < 3 || req.username.len() > 50 {
return api_error(StatusCode::BAD_REQUEST, "用户名长度必须在3-50之间");
}
if req.password.len() < 6 {
return api_error(StatusCode::BAD_REQUEST, "密码长度至少6位");
}
let repo = UserRepository::new(&state.db);
// 检查用户名是否已存在
if let Ok(Some(_)) = repo.find_by_username(&req.username).await {
return api_error(StatusCode::CONFLICT, "用户名已存在");
}
// 哈希密码
let (hash, salt) = AuthService::hash_password(&req.password);
// 创建用户
match repo.create(&req.username, &hash, &salt, req.email.as_deref()).await {
Ok(user) => {
let auth_service = AuthService::new(&state.config);
match auth_service.generate_token(&user) {
Ok(token) => (
StatusCode::CREATED,
Json(ApiResponse::ok(AuthResponse {
token,
user: user.into(),
})),
)
.into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 用户登录
pub async fn login(
State(state): State<Arc<AppState>>,
Json(req): Json<LoginRequest>,
) -> impl IntoResponse {
let repo = UserRepository::new(&state.db);
// 查找用户
let user = match repo.find_by_username(&req.username).await {
Ok(Some(user)) => user,
Ok(None) => return api_error(StatusCode::UNAUTHORIZED, "用户名或密码错误"),
Err(e) => return api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
};
// 验证密码
if !AuthService::verify_password(&req.password, &user.password_hash) {
return api_error(StatusCode::UNAUTHORIZED, "用户名或密码错误");
}
// 更新最后登录时间
let _ = repo.update_last_login(&user.id).await;
// 如果提供了设备ID绑定设备到用户
if let Some(device_id) = &req.device_id {
let device_repo = DeviceRepository::new(&state.db);
let _ = device_repo.bind_to_user(device_id, &user.id).await;
}
// 生成令牌
let auth_service = AuthService::new(&state.config);
match auth_service.generate_token(&user) {
Ok(token) => (
StatusCode::OK,
Json(ApiResponse::ok(AuthResponse {
token,
user: user.into(),
})),
)
.into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 退出登录
pub async fn logout(
State(_state): State<Arc<AppState>>,
_user: AuthUser,
) -> impl IntoResponse {
// JWT 是无状态的,客户端只需删除本地令牌
// 如果需要服务端失效令牌,可以使用令牌黑名单
(StatusCode::OK, Json(ApiResponse::ok("已退出登录"))).into_response()
}
/// 刷新令牌
pub async fn refresh_token(
State(state): State<Arc<AppState>>,
user: AuthUser,
) -> impl IntoResponse {
let repo = UserRepository::new(&state.db);
match repo.find_by_id(&user.user_id).await {
Ok(user_row) => {
let auth_service = AuthService::new(&state.config);
match auth_service.generate_token(&user_row) {
Ok(token) => (
StatusCode::OK,
Json(ApiResponse::ok(AuthResponse {
token,
user: user_row.into(),
})),
)
.into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}

View File

@ -0,0 +1,148 @@
//! 设备处理器
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use std::sync::Arc;
use crate::models::{ApiResponse, DeviceRegisterRequest, DeviceResponse};
use crate::services::{device::DeviceRepository, AppState};
use super::{api_error, AuthUser, OptionalAuthUser};
/// 注册设备
pub async fn register_device(
State(state): State<Arc<AppState>>,
OptionalAuthUser(auth_user): OptionalAuthUser,
Json(req): Json<DeviceRegisterRequest>,
) -> impl IntoResponse {
let repo = DeviceRepository::new(&state.db);
match repo.register(&req.device_id, &req.name, &req.os_type, &req.os_version).await {
Ok(device) => {
// 如果用户已登录,绑定设备到用户
if let Some(ref user) = auth_user {
tracing::info!("Binding device {} to user {}", req.device_id, user.user_id);
match repo.bind_to_user(&req.device_id, &user.user_id).await {
Ok(_) => tracing::info!("Device {} bound to user {} successfully", req.device_id, user.user_id),
Err(e) => tracing::error!("Failed to bind device: {}", e),
}
} else {
tracing::info!("Device {} registered without user (no auth token)", req.device_id);
}
// 重新获取设备信息(包含绑定用户)
let updated_device = repo.find_by_device_id(&req.device_id).await
.ok()
.flatten()
.unwrap_or(device);
let response: DeviceResponse = updated_device.into();
(StatusCode::CREATED, Json(ApiResponse::ok(response))).into_response()
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 获取当前用户的设备列表
pub async fn list_devices(
State(state): State<Arc<AppState>>,
user: AuthUser,
) -> impl IntoResponse {
let repo = DeviceRepository::new(&state.db);
match repo.find_by_user(&user.user_id).await {
Ok(devices) => {
let response: Vec<DeviceResponse> = devices.into_iter().map(Into::into).collect();
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 获取单个设备
pub async fn get_device(
State(state): State<Arc<AppState>>,
Path(device_id): Path<String>,
user: AuthUser,
) -> impl IntoResponse {
let repo = DeviceRepository::new(&state.db);
match repo.find_by_device_id(&device_id).await {
Ok(Some(device)) => {
// 验证设备属于当前用户
if device.user_id.as_ref() != Some(&user.user_id) && user.role != "admin" {
return api_error(StatusCode::FORBIDDEN, "无权访问此设备");
}
let response: DeviceResponse = device.into();
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
Ok(None) => api_error(StatusCode::NOT_FOUND, "设备不存在"),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 移除设备
pub async fn remove_device(
State(state): State<Arc<AppState>>,
Path(device_id): Path<String>,
user: AuthUser,
) -> impl IntoResponse {
let repo = DeviceRepository::new(&state.db);
// 验证设备属于当前用户
match repo.find_by_device_id(&device_id).await {
Ok(Some(device)) => {
if device.user_id.as_ref() != Some(&user.user_id) && user.role != "admin" {
return api_error(StatusCode::FORBIDDEN, "无权操作此设备");
}
}
Ok(None) => return api_error(StatusCode::NOT_FOUND, "设备不存在"),
Err(e) => return api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
// 解绑设备
match repo.unbind(&device_id).await {
Ok(_) => (StatusCode::OK, Json(ApiResponse::ok("设备已移除"))).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 强制设备下线
pub async fn force_offline(
State(state): State<Arc<AppState>>,
Path(device_id): Path<String>,
user: AuthUser,
) -> impl IntoResponse {
let repo = DeviceRepository::new(&state.db);
// 验证设备属于当前用户
match repo.find_by_device_id(&device_id).await {
Ok(Some(device)) => {
if device.user_id.as_ref() != Some(&user.user_id) && user.role != "admin" {
return api_error(StatusCode::FORBIDDEN, "无权操作此设备");
}
}
Ok(None) => return api_error(StatusCode::NOT_FOUND, "设备不存在"),
Err(e) => return api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
// 发送下线命令
let message = serde_json::json!({
"type": "force_offline",
"device_id": device_id,
});
if state.send_to_device(&device_id, &message.to_string()).await {
// 更新数据库状态
let _ = repo.set_online(&device_id, false).await;
(StatusCode::OK, Json(ApiResponse::ok("设备已下线"))).into_response()
} else {
// 设备可能已经离线
let _ = repo.set_online(&device_id, false).await;
(StatusCode::OK, Json(ApiResponse::ok("设备已下线"))).into_response()
}
}

View File

@ -0,0 +1,110 @@
//! API 处理器
pub mod auth;
pub mod users;
pub mod devices;
pub mod sessions;
pub mod admin;
pub mod setup;
use axum::{
extract::FromRequestParts,
http::{request::Parts, StatusCode},
response::{IntoResponse, Response},
Json,
};
use crate::models::ApiResponse;
use crate::services::AppState;
use std::sync::Arc;
/// 认证用户提取器
pub struct AuthUser {
pub user_id: String,
pub username: String,
pub role: String,
}
#[axum::async_trait]
impl FromRequestParts<Arc<AppState>> for AuthUser {
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, state: &Arc<AppState>) -> Result<Self, Self::Rejection> {
// 从 Authorization header 获取 token
let auth_header = parts
.headers
.get("Authorization")
.and_then(|v| v.to_str().ok())
.ok_or(AuthError::MissingToken)?;
// 解析 Bearer token
let token = auth_header
.strip_prefix("Bearer ")
.ok_or(AuthError::InvalidToken)?;
// 验证 token
let auth_service = crate::services::auth::AuthService::new(&state.config);
let claims = auth_service
.verify_token(token)
.map_err(|_| AuthError::InvalidToken)?;
Ok(AuthUser {
user_id: claims.sub,
username: claims.username,
role: claims.role,
})
}
}
/// 可选认证用户提取器
pub struct OptionalAuthUser(pub Option<AuthUser>);
#[axum::async_trait]
impl FromRequestParts<Arc<AppState>> for OptionalAuthUser {
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, state: &Arc<AppState>) -> Result<Self, Self::Rejection> {
Ok(OptionalAuthUser(
AuthUser::from_request_parts(parts, state).await.ok()
))
}
}
/// 管理员用户提取器
pub struct AdminUser(pub AuthUser);
#[axum::async_trait]
impl FromRequestParts<Arc<AppState>> for AdminUser {
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, state: &Arc<AppState>) -> Result<Self, Self::Rejection> {
let user = AuthUser::from_request_parts(parts, state).await?;
if user.role != "admin" {
return Err(AuthError::Forbidden);
}
Ok(AdminUser(user))
}
}
/// 认证错误
pub enum AuthError {
MissingToken,
InvalidToken,
Forbidden,
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, message) = match self {
AuthError::MissingToken => (StatusCode::UNAUTHORIZED, "缺少认证令牌"),
AuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "无效的认证令牌"),
AuthError::Forbidden => (StatusCode::FORBIDDEN, "权限不足"),
};
(status, Json(ApiResponse::<()>::err(message))).into_response()
}
}
/// API 错误处理
pub fn api_error(status: StatusCode, message: impl ToString) -> Response {
(status, Json(ApiResponse::<()>::err(message))).into_response()
}

View File

@ -0,0 +1,141 @@
//! 会话处理器
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use std::sync::Arc;
use crate::models::{ApiResponse, HistoryResponse, PaginatedResponse, PaginationParams, SessionResponse};
use crate::services::{
device::DeviceRepository,
session::{HistoryRepository, SessionRepository},
AppState,
};
use super::{api_error, AuthUser};
/// 获取活跃会话列表
pub async fn list_sessions(
State(state): State<Arc<AppState>>,
user: AuthUser,
) -> impl IntoResponse {
let session_repo = SessionRepository::new(&state.db);
let device_repo = DeviceRepository::new(&state.db);
// 获取用户的设备
let devices = match device_repo.find_by_user(&user.user_id).await {
Ok(devices) => devices,
Err(e) => return api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
};
let device_ids: Vec<&str> = devices.iter().map(|d| d.device_id.as_str()).collect();
// 获取活跃会话
match session_repo.find_active().await {
Ok(sessions) => {
// 过滤出与用户设备相关的会话
let filtered: Vec<SessionResponse> = sessions
.into_iter()
.filter(|s| {
device_ids.contains(&s.controller_device_id.as_str())
|| device_ids.contains(&s.controlled_device_id.as_str())
})
.map(Into::into)
.collect();
(StatusCode::OK, Json(ApiResponse::ok(filtered))).into_response()
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 获取控制历史记录
pub async fn get_history(
State(state): State<Arc<AppState>>,
user: AuthUser,
Query(params): Query<PaginationParams>,
) -> impl IntoResponse {
let repo = HistoryRepository::new(&state.db);
match repo.find_by_user(&user.user_id, params.offset(), params.limit()).await {
Ok((history, total)) => {
let items: Vec<HistoryResponse> = history.into_iter().map(Into::into).collect();
let total_pages = ((total as f64) / (params.limit() as f64)).ceil() as u32;
let response = PaginatedResponse {
items,
total,
page: params.page.unwrap_or(1),
limit: params.limit(),
total_pages,
};
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 获取单个会话
pub async fn get_session(
State(state): State<Arc<AppState>>,
Path(session_id): Path<String>,
_user: AuthUser,
) -> impl IntoResponse {
let repo = SessionRepository::new(&state.db);
match repo.find_by_id(&session_id).await {
Ok(session) => {
let response: SessionResponse = session.into();
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
Err(_) => api_error(StatusCode::NOT_FOUND, "会话不存在"),
}
}
/// 结束会话
pub async fn end_session(
State(state): State<Arc<AppState>>,
Path(session_id): Path<String>,
user: AuthUser,
) -> impl IntoResponse {
let session_repo = SessionRepository::new(&state.db);
let device_repo = DeviceRepository::new(&state.db);
// 获取会话
let session = match session_repo.find_by_id(&session_id).await {
Ok(session) => session,
Err(_) => return api_error(StatusCode::NOT_FOUND, "会话不存在"),
};
// 验证用户有权结束会话
let user_devices = match device_repo.find_by_user(&user.user_id).await {
Ok(devices) => devices,
Err(e) => return api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
};
let device_ids: Vec<&str> = user_devices.iter().map(|d| d.device_id.as_str()).collect();
let is_owner = device_ids.contains(&session.controller_device_id.as_str())
|| device_ids.contains(&session.controlled_device_id.as_str());
if !is_owner && user.role != "admin" {
return api_error(StatusCode::FORBIDDEN, "无权结束此会话");
}
// 发送断开连接消息
let message = serde_json::json!({
"type": "session_end",
"session_id": session_id,
});
state.send_to_device(&session.controller_device_id, &message.to_string()).await;
state.send_to_device(&session.controlled_device_id, &message.to_string()).await;
// 更新会话状态
match session_repo.end_session(&session_id).await {
Ok(_) => (StatusCode::OK, Json(ApiResponse::ok("会话已结束"))).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}

View File

@ -0,0 +1,191 @@
//! 初始化配置处理器
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::models::ApiResponse;
use crate::services::{
auth::{AuthService, UserRepository},
AppState,
};
/// 初始化状态响应
#[derive(Debug, Serialize)]
pub struct SetupStatusResponse {
pub need_setup: bool,
}
/// 初始化请求
#[derive(Debug, Deserialize)]
pub struct SetupInitRequest {
pub admin_username: String,
pub admin_password: String,
pub jwt_secret: String,
pub jwt_expiry: i64,
pub stun_servers: String,
pub turn_server: Option<String>,
pub turn_username: Option<String>,
pub turn_password: Option<String>,
}
/// 初始化响应
#[derive(Debug, Serialize)]
pub struct SetupInitResponse {
pub token: String,
pub message: String,
}
/// 检查是否需要初始化
pub async fn check_setup_status(
State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
let repo = UserRepository::new(&state.db);
// 检查是否存在管理员用户
let need_setup = match repo.find_all(0, 1).await {
Ok((users, count)) => count == 0,
Err(_) => true,
};
(
StatusCode::OK,
Json(ApiResponse::ok(SetupStatusResponse { need_setup })),
)
}
/// 执行初始化配置
pub async fn init_setup(
State(state): State<Arc<AppState>>,
Json(req): Json<SetupInitRequest>,
) -> impl IntoResponse {
// 验证输入
if req.admin_username.len() < 3 {
return (
StatusCode::BAD_REQUEST,
Json(ApiResponse::<SetupInitResponse>::err("用户名至少3个字符")),
);
}
if req.admin_password.len() < 6 {
return (
StatusCode::BAD_REQUEST,
Json(ApiResponse::<SetupInitResponse>::err("密码至少6位")),
);
}
let repo = UserRepository::new(&state.db);
// 检查是否已经初始化过
if let Ok((_, count)) = repo.find_all(0, 1).await {
if count > 0 {
return (
StatusCode::BAD_REQUEST,
Json(ApiResponse::<SetupInitResponse>::err("系统已初始化,请直接登录")),
);
}
}
// 创建管理员用户
let (hash, salt) = AuthService::hash_password(&req.admin_password);
// 先创建普通用户
let user = match repo.create(&req.admin_username, &hash, &salt, None).await {
Ok(user) => user,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse::<SetupInitResponse>::err(format!("创建用户失败: {}", e))),
);
}
};
// 将用户设置为管理员
if let Err(e) = repo.set_role(&user.id, "admin").await {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse::<SetupInitResponse>::err(format!("设置管理员失败: {}", e))),
);
}
// 重新获取用户信息
let admin_user = match repo.find_by_id(&user.id).await {
Ok(user) => user,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse::<SetupInitResponse>::err(format!("获取用户失败: {}", e))),
);
}
};
// 生成令牌
let auth_service = AuthService::new(&state.config);
let token = match auth_service.generate_token(&admin_user) {
Ok(token) => token,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse::<SetupInitResponse>::err(format!("生成令牌失败: {}", e))),
);
}
};
// 保存配置到文件(可选)
save_env_config(&req);
(
StatusCode::OK,
Json(ApiResponse::ok(SetupInitResponse {
token,
message: "初始化成功!配置已保存到 .env 文件,部分配置需要重启服务后生效。".to_string(),
})),
)
}
/// 保存配置到 .env 文件
fn save_env_config(req: &SetupInitRequest) {
let env_content = format!(
r#"# EasyRemote 服务端配置(自动生成)
#
HOST=0.0.0.0
PORT=8080
#
DATABASE_URL=sqlite:easyremote.db?mode=rwc
# JWT
JWT_SECRET={}
JWT_EXPIRY={}
# STUN
STUN_SERVERS={}
# TURN
{}
{}
{}
#
RUST_LOG=info,tower_http=debug
"#,
req.jwt_secret,
req.jwt_expiry,
req.stun_servers,
req.turn_server.as_ref().map(|s| format!("TURN_SERVER={}", s)).unwrap_or_else(|| "# TURN_SERVER=".to_string()),
req.turn_username.as_ref().map(|s| format!("TURN_USERNAME={}", s)).unwrap_or_else(|| "# TURN_USERNAME=".to_string()),
req.turn_password.as_ref().map(|s| format!("TURN_PASSWORD={}", s)).unwrap_or_else(|| "# TURN_PASSWORD=".to_string()),
);
// 保存到 .env 文件
if let Err(e) = std::fs::write(".env", env_content) {
tracing::warn!("无法保存 .env 文件: {}", e);
} else {
tracing::info!("配置已保存到 .env 文件");
}
}

View File

@ -0,0 +1,57 @@
//! 用户处理器
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
Json,
};
use std::sync::Arc;
use crate::models::{ApiResponse, UpdateUserRequest, UserResponse};
use crate::services::{auth::{AuthService, UserRepository}, AppState};
use super::{api_error, AuthUser};
/// 获取当前用户信息
pub async fn get_current_user(
State(state): State<Arc<AppState>>,
user: AuthUser,
) -> impl IntoResponse {
let repo = UserRepository::new(&state.db);
match repo.find_by_id(&user.user_id).await {
Ok(user_row) => {
let response: UserResponse = user_row.into();
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
/// 更新用户信息
pub async fn update_user(
State(state): State<Arc<AppState>>,
user: AuthUser,
Json(req): Json<UpdateUserRequest>,
) -> impl IntoResponse {
let repo = UserRepository::new(&state.db);
// 如果要更新密码,先哈希
let password_hash = req.password.as_ref().map(|p| {
let (hash, _) = AuthService::hash_password(p);
hash
});
match repo.update(&user.user_id, req.email.as_deref(), password_hash.as_deref()).await {
Ok(_) => {
match repo.find_by_id(&user.user_id).await {
Ok(user_row) => {
let response: UserResponse = user_row.into();
(StatusCode::OK, Json(ApiResponse::ok(response))).into_response()
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}

157
crates/server/src/main.rs Normal file
View File

@ -0,0 +1,157 @@
//! EasyRemote 服务端主入口
mod config;
mod db;
mod handlers;
mod models;
mod services;
mod stun_server;
mod turn_server;
mod websocket;
use axum::{
routing::{get, post, delete},
Router, Json, extract::State,
};
use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use crate::config::{Config, IceServersConfig};
use crate::db::Database;
use crate::services::AppState;
use crate::stun_server::StunServer;
use crate::turn_server::TurnServer;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 初始化日志
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info,tower_http=debug".into()),
))
.with(tracing_subscriber::fmt::layer())
.init();
// 加载配置
dotenvy::dotenv().ok();
let config = Config::from_env()?;
tracing::info!("Starting EasyRemote Server on {}:{}", config.host, config.port);
// 启动本地 STUN 服务
if config.enable_local_stun {
let stun_port = config.stun_port;
tokio::spawn(async move {
let stun_server = StunServer::new(stun_port);
if let Err(e) = stun_server.start().await {
tracing::error!("STUN server error: {}", e);
}
});
tracing::info!("Local STUN server enabled on port {}", config.stun_port);
}
// 启动本地 TURN 服务
if config.enable_local_turn {
let turn_port = config.turn_port;
let turn_realm = config.turn_realm.clone();
let turn_username = config.turn_username.clone();
let turn_password = config.turn_password.clone();
tokio::spawn(async move {
let turn_server = TurnServer::new(turn_port, turn_realm, turn_username, turn_password);
if let Err(e) = turn_server.start().await {
tracing::error!("TURN server error: {}", e);
}
});
tracing::info!("Local TURN server enabled on port {}", config.turn_port);
}
// 初始化数据库
let db = Database::new(&config.database_url).await?;
db.migrate().await?;
// 创建应用状态
let state = Arc::new(AppState::new(db, config.clone()));
// 启动时将所有设备设为离线(清理上次异常退出的状态)
{
let device_repo = crate::services::device::DeviceRepository::new(&state.db);
match device_repo.set_all_offline().await {
Ok(count) => {
if count > 0 {
tracing::info!("Reset {} devices to offline status", count);
}
}
Err(e) => {
tracing::warn!("Failed to reset device status: {}", e);
}
}
}
// 构建路由
let app = Router::new()
// 初始化配置路由
.route("/api/setup/status", get(handlers::setup::check_setup_status))
.route("/api/setup/init", post(handlers::setup::init_setup))
// 认证路由
.route("/api/auth/register", post(handlers::auth::register))
.route("/api/auth/login", post(handlers::auth::login))
.route("/api/auth/logout", post(handlers::auth::logout))
.route("/api/auth/refresh", post(handlers::auth::refresh_token))
// 用户路由
.route("/api/users/me", get(handlers::users::get_current_user))
.route("/api/users/me", post(handlers::users::update_user))
// 设备路由
.route("/api/devices", get(handlers::devices::list_devices))
.route("/api/devices/:id", get(handlers::devices::get_device))
.route("/api/devices/:id", delete(handlers::devices::remove_device))
.route("/api/devices/:id/offline", post(handlers::devices::force_offline))
.route("/api/devices/register", post(handlers::devices::register_device))
// ICE 服务器配置
.route("/api/ice-servers", get(get_ice_servers))
// 会话路由
.route("/api/sessions", get(handlers::sessions::list_sessions))
.route("/api/sessions/history", get(handlers::sessions::get_history))
.route("/api/sessions/:id", get(handlers::sessions::get_session))
.route("/api/sessions/:id/end", post(handlers::sessions::end_session))
// 管理员路由
.route("/api/admin/users", get(handlers::admin::list_users))
.route("/api/admin/users/:id", delete(handlers::admin::delete_user))
.route("/api/admin/devices", get(handlers::admin::list_all_devices))
.route("/api/admin/sessions", get(handlers::admin::list_all_sessions))
.route("/api/admin/stats", get(handlers::admin::get_stats))
.route("/api/admin/config", get(handlers::admin::get_server_config))
.route("/api/admin/env-config", get(handlers::admin::get_env_config))
.route("/api/admin/env-config", post(handlers::admin::save_env_config))
// WebSocket 信令
.route("/ws/signal", get(websocket::signal_handler))
// WebSocket 远程控制 (浏览器)
.route("/ws/remote/:device_id", get(websocket::remote_handler))
// 静态文件服务 (管理后台)
.nest_service("/", tower_http::services::ServeDir::new("static"))
// 中间件
.layer(CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any))
.layer(TraceLayer::new_for_http())
.with_state(state);
// 打印 STUN 配置信息
tracing::info!("ICE servers: {:?}", config.stun_servers);
// 启动服务器
let addr = format!("{}:{}", config.host, config.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("Server listening on {}", addr);
axum::serve(listener, app).await?;
Ok(())
}
/// 获取 ICE 服务器配置 API
async fn get_ice_servers(State(state): State<Arc<AppState>>) -> Json<IceServersConfig> {
Json(state.config.get_ice_servers())
}

217
crates/server/src/models.rs Normal file
View File

@ -0,0 +1,217 @@
//! 数据模型
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
/// 数据库用户模型
#[derive(Debug, Clone, FromRow, Serialize)]
pub struct UserRow {
pub id: String,
pub username: String,
#[serde(skip_serializing)]
pub password_hash: String,
#[serde(skip_serializing)]
pub salt: String,
pub email: Option<String>,
pub role: String,
pub created_at: String,
pub last_login: Option<String>,
}
/// 数据库设备模型
#[derive(Debug, Clone, FromRow, Serialize)]
pub struct DeviceRow {
pub id: String,
pub device_id: String,
pub name: String,
pub os_type: String,
pub os_version: String,
pub user_id: Option<String>,
pub is_online: bool,
pub allow_remote: bool,
pub verification_code: Option<String>,
pub last_seen: String,
pub created_at: String,
}
/// 数据库会话模型
#[derive(Debug, Clone, FromRow, Serialize)]
pub struct SessionRow {
pub id: String,
pub controller_device_id: String,
pub controlled_device_id: String,
pub started_at: String,
pub ended_at: Option<String>,
pub connection_type: String,
pub status: String,
}
/// 数据库控制历史模型
#[derive(Debug, Clone, FromRow, Serialize)]
pub struct HistoryRow {
pub id: String,
pub user_id: Option<String>,
pub controller_device_id: String,
pub controlled_device_id: String,
pub controlled_device_name: String,
pub started_at: String,
pub ended_at: Option<String>,
pub duration_seconds: Option<i64>,
pub connection_type: String,
}
// ==================== API 请求/响应模型 ====================
/// 注册请求
#[derive(Debug, Deserialize)]
pub struct RegisterRequest {
pub username: String,
pub password: String,
pub email: Option<String>,
}
/// 登录请求
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
pub device_id: Option<String>,
}
/// 认证响应
#[derive(Debug, Serialize)]
pub struct AuthResponse {
pub token: String,
pub user: UserResponse,
}
/// 用户响应
#[derive(Debug, Serialize)]
pub struct UserResponse {
pub id: String,
pub username: String,
pub email: Option<String>,
pub role: String,
pub created_at: String,
}
/// 设备注册请求
#[derive(Debug, Deserialize)]
pub struct DeviceRegisterRequest {
pub device_id: String,
pub name: String,
pub os_type: String,
pub os_version: String,
}
/// 设备响应
#[derive(Debug, Serialize)]
pub struct DeviceResponse {
pub id: String,
pub device_id: String,
pub name: String,
pub os_type: String,
pub os_version: String,
pub is_online: bool,
pub allow_remote: bool,
pub last_seen: String,
pub user_id: Option<String>,
pub username: Option<String>,
}
/// 会话响应
#[derive(Debug, Serialize)]
pub struct SessionResponse {
pub id: String,
pub controller_device_id: String,
pub controlled_device_id: String,
pub started_at: String,
pub ended_at: Option<String>,
pub connection_type: String,
pub status: String,
}
/// 历史记录响应
#[derive(Debug, Serialize)]
pub struct HistoryResponse {
pub id: String,
pub controller_device_id: String,
pub controlled_device_id: String,
pub controlled_device_name: String,
pub started_at: String,
pub ended_at: Option<String>,
pub duration: Option<String>,
pub connection_type: String,
}
/// 统计响应
#[derive(Debug, Serialize)]
pub struct StatsResponse {
pub total_users: i64,
pub total_devices: i64,
pub online_devices: i64,
pub active_sessions: i64,
pub total_sessions: i64,
}
/// 更新用户请求
#[derive(Debug, Deserialize)]
pub struct UpdateUserRequest {
pub email: Option<String>,
pub password: Option<String>,
}
/// 通用API响应
#[derive(Debug, Serialize)]
pub struct ApiResponse<T> {
pub success: bool,
pub data: Option<T>,
pub error: Option<String>,
}
impl<T> ApiResponse<T> {
pub fn ok(data: T) -> Self {
Self {
success: true,
data: Some(data),
error: None,
}
}
pub fn err(error: impl ToString) -> Self {
Self {
success: false,
data: None,
error: Some(error.to_string()),
}
}
}
/// 分页请求参数
#[derive(Debug, Deserialize)]
pub struct PaginationParams {
pub page: Option<u32>,
pub limit: Option<u32>,
}
impl PaginationParams {
pub fn offset(&self) -> u32 {
let page = self.page.unwrap_or(1).max(1);
let limit = self.limit();
(page - 1) * limit
}
pub fn limit(&self) -> u32 {
self.limit.unwrap_or(20).min(100)
}
}
/// 分页响应
#[derive(Debug, Serialize)]
pub struct PaginatedResponse<T> {
pub items: Vec<T>,
pub total: i64,
pub page: u32,
pub limit: u32,
pub total_pages: u32,
}

View File

@ -0,0 +1,252 @@
//! 认证服务
use crate::config::Config;
use crate::db::Database;
use crate::models::{UserRow, UserResponse};
use anyhow::Result;
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// JWT Claims
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // 用户ID
pub username: String, // 用户名
pub role: String, // 角色
pub exp: i64, // 过期时间
pub iat: i64, // 签发时间
}
/// 认证服务
pub struct AuthService {
jwt_secret: String,
jwt_expiry: i64,
}
impl AuthService {
pub fn new(config: &Config) -> Self {
Self {
jwt_secret: config.jwt_secret.clone(),
jwt_expiry: config.jwt_expiry,
}
}
/// 生成JWT令牌
pub fn generate_token(&self, user: &UserRow) -> Result<String> {
let now = Utc::now();
let exp = now + Duration::seconds(self.jwt_expiry);
let claims = Claims {
sub: user.id.clone(),
username: user.username.clone(),
role: user.role.clone(),
exp: exp.timestamp(),
iat: now.timestamp(),
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(self.jwt_secret.as_bytes()),
)?;
Ok(token)
}
/// 验证JWT令牌
pub fn verify_token(&self, token: &str) -> Result<Claims> {
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(self.jwt_secret.as_bytes()),
&Validation::default(),
)?;
Ok(token_data.claims)
}
/// 哈希密码
pub fn hash_password(password: &str) -> (String, String) {
use argon2::{
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
Argon2,
};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.expect("Failed to hash password")
.to_string();
(hash, salt.to_string())
}
/// 验证密码
pub fn verify_password(password: &str, hash: &str) -> bool {
use argon2::{
password_hash::{PasswordHash, PasswordVerifier},
Argon2,
};
let parsed_hash = match PasswordHash::new(hash) {
Ok(h) => h,
Err(_) => return false,
};
Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok()
}
}
/// 用户仓储
pub struct UserRepository<'a> {
db: &'a Database,
}
impl<'a> UserRepository<'a> {
pub fn new(db: &'a Database) -> Self {
Self { db }
}
/// 创建用户
pub async fn create(&self, username: &str, password_hash: &str, salt: &str, email: Option<&str>) -> Result<UserRow> {
let id = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
sqlx::query(
r#"
INSERT INTO users (id, username, password_hash, salt, email, role, created_at)
VALUES (?, ?, ?, ?, ?, 'user', ?)
"#,
)
.bind(&id)
.bind(username)
.bind(password_hash)
.bind(salt)
.bind(email)
.bind(&now)
.execute(&self.db.pool)
.await?;
self.find_by_id(&id).await
}
/// 根据ID查找用户
pub async fn find_by_id(&self, id: &str) -> Result<UserRow> {
let user = sqlx::query_as::<_, UserRow>("SELECT * FROM users WHERE id = ?")
.bind(id)
.fetch_one(&self.db.pool)
.await?;
Ok(user)
}
/// 根据用户名查找用户
pub async fn find_by_username(&self, username: &str) -> Result<Option<UserRow>> {
let user = sqlx::query_as::<_, UserRow>("SELECT * FROM users WHERE username = ?")
.bind(username)
.fetch_optional(&self.db.pool)
.await?;
Ok(user)
}
/// 更新最后登录时间
pub async fn update_last_login(&self, id: &str) -> Result<()> {
let now = Utc::now().to_rfc3339();
sqlx::query("UPDATE users SET last_login = ? WHERE id = ?")
.bind(&now)
.bind(id)
.execute(&self.db.pool)
.await?;
Ok(())
}
/// 更新用户信息
pub async fn update(&self, id: &str, email: Option<&str>, password_hash: Option<&str>) -> Result<()> {
if let Some(email) = email {
sqlx::query("UPDATE users SET email = ? WHERE id = ?")
.bind(email)
.bind(id)
.execute(&self.db.pool)
.await?;
}
if let Some(hash) = password_hash {
sqlx::query("UPDATE users SET password_hash = ? WHERE id = ?")
.bind(hash)
.bind(id)
.execute(&self.db.pool)
.await?;
}
Ok(())
}
/// 设置用户角色
pub async fn set_role(&self, id: &str, role: &str) -> Result<()> {
sqlx::query("UPDATE users SET role = ? WHERE id = ?")
.bind(role)
.bind(id)
.execute(&self.db.pool)
.await?;
Ok(())
}
/// 获取所有用户
pub async fn find_all(&self, offset: u32, limit: u32) -> Result<(Vec<UserRow>, i64)> {
let users = sqlx::query_as::<_, UserRow>(
"SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?",
)
.bind(limit)
.bind(offset)
.fetch_all(&self.db.pool)
.await?;
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
.fetch_one(&self.db.pool)
.await?;
Ok((users, count.0))
}
/// 删除用户
pub async fn delete(&self, id: &str) -> Result<()> {
// 先解除用户与设备的绑定
sqlx::query("UPDATE devices SET user_id = NULL WHERE user_id = ?")
.bind(id)
.execute(&self.db.pool)
.await?;
// 删除用户的控制历史记录
sqlx::query("DELETE FROM control_history WHERE user_id = ?")
.bind(id)
.execute(&self.db.pool)
.await?;
// 最后删除用户
sqlx::query("DELETE FROM users WHERE id = ?")
.bind(id)
.execute(&self.db.pool)
.await?;
Ok(())
}
}
impl From<UserRow> for UserResponse {
fn from(user: UserRow) -> Self {
Self {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
created_at: user.created_at,
}
}
}

View File

@ -0,0 +1,303 @@
//! 设备服务
use crate::db::Database;
use crate::models::{DeviceRow, DeviceResponse};
use anyhow::Result;
use chrono::Utc;
use uuid::Uuid;
/// 设备仓储
pub struct DeviceRepository<'a> {
db: &'a Database,
}
impl<'a> DeviceRepository<'a> {
pub fn new(db: &'a Database) -> Self {
Self { db }
}
/// 注册设备
pub async fn register(
&self,
device_id: &str,
name: &str,
os_type: &str,
os_version: &str,
) -> Result<DeviceRow> {
// 检查设备是否已存在
if let Some(existing) = self.find_by_device_id(device_id).await? {
// 更新设备信息
self.update_last_seen(device_id).await?;
return Ok(existing);
}
let id = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
let verification_code = generate_verification_code();
sqlx::query(
r#"
INSERT INTO devices (id, device_id, name, os_type, os_version, verification_code, last_seen, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(&id)
.bind(device_id)
.bind(name)
.bind(os_type)
.bind(os_version)
.bind(&verification_code)
.bind(&now)
.bind(&now)
.execute(&self.db.pool)
.await?;
self.find_by_id(&id).await
}
/// 根据ID查找设备
pub async fn find_by_id(&self, id: &str) -> Result<DeviceRow> {
let device = sqlx::query_as::<_, DeviceRow>("SELECT * FROM devices WHERE id = ?")
.bind(id)
.fetch_one(&self.db.pool)
.await?;
Ok(device)
}
/// 根据设备ID查找设备
pub async fn find_by_device_id(&self, device_id: &str) -> Result<Option<DeviceRow>> {
let device = sqlx::query_as::<_, DeviceRow>("SELECT * FROM devices WHERE device_id = ?")
.bind(device_id)
.fetch_optional(&self.db.pool)
.await?;
Ok(device)
}
/// 获取用户的所有设备
pub async fn find_by_user(&self, user_id: &str) -> Result<Vec<DeviceRow>> {
let devices = sqlx::query_as::<_, DeviceRow>(
"SELECT * FROM devices WHERE user_id = ? ORDER BY last_seen DESC",
)
.bind(user_id)
.fetch_all(&self.db.pool)
.await?;
Ok(devices)
}
/// 绑定设备到用户
pub async fn bind_to_user(&self, device_id: &str, user_id: &str) -> Result<()> {
sqlx::query("UPDATE devices SET user_id = ? WHERE device_id = ?")
.bind(user_id)
.bind(device_id)
.execute(&self.db.pool)
.await?;
Ok(())
}
/// 解绑设备
pub async fn unbind(&self, device_id: &str) -> Result<()> {
sqlx::query("UPDATE devices SET user_id = NULL WHERE device_id = ?")
.bind(device_id)
.execute(&self.db.pool)
.await?;
Ok(())
}
/// 更新设备在线状态
pub async fn set_online(&self, device_id: &str, online: bool) -> Result<()> {
let now = Utc::now().to_rfc3339();
sqlx::query("UPDATE devices SET is_online = ?, last_seen = ? WHERE device_id = ?")
.bind(online)
.bind(&now)
.bind(device_id)
.execute(&self.db.pool)
.await?;
Ok(())
}
/// 将所有设备设为离线(服务器启动时调用)
pub async fn set_all_offline(&self) -> Result<u64> {
let result = sqlx::query("UPDATE devices SET is_online = false WHERE is_online = true")
.execute(&self.db.pool)
.await?;
Ok(result.rows_affected())
}
/// 更新设备允许远程状态
pub async fn set_allow_remote(&self, device_id: &str, allow: bool) -> Result<()> {
sqlx::query("UPDATE devices SET allow_remote = ? WHERE device_id = ?")
.bind(allow)
.bind(device_id)
.execute(&self.db.pool)
.await?;
Ok(())
}
/// 更新验证码
pub async fn update_verification_code(&self, device_id: &str) -> Result<String> {
let code = generate_verification_code();
sqlx::query("UPDATE devices SET verification_code = ? WHERE device_id = ?")
.bind(&code)
.bind(device_id)
.execute(&self.db.pool)
.await?;
Ok(code)
}
/// 验证设备验证码
pub async fn verify_code(&self, device_id: &str, code: &str) -> Result<bool> {
let device = self.find_by_device_id(device_id).await?;
if let Some(device) = device {
Ok(device.verification_code.as_deref() == Some(code))
} else {
Ok(false)
}
}
/// 更新最后访问时间
pub async fn update_last_seen(&self, device_id: &str) -> Result<()> {
let now = Utc::now().to_rfc3339();
sqlx::query("UPDATE devices SET last_seen = ? WHERE device_id = ?")
.bind(&now)
.bind(device_id)
.execute(&self.db.pool)
.await?;
Ok(())
}
/// 获取所有设备
pub async fn find_all(&self, offset: u32, limit: u32) -> Result<(Vec<DeviceRow>, i64)> {
let devices = sqlx::query_as::<_, DeviceRow>(
"SELECT * FROM devices ORDER BY last_seen DESC LIMIT ? OFFSET ?",
)
.bind(limit)
.bind(offset)
.fetch_all(&self.db.pool)
.await?;
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM devices")
.fetch_one(&self.db.pool)
.await?;
Ok((devices, count.0))
}
/// 获取所有设备(包含用户名)
pub async fn find_all_with_username(&self, offset: u32, limit: u32) -> Result<(Vec<DeviceWithUsername>, i64)> {
let devices = sqlx::query_as::<_, DeviceWithUsername>(
r#"
SELECT d.*, u.username
FROM devices d
LEFT JOIN users u ON d.user_id = u.id
ORDER BY d.last_seen DESC
LIMIT ? OFFSET ?
"#,
)
.bind(limit)
.bind(offset)
.fetch_all(&self.db.pool)
.await?;
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM devices")
.fetch_one(&self.db.pool)
.await?;
Ok((devices, count.0))
}
/// 获取在线设备数量
pub async fn count_online(&self) -> Result<i64> {
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM devices WHERE is_online = 1")
.fetch_one(&self.db.pool)
.await?;
Ok(count.0)
}
/// 删除设备
pub async fn delete(&self, device_id: &str) -> Result<()> {
sqlx::query("DELETE FROM devices WHERE device_id = ?")
.bind(device_id)
.execute(&self.db.pool)
.await?;
Ok(())
}
}
/// 生成验证码
fn generate_verification_code() -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
(0..8)
.map(|_| {
let idx = rng.gen_range(0..36);
if idx < 10 {
(b'0' + idx) as char
} else {
(b'a' + idx - 10) as char
}
})
.collect()
}
impl From<DeviceRow> for DeviceResponse {
fn from(device: DeviceRow) -> Self {
Self {
id: device.id,
device_id: device.device_id,
name: device.name,
os_type: device.os_type,
os_version: device.os_version,
is_online: device.is_online,
allow_remote: device.allow_remote,
last_seen: device.last_seen,
user_id: device.user_id,
username: None,
}
}
}
/// 设备及用户名的联合查询结果
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct DeviceWithUsername {
pub id: String,
pub device_id: String,
pub name: String,
pub os_type: String,
pub os_version: String,
pub user_id: Option<String>,
pub is_online: bool,
pub allow_remote: bool,
pub verification_code: Option<String>,
pub last_seen: String,
pub created_at: String,
pub username: Option<String>,
}
impl From<DeviceWithUsername> for DeviceResponse {
fn from(device: DeviceWithUsername) -> Self {
Self {
id: device.id,
device_id: device.device_id,
name: device.name,
os_type: device.os_type,
os_version: device.os_version,
is_online: device.is_online,
allow_remote: device.allow_remote,
last_seen: device.last_seen,
user_id: device.user_id,
username: device.username,
}
}
}

View File

@ -0,0 +1,66 @@
//! 服务层
pub mod auth;
pub mod device;
pub mod session;
use crate::config::Config;
use crate::db::Database;
use std::collections::HashMap;
use tokio::sync::RwLock;
/// WebSocket连接信息
pub struct WsConnection {
pub device_id: String,
pub user_id: Option<String>,
pub tx: tokio::sync::mpsc::Sender<String>,
}
/// 应用状态
pub struct AppState {
pub db: Database,
pub config: Config,
/// 在线设备的WebSocket连接 (device_id -> connection)
pub connections: RwLock<HashMap<String, WsConnection>>,
/// 活跃的远程控制会话 (session_id -> (controller_ws, controlled_ws))
pub active_sessions: RwLock<HashMap<String, (String, String)>>,
}
impl AppState {
pub fn new(db: Database, config: Config) -> Self {
Self {
db,
config,
connections: RwLock::new(HashMap::new()),
active_sessions: RwLock::new(HashMap::new()),
}
}
/// 添加WebSocket连接
pub async fn add_connection(&self, device_id: String, user_id: Option<String>, tx: tokio::sync::mpsc::Sender<String>) {
let mut connections = self.connections.write().await;
connections.insert(device_id.clone(), WsConnection { device_id, user_id, tx });
}
/// 移除WebSocket连接
pub async fn remove_connection(&self, device_id: &str) {
let mut connections = self.connections.write().await;
connections.remove(device_id);
}
/// 发送消息给指定设备
pub async fn send_to_device(&self, device_id: &str, message: &str) -> bool {
let connections = self.connections.read().await;
if let Some(conn) = connections.get(device_id) {
conn.tx.send(message.to_string()).await.is_ok()
} else {
false
}
}
/// 检查设备是否在线
pub async fn is_device_online(&self, device_id: &str) -> bool {
let connections = self.connections.read().await;
connections.contains_key(device_id)
}
}

View File

@ -0,0 +1,286 @@
//! 会话服务
use crate::db::Database;
use crate::models::{SessionRow, HistoryRow, SessionResponse, HistoryResponse};
use anyhow::Result;
use chrono::Utc;
use uuid::Uuid;
/// 会话仓储
pub struct SessionRepository<'a> {
db: &'a Database,
}
impl<'a> SessionRepository<'a> {
pub fn new(db: &'a Database) -> Self {
Self { db }
}
/// 创建会话
pub async fn create(
&self,
controller_device_id: &str,
controlled_device_id: &str,
connection_type: &str,
) -> Result<SessionRow> {
let id = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
sqlx::query(
r#"
INSERT INTO sessions (id, controller_device_id, controlled_device_id, started_at, connection_type, status)
VALUES (?, ?, ?, ?, ?, 'connecting')
"#,
)
.bind(&id)
.bind(controller_device_id)
.bind(controlled_device_id)
.bind(&now)
.bind(connection_type)
.execute(&self.db.pool)
.await?;
self.find_by_id(&id).await
}
/// 根据ID查找会话
pub async fn find_by_id(&self, id: &str) -> Result<SessionRow> {
let session = sqlx::query_as::<_, SessionRow>("SELECT * FROM sessions WHERE id = ?")
.bind(id)
.fetch_one(&self.db.pool)
.await?;
Ok(session)
}
/// 更新会话状态
pub async fn update_status(&self, id: &str, status: &str) -> Result<()> {
sqlx::query("UPDATE sessions SET status = ? WHERE id = ?")
.bind(status)
.bind(id)
.execute(&self.db.pool)
.await?;
Ok(())
}
/// 结束会话
pub async fn end_session(&self, id: &str) -> Result<()> {
let now = Utc::now().to_rfc3339();
sqlx::query("UPDATE sessions SET ended_at = ?, status = 'disconnected' WHERE id = ?")
.bind(&now)
.bind(id)
.execute(&self.db.pool)
.await?;
Ok(())
}
/// 获取活跃会话
pub async fn find_active(&self) -> Result<Vec<SessionRow>> {
let sessions = sqlx::query_as::<_, SessionRow>(
"SELECT * FROM sessions WHERE status IN ('connecting', 'connected') ORDER BY started_at DESC",
)
.fetch_all(&self.db.pool)
.await?;
Ok(sessions)
}
/// 获取设备相关的会话
pub async fn find_by_device(&self, device_id: &str) -> Result<Vec<SessionRow>> {
let sessions = sqlx::query_as::<_, SessionRow>(
"SELECT * FROM sessions WHERE controller_device_id = ? OR controlled_device_id = ? ORDER BY started_at DESC",
)
.bind(device_id)
.bind(device_id)
.fetch_all(&self.db.pool)
.await?;
Ok(sessions)
}
/// 获取所有会话
pub async fn find_all(&self, offset: u32, limit: u32) -> Result<(Vec<SessionRow>, i64)> {
let sessions = sqlx::query_as::<_, SessionRow>(
"SELECT * FROM sessions ORDER BY started_at DESC LIMIT ? OFFSET ?",
)
.bind(limit)
.bind(offset)
.fetch_all(&self.db.pool)
.await?;
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM sessions")
.fetch_one(&self.db.pool)
.await?;
Ok((sessions, count.0))
}
/// 获取活跃会话数量
pub async fn count_active(&self) -> Result<i64> {
let count: (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM sessions WHERE status IN ('connecting', 'connected')",
)
.fetch_one(&self.db.pool)
.await?;
Ok(count.0)
}
}
/// 历史记录仓储
pub struct HistoryRepository<'a> {
db: &'a Database,
}
impl<'a> HistoryRepository<'a> {
pub fn new(db: &'a Database) -> Self {
Self { db }
}
/// 创建历史记录
pub async fn create(
&self,
user_id: Option<&str>,
controller_device_id: &str,
controlled_device_id: &str,
controlled_device_name: &str,
connection_type: &str,
) -> Result<HistoryRow> {
let id = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
sqlx::query(
r#"
INSERT INTO control_history (id, user_id, controller_device_id, controlled_device_id, controlled_device_name, started_at, connection_type)
VALUES (?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(&id)
.bind(user_id)
.bind(controller_device_id)
.bind(controlled_device_id)
.bind(controlled_device_name)
.bind(&now)
.bind(connection_type)
.execute(&self.db.pool)
.await?;
self.find_by_id(&id).await
}
/// 根据ID查找历史记录
pub async fn find_by_id(&self, id: &str) -> Result<HistoryRow> {
let history = sqlx::query_as::<_, HistoryRow>("SELECT * FROM control_history WHERE id = ?")
.bind(id)
.fetch_one(&self.db.pool)
.await?;
Ok(history)
}
/// 结束历史记录
pub async fn end_record(&self, id: &str) -> Result<()> {
let now = Utc::now();
let now_str = now.to_rfc3339();
// 计算持续时间
let history = self.find_by_id(id).await?;
let started_at = chrono::DateTime::parse_from_rfc3339(&history.started_at)
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or(now);
let duration = (now - started_at).num_seconds();
sqlx::query(
"UPDATE control_history SET ended_at = ?, duration_seconds = ? WHERE id = ?",
)
.bind(&now_str)
.bind(duration)
.bind(id)
.execute(&self.db.pool)
.await?;
Ok(())
}
/// 获取用户的历史记录
pub async fn find_by_user(&self, user_id: &str, offset: u32, limit: u32) -> Result<(Vec<HistoryRow>, i64)> {
let history = sqlx::query_as::<_, HistoryRow>(
"SELECT * FROM control_history WHERE user_id = ? ORDER BY started_at DESC LIMIT ? OFFSET ?",
)
.bind(user_id)
.bind(limit)
.bind(offset)
.fetch_all(&self.db.pool)
.await?;
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM control_history WHERE user_id = ?")
.bind(user_id)
.fetch_one(&self.db.pool)
.await?;
Ok((history, count.0))
}
/// 获取设备的历史记录
pub async fn find_by_device(&self, device_id: &str, offset: u32, limit: u32) -> Result<(Vec<HistoryRow>, i64)> {
let history = sqlx::query_as::<_, HistoryRow>(
"SELECT * FROM control_history WHERE controller_device_id = ? ORDER BY started_at DESC LIMIT ? OFFSET ?",
)
.bind(device_id)
.bind(limit)
.bind(offset)
.fetch_all(&self.db.pool)
.await?;
let count: (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM control_history WHERE controller_device_id = ?",
)
.bind(device_id)
.fetch_one(&self.db.pool)
.await?;
Ok((history, count.0))
}
}
impl From<SessionRow> for SessionResponse {
fn from(session: SessionRow) -> Self {
Self {
id: session.id,
controller_device_id: session.controller_device_id,
controlled_device_id: session.controlled_device_id,
started_at: session.started_at,
ended_at: session.ended_at,
connection_type: session.connection_type,
status: session.status,
}
}
}
impl From<HistoryRow> for HistoryResponse {
fn from(history: HistoryRow) -> Self {
let duration = history.duration_seconds.map(|secs| {
let hours = secs / 3600;
let minutes = (secs % 3600) / 60;
let seconds = secs % 60;
if hours > 0 {
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
} else {
format!("{:02}:{:02}", minutes, seconds)
}
});
Self {
id: history.id,
controller_device_id: history.controller_device_id,
controlled_device_id: history.controlled_device_id,
controlled_device_name: history.controlled_device_name,
started_at: history.started_at,
ended_at: history.ended_at,
duration,
connection_type: history.connection_type,
}
}
}

View File

@ -0,0 +1,210 @@
//! STUN Server Implementation
//! RFC 5389 compliant STUN server for NAT traversal
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::UdpSocket;
use tracing::{info, debug, warn};
/// STUN Message Types
const BINDING_REQUEST: u16 = 0x0001;
const BINDING_RESPONSE: u16 = 0x0101;
/// STUN Attribute Types
const ATTR_MAPPED_ADDRESS: u16 = 0x0001;
const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020;
const ATTR_SOFTWARE: u16 = 0x8022;
const ATTR_FINGERPRINT: u16 = 0x8028;
/// STUN Magic Cookie (RFC 5389)
const MAGIC_COOKIE: u32 = 0x2112A442;
/// STUN Server
pub struct StunServer {
port: u16,
}
impl StunServer {
pub fn new(port: u16) -> Self {
Self { port }
}
/// Start the STUN server
pub async fn start(&self) -> anyhow::Result<()> {
let addr = format!("0.0.0.0:{}", self.port);
let socket = UdpSocket::bind(&addr).await?;
let socket = Arc::new(socket);
info!("STUN Server listening on UDP port {}", self.port);
loop {
let mut buf = [0u8; 1024];
match socket.recv_from(&mut buf).await {
Ok((len, src_addr)) => {
let socket_clone = socket.clone();
let data = buf[..len].to_vec();
tokio::spawn(async move {
if let Err(e) = handle_stun_request(&socket_clone, &data, src_addr).await {
warn!("Failed to handle STUN request: {}", e);
}
});
}
Err(e) => {
warn!("STUN recv error: {}", e);
}
}
}
}
}
/// Handle a STUN request
async fn handle_stun_request(
socket: &UdpSocket,
data: &[u8],
src_addr: SocketAddr,
) -> anyhow::Result<()> {
// Validate minimum STUN header size (20 bytes)
if data.len() < 20 {
return Ok(());
}
// Parse STUN header
let msg_type = u16::from_be_bytes([data[0], data[1]]);
let msg_len = u16::from_be_bytes([data[2], data[3]]);
let magic = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
let transaction_id = &data[8..20];
// Verify magic cookie
if magic != MAGIC_COOKIE {
debug!("Invalid STUN magic cookie from {}", src_addr);
return Ok(());
}
// Only handle Binding Request
if msg_type != BINDING_REQUEST {
debug!("Ignoring non-binding STUN message type: 0x{:04x}", msg_type);
return Ok(());
}
debug!("STUN Binding Request from {}", src_addr);
// Build Binding Response
let response = build_binding_response(transaction_id, src_addr)?;
// Send response
socket.send_to(&response, src_addr).await?;
debug!("STUN Binding Response sent to {}", src_addr);
Ok(())
}
/// Build a STUN Binding Response
fn build_binding_response(transaction_id: &[u8], mapped_addr: SocketAddr) -> anyhow::Result<Vec<u8>> {
let mut response = Vec::with_capacity(64);
// Build attributes first to calculate length
let xor_mapped_attr = build_xor_mapped_address(mapped_addr, transaction_id);
let software_attr = build_software_attribute();
let attrs_len = xor_mapped_attr.len() + software_attr.len();
// STUN Header (20 bytes)
// Message Type: Binding Response
response.extend_from_slice(&BINDING_RESPONSE.to_be_bytes());
// Message Length (excluding header)
response.extend_from_slice(&(attrs_len as u16).to_be_bytes());
// Magic Cookie
response.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
// Transaction ID (12 bytes)
response.extend_from_slice(transaction_id);
// Attributes
response.extend_from_slice(&xor_mapped_attr);
response.extend_from_slice(&software_attr);
Ok(response)
}
/// Build XOR-MAPPED-ADDRESS attribute (RFC 5389)
fn build_xor_mapped_address(addr: SocketAddr, transaction_id: &[u8]) -> Vec<u8> {
let mut attr = Vec::with_capacity(12);
match addr {
SocketAddr::V4(v4) => {
// Attribute Type
attr.extend_from_slice(&ATTR_XOR_MAPPED_ADDRESS.to_be_bytes());
// Attribute Length (8 bytes for IPv4)
attr.extend_from_slice(&8u16.to_be_bytes());
// Reserved (1 byte) + Family (1 byte)
attr.push(0x00);
attr.push(0x01); // IPv4
// XOR'd Port
let port = v4.port() ^ ((MAGIC_COOKIE >> 16) as u16);
attr.extend_from_slice(&port.to_be_bytes());
// XOR'd Address
let ip_bytes = v4.ip().octets();
let magic_bytes = MAGIC_COOKIE.to_be_bytes();
for i in 0..4 {
attr.push(ip_bytes[i] ^ magic_bytes[i]);
}
}
SocketAddr::V6(v6) => {
// Attribute Type
attr.extend_from_slice(&ATTR_XOR_MAPPED_ADDRESS.to_be_bytes());
// Attribute Length (20 bytes for IPv6)
attr.extend_from_slice(&20u16.to_be_bytes());
// Reserved (1 byte) + Family (1 byte)
attr.push(0x00);
attr.push(0x02); // IPv6
// XOR'd Port
let port = v6.port() ^ ((MAGIC_COOKIE >> 16) as u16);
attr.extend_from_slice(&port.to_be_bytes());
// XOR'd Address (XOR with magic cookie + transaction ID)
let ip_bytes = v6.ip().octets();
let magic_bytes = MAGIC_COOKIE.to_be_bytes();
for i in 0..4 {
attr.push(ip_bytes[i] ^ magic_bytes[i]);
}
for i in 4..16 {
attr.push(ip_bytes[i] ^ transaction_id[i - 4]);
}
}
}
attr
}
/// Build SOFTWARE attribute
fn build_software_attribute() -> Vec<u8> {
let software = b"EasyRemote STUN Server";
let mut attr = Vec::with_capacity(4 + software.len() + 4);
// Attribute Type
attr.extend_from_slice(&ATTR_SOFTWARE.to_be_bytes());
// Attribute Length
let len = software.len() as u16;
attr.extend_from_slice(&len.to_be_bytes());
// Value
attr.extend_from_slice(software);
// Padding to 4-byte boundary
let padding = (4 - (software.len() % 4)) % 4;
for _ in 0..padding {
attr.push(0x00);
}
attr
}
/// Get the default local STUN server URL
pub fn get_local_stun_url(port: u16, public_ip: Option<&str>) -> String {
let host = public_ip.unwrap_or("localhost");
format!("stun:{}:{}", host, port)
}

View File

@ -0,0 +1,929 @@
//! TURN Server Implementation
//! RFC 5766 compliant TURN server for NAT traversal relay
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::net::UdpSocket;
use tokio::sync::RwLock;
use tracing::{info, debug, warn};
/// STUN/TURN Message Types
const BINDING_REQUEST: u16 = 0x0001;
const BINDING_RESPONSE: u16 = 0x0101;
const ALLOCATE_REQUEST: u16 = 0x0003;
const ALLOCATE_RESPONSE: u16 = 0x0103;
const ALLOCATE_ERROR_RESPONSE: u16 = 0x0113;
const REFRESH_REQUEST: u16 = 0x0004;
const REFRESH_RESPONSE: u16 = 0x0104;
const SEND_INDICATION: u16 = 0x0016;
const DATA_INDICATION: u16 = 0x0017;
const CREATE_PERMISSION_REQUEST: u16 = 0x0008;
const CREATE_PERMISSION_RESPONSE: u16 = 0x0108;
const CHANNEL_BIND_REQUEST: u16 = 0x0009;
const CHANNEL_BIND_RESPONSE: u16 = 0x0109;
/// STUN/TURN Attribute Types
const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020;
const ATTR_XOR_RELAYED_ADDRESS: u16 = 0x0016;
const ATTR_XOR_PEER_ADDRESS: u16 = 0x0012;
const ATTR_USERNAME: u16 = 0x0006;
const ATTR_MESSAGE_INTEGRITY: u16 = 0x0008;
const ATTR_ERROR_CODE: u16 = 0x0009;
const ATTR_REALM: u16 = 0x0014;
const ATTR_NONCE: u16 = 0x0015;
const ATTR_LIFETIME: u16 = 0x000D;
const ATTR_DATA: u16 = 0x0013;
const ATTR_SOFTWARE: u16 = 0x8022;
const ATTR_REQUESTED_TRANSPORT: u16 = 0x0019;
const ATTR_CHANNEL_NUMBER: u16 = 0x000C;
/// STUN Magic Cookie (RFC 5389)
const MAGIC_COOKIE: u32 = 0x2112A442;
/// Error codes
const ERROR_UNAUTHORIZED: u16 = 401;
const ERROR_STALE_NONCE: u16 = 438;
const ERROR_ALLOCATION_MISMATCH: u16 = 437;
const ERROR_INSUFFICIENT_CAPACITY: u16 = 508;
/// Default allocation lifetime (10 minutes)
const DEFAULT_LIFETIME: u32 = 600;
/// Allocation state
#[derive(Debug)]
struct Allocation {
client_addr: SocketAddr,
relay_socket: Arc<UdpSocket>,
relay_addr: SocketAddr,
username: String,
permissions: HashMap<String, Instant>, // peer IP -> expiry
channels: HashMap<u16, SocketAddr>, // channel number -> peer addr
created_at: Instant,
lifetime: Duration,
}
/// TURN Server
pub struct TurnServer {
port: u16,
realm: String,
username: String,
password: String,
allocations: Arc<RwLock<HashMap<String, Allocation>>>,
nonces: Arc<RwLock<HashMap<String, Instant>>>,
}
impl TurnServer {
pub fn new(port: u16, realm: String, username: String, password: String) -> Self {
Self {
port,
realm,
username,
password,
allocations: Arc::new(RwLock::new(HashMap::new())),
nonces: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Start the TURN server
pub async fn start(&self) -> anyhow::Result<()> {
let addr = format!("0.0.0.0:{}", self.port);
let socket = UdpSocket::bind(&addr).await?;
let socket = Arc::new(socket);
info!("TURN Server listening on UDP port {}", self.port);
// Start cleanup task for expired allocations
let allocations_clone = self.allocations.clone();
let nonces_clone = self.nonces.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(60)).await;
cleanup_expired(&allocations_clone, &nonces_clone).await;
}
});
loop {
let mut buf = [0u8; 2048];
match socket.recv_from(&mut buf).await {
Ok((len, src_addr)) => {
let socket_clone = socket.clone();
let data = buf[..len].to_vec();
let realm = self.realm.clone();
let username = self.username.clone();
let password = self.password.clone();
let allocations = self.allocations.clone();
let nonces = self.nonces.clone();
tokio::spawn(async move {
if let Err(e) = handle_turn_request(
&socket_clone,
&data,
src_addr,
&realm,
&username,
&password,
&allocations,
&nonces,
).await {
warn!("Failed to handle TURN request: {}", e);
}
});
}
Err(e) => {
warn!("TURN recv error: {}", e);
}
}
}
}
}
/// Cleanup expired allocations and nonces
async fn cleanup_expired(
allocations: &Arc<RwLock<HashMap<String, Allocation>>>,
nonces: &Arc<RwLock<HashMap<String, Instant>>>,
) {
let now = Instant::now();
// Cleanup allocations
let mut allocs = allocations.write().await;
allocs.retain(|_, alloc| {
alloc.created_at.elapsed() < alloc.lifetime
});
// Cleanup nonces (expire after 1 hour)
let mut nonce_map = nonces.write().await;
nonce_map.retain(|_, created| {
now.duration_since(*created) < Duration::from_secs(3600)
});
}
/// Handle a TURN request
async fn handle_turn_request(
socket: &UdpSocket,
data: &[u8],
src_addr: SocketAddr,
realm: &str,
valid_username: &str,
valid_password: &str,
allocations: &Arc<RwLock<HashMap<String, Allocation>>>,
nonces: &Arc<RwLock<HashMap<String, Instant>>>,
) -> anyhow::Result<()> {
// Validate minimum STUN header size (20 bytes)
if data.len() < 20 {
return Ok(());
}
// Parse STUN header
let msg_type = u16::from_be_bytes([data[0], data[1]]);
let magic = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
let transaction_id = &data[8..20];
// Verify magic cookie
if magic != MAGIC_COOKIE {
debug!("Invalid STUN magic cookie from {}", src_addr);
return Ok(());
}
match msg_type {
BINDING_REQUEST => {
// Handle as STUN Binding Request
let response = build_binding_response(transaction_id, src_addr)?;
socket.send_to(&response, src_addr).await?;
debug!("TURN: STUN Binding Response sent to {}", src_addr);
}
ALLOCATE_REQUEST => {
handle_allocate(
socket, data, src_addr, transaction_id,
realm, valid_username, valid_password, allocations, nonces
).await?;
}
REFRESH_REQUEST => {
handle_refresh(
socket, data, src_addr, transaction_id, allocations
).await?;
}
CREATE_PERMISSION_REQUEST => {
handle_create_permission(
socket, data, src_addr, transaction_id, allocations
).await?;
}
SEND_INDICATION => {
handle_send_indication(data, src_addr, allocations).await?;
}
CHANNEL_BIND_REQUEST => {
handle_channel_bind(
socket, data, src_addr, transaction_id, allocations
).await?;
}
_ => {
debug!("TURN: Unknown message type 0x{:04x} from {}", msg_type, src_addr);
}
}
Ok(())
}
/// Handle Allocate Request
async fn handle_allocate(
socket: &UdpSocket,
data: &[u8],
src_addr: SocketAddr,
transaction_id: &[u8],
realm: &str,
valid_username: &str,
valid_password: &str,
allocations: &Arc<RwLock<HashMap<String, Allocation>>>,
nonces: &Arc<RwLock<HashMap<String, Instant>>>,
) -> anyhow::Result<()> {
let client_key = src_addr.to_string();
// Check if allocation already exists
{
let allocs = allocations.read().await;
if allocs.contains_key(&client_key) {
// Allocation already exists
let response = build_error_response(
transaction_id,
ERROR_ALLOCATION_MISMATCH,
"Allocation already exists"
);
socket.send_to(&response, src_addr).await?;
return Ok(());
}
}
// Parse attributes to check authentication
let attrs = parse_attributes(data)?;
// Check for credentials
let username = attrs.get(&ATTR_USERNAME);
let nonce = attrs.get(&ATTR_NONCE);
let msg_integrity = attrs.get(&ATTR_MESSAGE_INTEGRITY);
if username.is_none() || nonce.is_none() || msg_integrity.is_none() {
// Send challenge response
let new_nonce = generate_nonce();
{
let mut nonce_map = nonces.write().await;
nonce_map.insert(new_nonce.clone(), Instant::now());
}
let response = build_challenge_response(transaction_id, realm, &new_nonce);
socket.send_to(&response, src_addr).await?;
debug!("TURN: Sent challenge to {}", src_addr);
return Ok(());
}
// Verify credentials
let username_str = String::from_utf8_lossy(username.unwrap());
if username_str != valid_username {
let response = build_error_response(transaction_id, ERROR_UNAUTHORIZED, "Invalid username");
socket.send_to(&response, src_addr).await?;
return Ok(());
}
// Verify nonce
let nonce_str = String::from_utf8_lossy(nonce.unwrap());
{
let nonce_map = nonces.read().await;
if !nonce_map.contains_key(nonce_str.as_ref()) {
let response = build_error_response(transaction_id, ERROR_STALE_NONCE, "Stale nonce");
socket.send_to(&response, src_addr).await?;
return Ok(());
}
}
// TODO: Verify MESSAGE-INTEGRITY with HMAC-SHA1
// For simplicity, we skip this in the basic implementation
// Create relay socket
let relay_socket = match UdpSocket::bind("0.0.0.0:0").await {
Ok(s) => Arc::new(s),
Err(_) => {
let response = build_error_response(
transaction_id,
ERROR_INSUFFICIENT_CAPACITY,
"Cannot allocate relay address"
);
socket.send_to(&response, src_addr).await?;
return Ok(());
}
};
let relay_addr = relay_socket.local_addr()?;
// Create allocation
let allocation = Allocation {
client_addr: src_addr,
relay_socket: relay_socket.clone(),
relay_addr,
username: username_str.to_string(),
permissions: HashMap::new(),
channels: HashMap::new(),
created_at: Instant::now(),
lifetime: Duration::from_secs(DEFAULT_LIFETIME as u64),
};
// Store allocation
{
let mut allocs = allocations.write().await;
allocs.insert(client_key.clone(), allocation);
}
// Start relay task
let socket_clone = socket.local_addr()?.to_string();
let client_addr = src_addr;
let allocations_clone = allocations.clone();
tokio::spawn(async move {
let main_socket = UdpSocket::bind("0.0.0.0:0").await.ok();
if let Some(main) = main_socket {
main.connect(&socket_clone).await.ok();
relay_incoming_data(relay_socket, main, client_addr, allocations_clone).await;
}
});
// Build success response
let response = build_allocate_response(transaction_id, src_addr, relay_addr)?;
socket.send_to(&response, src_addr).await?;
info!("TURN: Allocation created for {} -> relay {}", src_addr, relay_addr);
Ok(())
}
/// Handle Refresh Request
async fn handle_refresh(
socket: &UdpSocket,
data: &[u8],
src_addr: SocketAddr,
transaction_id: &[u8],
allocations: &Arc<RwLock<HashMap<String, Allocation>>>,
) -> anyhow::Result<()> {
let client_key = src_addr.to_string();
// Parse lifetime attribute
let attrs = parse_attributes(data)?;
let lifetime = attrs.get(&ATTR_LIFETIME)
.map(|v| {
if v.len() >= 4 {
u32::from_be_bytes([v[0], v[1], v[2], v[3]])
} else {
DEFAULT_LIFETIME
}
})
.unwrap_or(DEFAULT_LIFETIME);
let mut allocs = allocations.write().await;
if lifetime == 0 {
// Delete allocation
allocs.remove(&client_key);
info!("TURN: Allocation deleted for {}", src_addr);
} else if let Some(alloc) = allocs.get_mut(&client_key) {
// Refresh allocation
alloc.lifetime = Duration::from_secs(lifetime as u64);
alloc.created_at = Instant::now();
}
// Build response
let response = build_refresh_response(transaction_id, lifetime);
socket.send_to(&response, src_addr).await?;
Ok(())
}
/// Handle Create Permission Request
async fn handle_create_permission(
socket: &UdpSocket,
data: &[u8],
src_addr: SocketAddr,
transaction_id: &[u8],
allocations: &Arc<RwLock<HashMap<String, Allocation>>>,
) -> anyhow::Result<()> {
let client_key = src_addr.to_string();
// Parse peer address
let attrs = parse_attributes(data)?;
let peer_addr = attrs.get(&ATTR_XOR_PEER_ADDRESS);
if peer_addr.is_none() {
return Ok(());
}
let peer_ip = parse_xor_address(peer_addr.unwrap(), &data[8..20])?;
let mut allocs = allocations.write().await;
if let Some(alloc) = allocs.get_mut(&client_key) {
// Add permission (expires in 5 minutes)
alloc.permissions.insert(
peer_ip.ip().to_string(),
Instant::now() + Duration::from_secs(300)
);
// Build success response
let response = build_create_permission_response(transaction_id);
socket.send_to(&response, src_addr).await?;
debug!("TURN: Permission created for {} -> {}", src_addr, peer_ip);
}
Ok(())
}
/// Handle Send Indication
async fn handle_send_indication(
data: &[u8],
src_addr: SocketAddr,
allocations: &Arc<RwLock<HashMap<String, Allocation>>>,
) -> anyhow::Result<()> {
let client_key = src_addr.to_string();
// Parse attributes
let attrs = parse_attributes(data)?;
let peer_addr = attrs.get(&ATTR_XOR_PEER_ADDRESS);
let send_data = attrs.get(&ATTR_DATA);
if peer_addr.is_none() || send_data.is_none() {
return Ok(());
}
let peer = parse_xor_address(peer_addr.unwrap(), &data[8..20])?;
let allocs = allocations.read().await;
if let Some(alloc) = allocs.get(&client_key) {
// Check permission
if let Some(expiry) = alloc.permissions.get(&peer.ip().to_string()) {
if Instant::now() < *expiry {
// Send data to peer
alloc.relay_socket.send_to(send_data.unwrap(), peer).await?;
debug!("TURN: Relayed data from {} to {}", src_addr, peer);
}
}
}
Ok(())
}
/// Handle Channel Bind Request
async fn handle_channel_bind(
socket: &UdpSocket,
data: &[u8],
src_addr: SocketAddr,
transaction_id: &[u8],
allocations: &Arc<RwLock<HashMap<String, Allocation>>>,
) -> anyhow::Result<()> {
let client_key = src_addr.to_string();
// Parse attributes
let attrs = parse_attributes(data)?;
let channel_num = attrs.get(&ATTR_CHANNEL_NUMBER);
let peer_addr = attrs.get(&ATTR_XOR_PEER_ADDRESS);
if channel_num.is_none() || peer_addr.is_none() {
return Ok(());
}
let channel = u16::from_be_bytes([channel_num.unwrap()[0], channel_num.unwrap()[1]]);
let peer = parse_xor_address(peer_addr.unwrap(), &data[8..20])?;
let mut allocs = allocations.write().await;
if let Some(alloc) = allocs.get_mut(&client_key) {
alloc.channels.insert(channel, peer);
// Build success response
let response = build_channel_bind_response(transaction_id);
socket.send_to(&response, src_addr).await?;
debug!("TURN: Channel {} bound to {} for {}", channel, peer, src_addr);
}
Ok(())
}
/// Relay incoming data from peers to client
async fn relay_incoming_data(
relay_socket: Arc<UdpSocket>,
main_socket: UdpSocket,
client_addr: SocketAddr,
allocations: Arc<RwLock<HashMap<String, Allocation>>>,
) {
let mut buf = [0u8; 2048];
let client_key = client_addr.to_string();
loop {
match relay_socket.recv_from(&mut buf).await {
Ok((len, peer_addr)) => {
// Check if allocation still exists
let allocs = allocations.read().await;
if let Some(alloc) = allocs.get(&client_key) {
// Check permission
if let Some(expiry) = alloc.permissions.get(&peer_addr.ip().to_string()) {
if Instant::now() < *expiry {
// Build Data Indication
let indication = build_data_indication(peer_addr, &buf[..len]);
if let Ok(ind) = indication {
let _ = main_socket.send_to(&ind, client_addr).await;
}
}
}
} else {
// Allocation expired
break;
}
}
Err(_) => break,
}
}
}
/// Parse STUN attributes
fn parse_attributes(data: &[u8]) -> anyhow::Result<HashMap<u16, Vec<u8>>> {
let mut attrs = HashMap::new();
if data.len() < 20 {
return Ok(attrs);
}
let msg_len = u16::from_be_bytes([data[2], data[3]]) as usize;
let mut offset = 20;
while offset + 4 <= 20 + msg_len && offset + 4 <= data.len() {
let attr_type = u16::from_be_bytes([data[offset], data[offset + 1]]);
let attr_len = u16::from_be_bytes([data[offset + 2], data[offset + 3]]) as usize;
if offset + 4 + attr_len > data.len() {
break;
}
let value = data[offset + 4..offset + 4 + attr_len].to_vec();
attrs.insert(attr_type, value);
// 4-byte alignment
offset += 4 + attr_len + (4 - (attr_len % 4)) % 4;
}
Ok(attrs)
}
/// Parse XOR-MAPPED-ADDRESS or XOR-PEER-ADDRESS
fn parse_xor_address(data: &[u8], transaction_id: &[u8]) -> anyhow::Result<SocketAddr> {
if data.len() < 8 {
anyhow::bail!("Invalid XOR address");
}
let family = data[1];
let xor_port = u16::from_be_bytes([data[2], data[3]]);
let port = xor_port ^ ((MAGIC_COOKIE >> 16) as u16);
match family {
0x01 => {
// IPv4
let magic_bytes = MAGIC_COOKIE.to_be_bytes();
let ip = std::net::Ipv4Addr::new(
data[4] ^ magic_bytes[0],
data[5] ^ magic_bytes[1],
data[6] ^ magic_bytes[2],
data[7] ^ magic_bytes[3],
);
Ok(SocketAddr::new(std::net::IpAddr::V4(ip), port))
}
0x02 => {
// IPv6
if data.len() < 20 {
anyhow::bail!("Invalid IPv6 XOR address");
}
let magic_bytes = MAGIC_COOKIE.to_be_bytes();
let mut ip_bytes = [0u8; 16];
for i in 0..4 {
ip_bytes[i] = data[4 + i] ^ magic_bytes[i];
}
for i in 4..16 {
ip_bytes[i] = data[4 + i] ^ transaction_id[i - 4];
}
let ip = std::net::Ipv6Addr::from(ip_bytes);
Ok(SocketAddr::new(std::net::IpAddr::V6(ip), port))
}
_ => anyhow::bail!("Unknown address family"),
}
}
/// Generate a random nonce
fn generate_nonce() -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
let bytes: [u8; 16] = rng.gen();
hex::encode(bytes)
}
/// Build STUN Binding Response (same as STUN server)
fn build_binding_response(transaction_id: &[u8], mapped_addr: SocketAddr) -> anyhow::Result<Vec<u8>> {
let mut response = Vec::with_capacity(64);
let xor_mapped_attr = build_xor_mapped_address(mapped_addr, transaction_id);
let software_attr = build_software_attribute();
let attrs_len = xor_mapped_attr.len() + software_attr.len();
response.extend_from_slice(&BINDING_RESPONSE.to_be_bytes());
response.extend_from_slice(&(attrs_len as u16).to_be_bytes());
response.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
response.extend_from_slice(transaction_id);
response.extend_from_slice(&xor_mapped_attr);
response.extend_from_slice(&software_attr);
Ok(response)
}
/// Build Allocate Response
fn build_allocate_response(
transaction_id: &[u8],
mapped_addr: SocketAddr,
relay_addr: SocketAddr,
) -> anyhow::Result<Vec<u8>> {
let mut response = Vec::with_capacity(128);
let xor_mapped = build_xor_mapped_address(mapped_addr, transaction_id);
let xor_relayed = build_xor_relayed_address(relay_addr, transaction_id);
let lifetime = build_lifetime_attribute(DEFAULT_LIFETIME);
let software = build_software_attribute();
let attrs_len = xor_mapped.len() + xor_relayed.len() + lifetime.len() + software.len();
response.extend_from_slice(&ALLOCATE_RESPONSE.to_be_bytes());
response.extend_from_slice(&(attrs_len as u16).to_be_bytes());
response.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
response.extend_from_slice(transaction_id);
response.extend_from_slice(&xor_mapped);
response.extend_from_slice(&xor_relayed);
response.extend_from_slice(&lifetime);
response.extend_from_slice(&software);
Ok(response)
}
/// Build Challenge Response (401)
fn build_challenge_response(transaction_id: &[u8], realm: &str, nonce: &str) -> Vec<u8> {
let mut response = Vec::with_capacity(128);
let error_attr = build_error_attribute(ERROR_UNAUTHORIZED, "Unauthorized");
let realm_attr = build_realm_attribute(realm);
let nonce_attr = build_nonce_attribute(nonce);
let attrs_len = error_attr.len() + realm_attr.len() + nonce_attr.len();
response.extend_from_slice(&ALLOCATE_ERROR_RESPONSE.to_be_bytes());
response.extend_from_slice(&(attrs_len as u16).to_be_bytes());
response.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
response.extend_from_slice(transaction_id);
response.extend_from_slice(&error_attr);
response.extend_from_slice(&realm_attr);
response.extend_from_slice(&nonce_attr);
response
}
/// Build Error Response
fn build_error_response(transaction_id: &[u8], code: u16, reason: &str) -> Vec<u8> {
let mut response = Vec::with_capacity(64);
let error_attr = build_error_attribute(code, reason);
let attrs_len = error_attr.len();
response.extend_from_slice(&ALLOCATE_ERROR_RESPONSE.to_be_bytes());
response.extend_from_slice(&(attrs_len as u16).to_be_bytes());
response.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
response.extend_from_slice(transaction_id);
response.extend_from_slice(&error_attr);
response
}
/// Build Refresh Response
fn build_refresh_response(transaction_id: &[u8], lifetime: u32) -> Vec<u8> {
let mut response = Vec::with_capacity(64);
let lifetime_attr = build_lifetime_attribute(lifetime);
let attrs_len = lifetime_attr.len();
response.extend_from_slice(&REFRESH_RESPONSE.to_be_bytes());
response.extend_from_slice(&(attrs_len as u16).to_be_bytes());
response.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
response.extend_from_slice(transaction_id);
response.extend_from_slice(&lifetime_attr);
response
}
/// Build Create Permission Response
fn build_create_permission_response(transaction_id: &[u8]) -> Vec<u8> {
let mut response = Vec::with_capacity(32);
response.extend_from_slice(&CREATE_PERMISSION_RESPONSE.to_be_bytes());
response.extend_from_slice(&0u16.to_be_bytes()); // No attributes
response.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
response.extend_from_slice(transaction_id);
response
}
/// Build Channel Bind Response
fn build_channel_bind_response(transaction_id: &[u8]) -> Vec<u8> {
let mut response = Vec::with_capacity(32);
response.extend_from_slice(&CHANNEL_BIND_RESPONSE.to_be_bytes());
response.extend_from_slice(&0u16.to_be_bytes()); // No attributes
response.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
response.extend_from_slice(transaction_id);
response
}
/// Build Data Indication
fn build_data_indication(peer_addr: SocketAddr, data: &[u8]) -> anyhow::Result<Vec<u8>> {
use rand::Rng;
let mut indication = Vec::with_capacity(32 + data.len());
// Generate random transaction ID
let mut rng = rand::thread_rng();
let transaction_id: [u8; 12] = rng.gen();
let peer_attr = build_xor_peer_address(peer_addr, &transaction_id);
let data_attr = build_data_attribute(data);
let attrs_len = peer_attr.len() + data_attr.len();
indication.extend_from_slice(&DATA_INDICATION.to_be_bytes());
indication.extend_from_slice(&(attrs_len as u16).to_be_bytes());
indication.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
indication.extend_from_slice(&transaction_id);
indication.extend_from_slice(&peer_attr);
indication.extend_from_slice(&data_attr);
Ok(indication)
}
/// Build XOR-MAPPED-ADDRESS attribute
fn build_xor_mapped_address(addr: SocketAddr, transaction_id: &[u8]) -> Vec<u8> {
build_xor_address_attribute(ATTR_XOR_MAPPED_ADDRESS, addr, transaction_id)
}
/// Build XOR-RELAYED-ADDRESS attribute
fn build_xor_relayed_address(addr: SocketAddr, transaction_id: &[u8]) -> Vec<u8> {
build_xor_address_attribute(ATTR_XOR_RELAYED_ADDRESS, addr, transaction_id)
}
/// Build XOR-PEER-ADDRESS attribute
fn build_xor_peer_address(addr: SocketAddr, transaction_id: &[u8]) -> Vec<u8> {
build_xor_address_attribute(ATTR_XOR_PEER_ADDRESS, addr, transaction_id)
}
/// Build XOR address attribute
fn build_xor_address_attribute(attr_type: u16, addr: SocketAddr, transaction_id: &[u8]) -> Vec<u8> {
let mut attr = Vec::with_capacity(12);
match addr {
SocketAddr::V4(v4) => {
attr.extend_from_slice(&attr_type.to_be_bytes());
attr.extend_from_slice(&8u16.to_be_bytes()); // Length
attr.push(0x00); // Reserved
attr.push(0x01); // IPv4
let port = v4.port() ^ ((MAGIC_COOKIE >> 16) as u16);
attr.extend_from_slice(&port.to_be_bytes());
let ip_bytes = v4.ip().octets();
let magic_bytes = MAGIC_COOKIE.to_be_bytes();
for i in 0..4 {
attr.push(ip_bytes[i] ^ magic_bytes[i]);
}
}
SocketAddr::V6(v6) => {
attr.extend_from_slice(&attr_type.to_be_bytes());
attr.extend_from_slice(&20u16.to_be_bytes()); // Length
attr.push(0x00); // Reserved
attr.push(0x02); // IPv6
let port = v6.port() ^ ((MAGIC_COOKIE >> 16) as u16);
attr.extend_from_slice(&port.to_be_bytes());
let ip_bytes = v6.ip().octets();
let magic_bytes = MAGIC_COOKIE.to_be_bytes();
for i in 0..4 {
attr.push(ip_bytes[i] ^ magic_bytes[i]);
}
for i in 4..16 {
attr.push(ip_bytes[i] ^ transaction_id[i - 4]);
}
}
}
attr
}
/// Build SOFTWARE attribute
fn build_software_attribute() -> Vec<u8> {
let software = b"EasyRemote TURN Server";
let mut attr = Vec::with_capacity(4 + software.len() + 4);
attr.extend_from_slice(&ATTR_SOFTWARE.to_be_bytes());
attr.extend_from_slice(&(software.len() as u16).to_be_bytes());
attr.extend_from_slice(software);
// Padding
let padding = (4 - (software.len() % 4)) % 4;
for _ in 0..padding {
attr.push(0x00);
}
attr
}
/// Build LIFETIME attribute
fn build_lifetime_attribute(lifetime: u32) -> Vec<u8> {
let mut attr = Vec::with_capacity(8);
attr.extend_from_slice(&ATTR_LIFETIME.to_be_bytes());
attr.extend_from_slice(&4u16.to_be_bytes());
attr.extend_from_slice(&lifetime.to_be_bytes());
attr
}
/// Build ERROR-CODE attribute
fn build_error_attribute(code: u16, reason: &str) -> Vec<u8> {
let reason_bytes = reason.as_bytes();
let value_len = 4 + reason_bytes.len();
let padded_len = value_len + (4 - (value_len % 4)) % 4;
let mut attr = Vec::with_capacity(4 + padded_len);
attr.extend_from_slice(&ATTR_ERROR_CODE.to_be_bytes());
attr.extend_from_slice(&(value_len as u16).to_be_bytes());
// Reserved (2 bytes) + Class (1 byte) + Number (1 byte)
attr.push(0x00);
attr.push(0x00);
attr.push((code / 100) as u8);
attr.push((code % 100) as u8);
attr.extend_from_slice(reason_bytes);
// Padding
let padding = (4 - (value_len % 4)) % 4;
for _ in 0..padding {
attr.push(0x00);
}
attr
}
/// Build REALM attribute
fn build_realm_attribute(realm: &str) -> Vec<u8> {
let realm_bytes = realm.as_bytes();
let mut attr = Vec::with_capacity(4 + realm_bytes.len() + 4);
attr.extend_from_slice(&ATTR_REALM.to_be_bytes());
attr.extend_from_slice(&(realm_bytes.len() as u16).to_be_bytes());
attr.extend_from_slice(realm_bytes);
// Padding
let padding = (4 - (realm_bytes.len() % 4)) % 4;
for _ in 0..padding {
attr.push(0x00);
}
attr
}
/// Build NONCE attribute
fn build_nonce_attribute(nonce: &str) -> Vec<u8> {
let nonce_bytes = nonce.as_bytes();
let mut attr = Vec::with_capacity(4 + nonce_bytes.len() + 4);
attr.extend_from_slice(&ATTR_NONCE.to_be_bytes());
attr.extend_from_slice(&(nonce_bytes.len() as u16).to_be_bytes());
attr.extend_from_slice(nonce_bytes);
// Padding
let padding = (4 - (nonce_bytes.len() % 4)) % 4;
for _ in 0..padding {
attr.push(0x00);
}
attr
}
/// Build DATA attribute
fn build_data_attribute(data: &[u8]) -> Vec<u8> {
let mut attr = Vec::with_capacity(4 + data.len() + 4);
attr.extend_from_slice(&ATTR_DATA.to_be_bytes());
attr.extend_from_slice(&(data.len() as u16).to_be_bytes());
attr.extend_from_slice(data);
// Padding
let padding = (4 - (data.len() % 4)) % 4;
for _ in 0..padding {
attr.push(0x00);
}
attr
}

View File

@ -0,0 +1,435 @@
//! WebSocket 处理
use axum::{
extract::{
ws::{Message, WebSocket, WebSocketUpgrade},
Path, Query, State,
},
response::IntoResponse,
};
use futures::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::mpsc;
use uuid::Uuid;
use crate::services::{
device::DeviceRepository,
session::SessionRepository,
AppState,
};
/// WebSocket 查询参数
#[derive(Debug, Deserialize)]
pub struct WsQuery {
pub device_id: String,
pub token: Option<String>,
}
/// 信令消息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SignalMessage {
/// 注册设备
#[serde(rename = "register")]
Register {
device_id: String,
verification_code: String,
},
/// 心跳
#[serde(rename = "heartbeat")]
Heartbeat { device_id: String },
/// 心跳响应
#[serde(rename = "heartbeat_ack")]
HeartbeatAck,
/// 连接请求
#[serde(rename = "connect_request")]
ConnectRequest {
session_id: String,
from_device: String,
to_device: String,
verification_code: String,
},
/// 连接响应
#[serde(rename = "connect_response")]
ConnectResponse {
session_id: String,
accepted: bool,
reason: Option<String>,
},
/// SDP Offer
#[serde(rename = "offer")]
Offer {
session_id: String,
from_device: String,
to_device: String,
sdp: String,
},
/// SDP Answer
#[serde(rename = "answer")]
Answer {
session_id: String,
from_device: String,
to_device: String,
sdp: String,
},
/// ICE Candidate
#[serde(rename = "candidate")]
Candidate {
session_id: String,
from_device: String,
to_device: String,
candidate: String,
sdp_mid: Option<String>,
sdp_mline_index: Option<u32>,
},
/// 会话结束
#[serde(rename = "session_end")]
SessionEnd { session_id: String },
/// 强制下线
#[serde(rename = "force_offline")]
ForceOffline { device_id: String },
/// 错误
#[serde(rename = "error")]
Error { code: u32, message: String },
/// 设置允许远程
#[serde(rename = "set_allow_remote")]
SetAllowRemote { device_id: String, allow: bool },
/// 刷新验证码
#[serde(rename = "refresh_code")]
RefreshCode { device_id: String },
/// 验证码已刷新
#[serde(rename = "code_refreshed")]
CodeRefreshed {
device_id: String,
verification_code: String,
},
/// 屏幕帧数据
#[serde(rename = "screen_frame")]
ScreenFrame {
session_id: String,
from_device: String,
to_device: String,
width: u32,
height: u32,
data: String,
},
/// 鼠标事件
#[serde(rename = "mouse_event")]
MouseEvent {
session_id: String,
from_device: String,
to_device: String,
x: f64,
y: f64,
event_type: String,
button: Option<u8>,
delta: Option<f64>,
},
/// 键盘事件
#[serde(rename = "keyboard_event")]
KeyboardEvent {
session_id: String,
from_device: String,
to_device: String,
key: String,
event_type: String,
},
}
/// 信令WebSocket处理器
pub async fn signal_handler(
ws: WebSocketUpgrade,
State(state): State<Arc<AppState>>,
Query(query): Query<WsQuery>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_signal_socket(socket, state, query))
}
async fn handle_signal_socket(socket: WebSocket, state: Arc<AppState>, query: WsQuery) {
let (mut sender, mut receiver) = socket.split();
let (tx, mut rx) = mpsc::channel::<String>(32);
let device_id = query.device_id.clone();
// 注册连接
state.add_connection(device_id.clone(), None, tx).await;
// 更新设备在线状态
{
let device_repo = DeviceRepository::new(&state.db);
if let Err(e) = device_repo.set_online(&device_id, true).await {
tracing::error!("Failed to set device {} online: {}", device_id, e);
}
}
tracing::info!("Device {} connected via WebSocket", device_id);
// 发送任务
let send_task = tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
if sender.send(Message::Text(msg)).await.is_err() {
break;
}
}
});
// 接收任务
let state_clone = state.clone();
let device_id_clone = device_id.clone();
let recv_task = tokio::spawn(async move {
while let Some(Ok(msg)) = receiver.next().await {
if let Message::Text(text) = msg {
handle_signal_message(&state_clone, &device_id_clone, &text).await;
}
}
});
// 等待任务完成
tokio::select! {
_ = send_task => {},
_ = recv_task => {},
}
// 清理 - 确保断开连接时设置离线状态
tracing::info!("Device {} WebSocket disconnecting, cleaning up...", device_id);
state.remove_connection(&device_id).await;
// 重新创建 repo 来设置离线状态
let device_repo = DeviceRepository::new(&state.db);
if let Err(e) = device_repo.set_online(&device_id, false).await {
tracing::error!("Failed to set device {} offline: {}", device_id, e);
} else {
tracing::info!("Device {} set to offline", device_id);
}
tracing::info!("Device {} disconnected", device_id);
}
async fn handle_signal_message(state: &Arc<AppState>, device_id: &str, text: &str) {
let msg: SignalMessage = match serde_json::from_str(text) {
Ok(m) => m,
Err(e) => {
tracing::warn!("Failed to parse message: {}", e);
return;
}
};
match msg {
SignalMessage::Heartbeat { .. } => {
let device_repo = DeviceRepository::new(&state.db);
let _ = device_repo.update_last_seen(device_id).await;
let response = SignalMessage::HeartbeatAck;
let _ = state
.send_to_device(device_id, &serde_json::to_string(&response).unwrap())
.await;
}
SignalMessage::ConnectRequest {
session_id,
from_device,
to_device,
verification_code,
} => {
let device_repo = DeviceRepository::new(&state.db);
// 验证目标设备是否在线且允许远程
if let Ok(Some(target_device)) = device_repo.find_by_device_id(&to_device).await {
if !target_device.is_online {
let error = SignalMessage::Error {
code: 1001,
message: "目标设备不在线".to_string(),
};
let _ = state
.send_to_device(&from_device, &serde_json::to_string(&error).unwrap())
.await;
return;
}
if !target_device.allow_remote {
let error = SignalMessage::Error {
code: 1002,
message: "目标设备未开启远程协助".to_string(),
};
let _ = state
.send_to_device(&from_device, &serde_json::to_string(&error).unwrap())
.await;
return;
}
// 验证验证码
if target_device.verification_code.as_deref() != Some(&verification_code) {
let error = SignalMessage::Error {
code: 1003,
message: "验证码错误".to_string(),
};
let _ = state
.send_to_device(&from_device, &serde_json::to_string(&error).unwrap())
.await;
return;
}
// 创建会话
let session_repo = SessionRepository::new(&state.db);
let _ = session_repo
.create(&from_device, &to_device, "p2p")
.await;
// 转发连接请求到目标设备
let _ = state.send_to_device(&to_device, text).await;
} else {
let error = SignalMessage::Error {
code: 1004,
message: "目标设备不存在".to_string(),
};
let _ = state
.send_to_device(&from_device, &serde_json::to_string(&error).unwrap())
.await;
}
}
SignalMessage::ConnectResponse {
session_id,
accepted: _,
..
} => {
// 获取会话信息并转发响应
if let Some((controller, _controlled)) = state.active_sessions.read().await.get(&session_id) {
// 转发到控制端
let _ = state.send_to_device(controller, text).await;
}
}
SignalMessage::Offer {
to_device, ..
}
| SignalMessage::Answer {
to_device, ..
}
| SignalMessage::Candidate {
to_device, ..
} => {
// 转发信令消息
let _ = state.send_to_device(&to_device, text).await;
}
SignalMessage::SessionEnd { session_id } => {
let session_repo = SessionRepository::new(&state.db);
let _ = session_repo.end_session(&session_id).await;
}
SignalMessage::SetAllowRemote { device_id, allow } => {
let device_repo = DeviceRepository::new(&state.db);
let _ = device_repo.set_allow_remote(&device_id, allow).await;
}
SignalMessage::RefreshCode { device_id } => {
let device_repo = DeviceRepository::new(&state.db);
if let Ok(new_code) = device_repo.update_verification_code(&device_id).await {
let response = SignalMessage::CodeRefreshed {
device_id: device_id.clone(),
verification_code: new_code,
};
let _ = state
.send_to_device(&device_id, &serde_json::to_string(&response).unwrap())
.await;
}
}
// 转发屏幕帧到目标设备(浏览器)
SignalMessage::ScreenFrame { to_device, .. } => {
let _ = state.send_to_device(&to_device, text).await;
}
// 转发鼠标事件到目标设备(客户端)
SignalMessage::MouseEvent { to_device, .. } => {
let _ = state.send_to_device(&to_device, text).await;
}
// 转发键盘事件到目标设备(客户端)
SignalMessage::KeyboardEvent { to_device, .. } => {
let _ = state.send_to_device(&to_device, text).await;
}
_ => {}
}
}
/// 远程控制WebSocket处理器浏览器端
pub async fn remote_handler(
ws: WebSocketUpgrade,
State(state): State<Arc<AppState>>,
Path(device_id): Path<String>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_remote_socket(socket, state, device_id))
}
async fn handle_remote_socket(socket: WebSocket, state: Arc<AppState>, device_id: String) {
let (mut sender, mut receiver) = socket.split();
// 检查目标设备是否在线
if !state.is_device_online(&device_id).await {
let _ = sender
.send(Message::Text(
serde_json::json!({
"type": "error",
"message": "目标设备不在线"
})
.to_string(),
))
.await;
return;
}
let session_id = Uuid::new_v4().to_string();
let browser_device_id = format!("browser_{}", Uuid::new_v4());
// 创建浏览器端的临时连接
let (tx, mut rx) = mpsc::channel::<String>(32);
state
.add_connection(browser_device_id.clone(), None, tx)
.await;
// 发送连接请求
let connect_req = SignalMessage::ConnectRequest {
session_id: session_id.clone(),
from_device: browser_device_id.clone(),
to_device: device_id.clone(),
verification_code: String::new(), // 浏览器端需要管理员权限,跳过验证码
};
let _ = state
.send_to_device(&device_id, &serde_json::to_string(&connect_req).unwrap())
.await;
// 发送任务
let send_task = tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
if sender.send(Message::Text(msg)).await.is_err() {
break;
}
}
});
// 接收任务
let state_clone = state.clone();
let _browser_id_clone = browser_device_id.clone();
let target_device_id = device_id.clone();
let recv_task = tokio::spawn(async move {
while let Some(Ok(msg)) = receiver.next().await {
if let Message::Text(text) = msg {
// 转发消息到目标设备
let _ = state_clone.send_to_device(&target_device_id, &text).await;
}
}
});
tokio::select! {
_ = send_task => {},
_ = recv_task => {},
}
// 清理
state.remove_connection(&browser_device_id).await;
}

File diff suppressed because it is too large Load Diff

2258
static/index.html Normal file

File diff suppressed because it is too large Load Diff

979
static/static/index.html Normal file
View File

@ -0,0 +1,979 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EasyRemote 管理后台</title>
<style>
:root {
--primary: #3b82f6;
--primary-hover: #2563eb;
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--border-color: #334155;
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
}
.layout {
display: flex;
min-height: 100vh;
}
/* 侧边栏 */
.sidebar {
width: 260px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
padding: 24px 16px;
}
.logo {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
padding: 0 12px 24px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 10px;
}
.logo::before {
content: '';
width: 8px;
height: 24px;
background: var(--primary);
border-radius: 2px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 8px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
margin-bottom: 4px;
}
.nav-item:hover,
.nav-item.active {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.nav-item.active {
background: rgba(59, 130, 246, 0.15);
color: var(--primary);
}
/* 主内容区 */
.main {
flex: 1;
padding: 24px;
overflow-y: auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.page-title {
font-size: 24px;
font-weight: 600;
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
}
.stat-label {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
}
.stat-change {
font-size: 12px;
color: var(--success);
margin-top: 8px;
}
/* 表格 */
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
margin-bottom: 24px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
}
.card-title {
font-size: 16px;
font-weight: 600;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 16px 24px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.table th {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table td {
font-size: 14px;
}
.table tr:last-child td {
border-bottom: none;
}
.table tr:hover {
background: var(--bg-tertiary);
}
/* 状态标签 */
.status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status.online {
background: rgba(34, 197, 94, 0.15);
color: var(--success);
}
.status.offline {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.status.active {
background: rgba(59, 130, 246, 0.15);
color: var(--primary);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
/* 按钮 */
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-hover);
}
.btn-danger {
background: rgba(239, 68, 68, 0.15);
color: var(--error);
}
.btn-danger:hover {
background: rgba(239, 68, 68, 0.25);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-secondary:hover {
background: var(--border-color);
}
/* 搜索框 */
.search-box {
display: flex;
align-items: center;
gap: 12px;
background: var(--bg-tertiary);
border-radius: 8px;
padding: 8px 16px;
}
.search-box input {
background: transparent;
border: none;
color: var(--text-primary);
font-size: 14px;
outline: none;
width: 200px;
}
.search-box input::placeholder {
color: var(--text-secondary);
}
/* 分页 */
.pagination {
display: flex;
justify-content: center;
gap: 8px;
padding: 20px;
}
.pagination-btn {
padding: 8px 14px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.pagination-btn:hover,
.pagination-btn.active {
background: var(--primary);
border-color: var(--primary);
color: white;
}
/* 空状态 */
.empty {
text-align: center;
padding: 48px;
color: var(--text-secondary);
}
/* 远程控制面板 */
.remote-panel {
background: var(--bg-tertiary);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.remote-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.remote-screen {
background: #000;
border-radius: 8px;
aspect-ratio: 16/9;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
}
/* 登录表单 */
.login-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, var(--bg-primary) 0%, #0c1222 100%);
}
.login-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 40px;
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-logo {
font-size: 48px;
margin-bottom: 16px;
}
.login-title {
font-size: 24px;
font-weight: 600;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.form-input {
width: 100%;
padding: 12px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
outline: none;
transition: all 0.2s;
}
.form-input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.login-btn {
width: 100%;
padding: 14px;
background: var(--primary);
color: white;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.login-btn:hover {
background: var(--primary-hover);
}
/* 隐藏内容 */
.hidden {
display: none !important;
}
</style>
</head>
<body>
<!-- 登录页面 -->
<div id="login-page" class="login-container">
<div class="login-card">
<div class="login-header">
<div class="login-logo">🔒</div>
<h1 class="login-title">管理后台</h1>
</div>
<form id="login-form">
<div class="form-group">
<label class="form-label">用户名</label>
<input type="text" class="form-input" id="username" placeholder="请输入管理员用户名" required>
</div>
<div class="form-group">
<label class="form-label">密码</label>
<input type="password" class="form-input" id="password" placeholder="请输入密码" required>
</div>
<div id="login-error" class="hidden" style="color: var(--error); font-size: 14px; margin-bottom: 16px;"></div>
<button type="submit" class="login-btn">登录</button>
</form>
</div>
</div>
<!-- 管理面板 -->
<div id="admin-panel" class="layout hidden">
<aside class="sidebar">
<div class="logo">EasyRemote 管理</div>
<nav>
<div class="nav-item active" data-page="dashboard">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
</svg>
仪表盘
</div>
<div class="nav-item" data-page="users">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
用户管理
</div>
<div class="nav-item" data-page="devices">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
设备管理
</div>
<div class="nav-item" data-page="sessions">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
会话管理
</div>
<div class="nav-item" data-page="remote">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
<polyline points="10 17 15 12 10 7"/>
<line x1="15" y1="12" x2="3" y2="12"/>
</svg>
远程控制
</div>
</nav>
</aside>
<main class="main">
<!-- 仪表盘 -->
<div id="page-dashboard" class="page-content">
<div class="header">
<h1 class="page-title">仪表盘</h1>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">总用户数</div>
<div class="stat-value" id="stat-users">-</div>
</div>
<div class="stat-card">
<div class="stat-label">设备总数</div>
<div class="stat-value" id="stat-devices">-</div>
</div>
<div class="stat-card">
<div class="stat-label">在线设备</div>
<div class="stat-value" id="stat-online">-</div>
</div>
<div class="stat-card">
<div class="stat-label">活跃会话</div>
<div class="stat-value" id="stat-sessions">-</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">最近会话</h3>
</div>
<table class="table">
<thead>
<tr>
<th>控制端</th>
<th>被控端</th>
<th>开始时间</th>
<th>状态</th>
</tr>
</thead>
<tbody id="recent-sessions">
<tr>
<td colspan="4" class="empty">暂无数据</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 用户管理 -->
<div id="page-users" class="page-content hidden">
<div class="header">
<h1 class="page-title">用户管理</h1>
<div class="search-box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input type="text" placeholder="搜索用户...">
</div>
</div>
<div class="card">
<table class="table">
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="users-table">
<tr>
<td colspan="5" class="empty">加载中...</td>
</tr>
</tbody>
</table>
<div class="pagination" id="users-pagination"></div>
</div>
</div>
<!-- 设备管理 -->
<div id="page-devices" class="page-content hidden">
<div class="header">
<h1 class="page-title">设备管理</h1>
<div class="search-box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input type="text" placeholder="搜索设备...">
</div>
</div>
<div class="card">
<table class="table">
<thead>
<tr>
<th>设备ID</th>
<th>名称</th>
<th>绑定用户</th>
<th>系统</th>
<th>状态</th>
<th>最后在线</th>
<th>操作</th>
</tr>
</thead>
<tbody id="devices-table">
<tr>
<td colspan="7" class="empty">加载中...</td>
</tr>
</tbody>
</table>
<div class="pagination" id="devices-pagination"></div>
</div>
</div>
<!-- 会话管理 -->
<div id="page-sessions" class="page-content hidden">
<div class="header">
<h1 class="page-title">会话管理</h1>
</div>
<div class="card">
<table class="table">
<thead>
<tr>
<th>会话ID</th>
<th>控制端</th>
<th>被控端</th>
<th>类型</th>
<th>开始时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="sessions-table">
<tr>
<td colspan="7" class="empty">加载中...</td>
</tr>
</tbody>
</table>
<div class="pagination" id="sessions-pagination"></div>
</div>
</div>
<!-- 远程控制 -->
<div id="page-remote" class="page-content hidden">
<div class="header">
<h1 class="page-title">远程控制</h1>
</div>
<div class="remote-panel">
<div class="remote-header">
<div>
<select class="form-input" id="remote-device-select" style="width: 300px;">
<option value="">选择在线设备...</option>
</select>
</div>
<div>
<button class="btn btn-primary" id="connect-btn">连接</button>
<button class="btn btn-danger hidden" id="disconnect-btn">断开</button>
</div>
</div>
<div class="remote-screen" id="remote-screen">
<span>选择设备后点击连接开始远程控制</span>
</div>
</div>
</div>
</main>
</div>
<script>
// API 基础URL
const API_BASE = '/api';
let authToken = localStorage.getItem('admin_token');
// 页面初始化
document.addEventListener('DOMContentLoaded', () => {
if (authToken) {
checkAuth();
}
setupEventListeners();
});
// 检查认证状态
async function checkAuth() {
try {
const response = await fetch(`${API_BASE}/users/me`, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (response.ok) {
const data = await response.json();
if (data.data.role === 'admin') {
showAdminPanel();
loadDashboard();
return;
}
}
} catch (e) {}
localStorage.removeItem('admin_token');
authToken = null;
}
// 设置事件监听
function setupEventListeners() {
// 登录表单
document.getElementById('login-form').addEventListener('submit', handleLogin);
// 导航
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
const page = item.dataset.page;
navigateTo(page);
});
});
}
// 处理登录
async function handleLogin(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorEl = document.getElementById('login-error');
try {
const response = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok && data.success) {
if (data.data.user.role !== 'admin') {
throw new Error('需要管理员权限');
}
authToken = data.data.token;
localStorage.setItem('admin_token', authToken);
showAdminPanel();
loadDashboard();
} else {
throw new Error(data.error || '登录失败');
}
} catch (error) {
errorEl.textContent = error.message;
errorEl.classList.remove('hidden');
}
}
// 显示管理面板
function showAdminPanel() {
document.getElementById('login-page').classList.add('hidden');
document.getElementById('admin-panel').classList.remove('hidden');
}
// 页面导航
function navigateTo(page) {
// 更新导航状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.toggle('active', item.dataset.page === page);
});
// 切换页面内容
document.querySelectorAll('.page-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(`page-${page}`).classList.remove('hidden');
// 加载数据
switch (page) {
case 'dashboard': loadDashboard(); break;
case 'users': loadUsers(); break;
case 'devices': loadDevices(); break;
case 'sessions': loadSessions(); break;
case 'remote': loadRemoteDevices(); break;
}
}
// API 请求辅助函数
async function apiRequest(endpoint, options = {}) {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
...options.headers
}
});
return response.json();
}
// 加载仪表盘
async function loadDashboard() {
try {
const stats = await apiRequest('/admin/stats');
if (stats.success) {
document.getElementById('stat-users').textContent = stats.data.total_users;
document.getElementById('stat-devices').textContent = stats.data.total_devices;
document.getElementById('stat-online').textContent = stats.data.online_devices;
document.getElementById('stat-sessions').textContent = stats.data.active_sessions;
}
const sessions = await apiRequest('/admin/sessions?limit=5');
const tbody = document.getElementById('recent-sessions');
if (sessions.success && sessions.data.items.length > 0) {
tbody.innerHTML = sessions.data.items.map(s => `
<tr>
<td>${s.controller_device_id}</td>
<td>${s.controlled_device_id}</td>
<td>${new Date(s.started_at).toLocaleString('zh-CN')}</td>
<td><span class="status ${s.status === 'connected' ? 'active' : 'offline'}">
<span class="status-dot"></span>${s.status}
</span></td>
</tr>
`).join('');
} else {
tbody.innerHTML = '<tr><td colspan="4" class="empty">暂无数据</td></tr>';
}
} catch (e) {
console.error('加载仪表盘失败:', e);
}
}
// 加载用户列表
async function loadUsers(page = 1) {
try {
const data = await apiRequest(`/admin/users?page=${page}&limit=10`);
const tbody = document.getElementById('users-table');
if (data.success && data.data.items.length > 0) {
tbody.innerHTML = data.data.items.map(u => `
<tr>
<td>${u.username}</td>
<td>${u.email || '-'}</td>
<td><span class="status ${u.role === 'admin' ? 'active' : ''}">${u.role}</span></td>
<td>${new Date(u.created_at).toLocaleString('zh-CN')}</td>
<td>
<button class="btn btn-danger" onclick="deleteUser('${u.id}')">删除</button>
</td>
</tr>
`).join('');
renderPagination('users-pagination', data.data, (p) => loadUsers(p));
} else {
tbody.innerHTML = '<tr><td colspan="5" class="empty">暂无数据</td></tr>';
}
} catch (e) {
console.error('加载用户失败:', e);
}
}
// 加载设备列表
async function loadDevices(page = 1) {
try {
const data = await apiRequest(`/admin/devices?page=${page}&limit=10`);
const tbody = document.getElementById('devices-table');
if (data.success && data.data.items.length > 0) {
tbody.innerHTML = data.data.items.map(d => `
<tr>
<td style="font-family: monospace;">${d.device_id}</td>
<td>${d.name}</td>
<td>${d.username ? `<span class="status active"><span class="status-dot"></span>${d.username}</span>` : '<span style="color: var(--text-secondary);">未绑定</span>'}</td>
<td>${d.os_type}</td>
<td><span class="status ${d.is_online ? 'online' : 'offline'}">
<span class="status-dot"></span>${d.is_online ? '在线' : '离线'}
</span></td>
<td>${new Date(d.last_seen).toLocaleString('zh-CN')}</td>
<td>
${d.is_online ? `<button class="btn btn-secondary" onclick="forceOffline('${d.device_id}')">强制下线</button>` : ''}
</td>
</tr>
`).join('');
renderPagination('devices-pagination', data.data, (p) => loadDevices(p));
} else {
tbody.innerHTML = '<tr><td colspan="7" class="empty">暂无数据</td></tr>';
}
} catch (e) {
console.error('加载设备失败:', e);
}
}
// 加载会话列表
async function loadSessions(page = 1) {
try {
const data = await apiRequest(`/admin/sessions?page=${page}&limit=10`);
const tbody = document.getElementById('sessions-table');
if (data.success && data.data.items.length > 0) {
tbody.innerHTML = data.data.items.map(s => `
<tr>
<td style="font-family: monospace; font-size: 12px;">${s.id.substring(0, 8)}...</td>
<td>${s.controller_device_id}</td>
<td>${s.controlled_device_id}</td>
<td>${s.connection_type}</td>
<td>${new Date(s.started_at).toLocaleString('zh-CN')}</td>
<td><span class="status ${s.status === 'connected' ? 'active' : 'offline'}">
<span class="status-dot"></span>${s.status}
</span></td>
<td>
${s.status === 'connected' ? `<button class="btn btn-danger" onclick="endSession('${s.id}')">结束</button>` : ''}
</td>
</tr>
`).join('');
renderPagination('sessions-pagination', data.data, (p) => loadSessions(p));
} else {
tbody.innerHTML = '<tr><td colspan="7" class="empty">暂无数据</td></tr>';
}
} catch (e) {
console.error('加载会话失败:', e);
}
}
// 加载在线设备(用于远程控制)
async function loadRemoteDevices() {
try {
const data = await apiRequest('/admin/devices?limit=100');
const select = document.getElementById('remote-device-select');
select.innerHTML = '<option value="">选择在线设备...</option>';
if (data.success) {
data.data.items.filter(d => d.is_online).forEach(d => {
select.innerHTML += `<option value="${d.device_id}">${d.name} (${d.device_id})</option>`;
});
}
} catch (e) {
console.error('加载设备失败:', e);
}
}
// 渲染分页
function renderPagination(containerId, data, loadFunc) {
const container = document.getElementById(containerId);
if (data.total_pages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
for (let i = 1; i <= data.total_pages; i++) {
html += `<button class="pagination-btn ${i === data.page ? 'active' : ''}" onclick="(${loadFunc.toString()})(${i})">${i}</button>`;
}
container.innerHTML = html;
}
// 删除用户
async function deleteUser(userId) {
if (!confirm('确定要删除该用户吗?')) return;
try {
await apiRequest(`/admin/users/${userId}`, { method: 'DELETE' });
loadUsers();
} catch (e) {
alert('删除失败');
}
}
// 强制下线
async function forceOffline(deviceId) {
if (!confirm('确定要强制该设备下线吗?')) return;
try {
await apiRequest(`/devices/${deviceId}/offline`, { method: 'POST' });
loadDevices();
} catch (e) {
alert('操作失败');
}
}
// 结束会话
async function endSession(sessionId) {
if (!confirm('确定要结束该会话吗?')) return;
try {
await apiRequest(`/sessions/${sessionId}/end`, { method: 'POST' });
loadSessions();
} catch (e) {
alert('操作失败');
}
}
</script>
</body>
</html>