Files
One-KVM/docs/modules/web.md
mofeng-git a8a3b6c66b feat: 添加 RustDesk 协议支持和项目文档
- 新增 RustDesk 模块,支持与 RustDesk 客户端连接
  - 实现会合服务器协议和 P2P 连接
  - 支持 NaCl 加密和密钥交换
  - 添加视频帧和 HID 事件适配器
- 添加 Protobuf 协议定义 (message.proto, rendezvous.proto)
- 新增完整项目文档
  - 各功能模块文档 (video, hid, msd, otg, webrtc 等)
  - hwcodec 和 RustDesk 协议技术报告
  - 系统架构和技术栈文档
- 更新 Web 前端 RustDesk 配置界面和 API
2025-12-31 18:59:52 +08:00

11 KiB

Web 模块文档

1. 模块概述

Web 模块提供 HTTP API 和静态文件服务。

1.1 主要功能

  • REST API
  • WebSocket
  • 静态文件服务
  • 认证中间件
  • CORS 支持

1.2 文件结构

src/web/
├── mod.rs              # 模块导出
├── routes.rs           # 路由定义 (9KB)
├── ws.rs               # WebSocket (8KB)
├── audio_ws.rs         # 音频 WebSocket (8KB)
├── static_files.rs     # 静态文件 (6KB)
└── handlers/           # API 处理器
    ├── mod.rs
    └── config/
        ├── mod.rs
        ├── apply.rs
        ├── types.rs
        └── rustdesk.rs

2. 路由结构

2.1 公共路由 (无认证)

路由 方法 描述
/health GET 健康检查
/auth/login POST 登录
/setup GET 获取设置状态
/setup/init POST 初始化设置

2.2 用户路由 (需认证)

路由 方法 描述
/info GET 系统信息
/devices GET 设备列表
/stream/* * 流控制
/webrtc/* * WebRTC 信令
/hid/* * HID 控制
/audio/* * 音频控制
/ws WS 事件 WebSocket
/ws/audio WS 音频 WebSocket

2.3 管理员路由 (需 Admin)

路由 方法 描述
/config/* * 配置管理
/msd/* * MSD 操作
/atx/* * ATX 控制
/extensions/* * 扩展管理
/rustdesk/* * RustDesk
/users/* * 用户管理

3. 路由定义

pub fn create_router(state: Arc<AppState>) -> Router {
    Router::new()
        // 公共路由
        .route("/health", get(handlers::health))
        .route("/auth/login", post(handlers::login))
        .route("/setup", get(handlers::setup_status))
        .route("/setup/init", post(handlers::setup_init))

        // 用户路由
        .nest("/api", user_routes())

        // 管理员路由
        .nest("/api/admin", admin_routes())

        // 静态文件
        .fallback(static_files::serve)

        // 中间件
        .layer(CorsLayer::permissive())
        .layer(CompressionLayer::new())
        .layer(TraceLayer::new_for_http())

        // 状态
        .with_state(state)
}

fn user_routes() -> Router {
    Router::new()
        .route("/info", get(handlers::system_info))
        .route("/devices", get(handlers::list_devices))

        // 流控制
        .route("/stream/status", get(handlers::stream_status))
        .route("/stream/start", post(handlers::stream_start))
        .route("/stream/stop", post(handlers::stream_stop))
        .route("/stream/mjpeg", get(handlers::mjpeg_stream))

        // WebRTC
        .route("/webrtc/session", post(handlers::webrtc_create_session))
        .route("/webrtc/offer", post(handlers::webrtc_offer))
        .route("/webrtc/ice", post(handlers::webrtc_ice))
        .route("/webrtc/close", post(handlers::webrtc_close))

        // HID
        .route("/hid/status", get(handlers::hid_status))
        .route("/hid/reset", post(handlers::hid_reset))

        // WebSocket
        .route("/ws", get(handlers::ws_handler))
        .route("/ws/audio", get(handlers::audio_ws_handler))

        // 认证中间件
        .layer(middleware::from_fn(auth_middleware))
}

fn admin_routes() -> Router {
    Router::new()
        // 配置
        .route("/config", get(handlers::config::get_config))
        .route("/config", patch(handlers::config::update_config))

        // MSD
        .route("/msd/status", get(handlers::msd_status))
        .route("/msd/connect", post(handlers::msd_connect))

        // ATX
        .route("/atx/status", get(handlers::atx_status))
        .route("/atx/power/short", post(handlers::atx_power_short))

        // 认证中间件
        .layer(middleware::from_fn(auth_middleware))
        .layer(middleware::from_fn(admin_middleware))
}

4. 静态文件服务

#[derive(RustEmbed)]
#[folder = "web/dist"]
#[include = "*.html"]
#[include = "*.js"]
#[include = "*.css"]
#[include = "assets/*"]
struct Assets;

pub async fn serve(uri: Uri) -> impl IntoResponse {
    let path = uri.path().trim_start_matches('/');

    // 尝试获取文件
    if let Some(content) = Assets::get(path) {
        let mime = mime_guess::from_path(path)
            .first_or_octet_stream();

        return (
            [(header::CONTENT_TYPE, mime.as_ref())],
            content.data.into_owned(),
        ).into_response();
    }

    // SPA 回退到 index.html
    if let Some(content) = Assets::get("index.html") {
        return (
            [(header::CONTENT_TYPE, "text/html")],
            content.data.into_owned(),
        ).into_response();
    }

    StatusCode::NOT_FOUND.into_response()
}

5. WebSocket 处理

5.1 事件 WebSocket (ws.rs)

pub async fn ws_handler(
    ws: WebSocketUpgrade,
    State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
    ws.on_upgrade(|socket| handle_ws(socket, state))
}

async fn handle_ws(mut socket: WebSocket, state: Arc<AppState>) {
    // 发送初始设备信息
    let device_info = state.get_device_info().await;
    let json = serde_json::to_string(&device_info).unwrap();
    let _ = socket.send(Message::Text(json)).await;

    // 订阅事件
    let mut rx = state.events.subscribe();

    loop {
        tokio::select! {
            // 发送事件
            result = rx.recv() => {
                if let Ok(event) = result {
                    let json = serde_json::to_string(&event).unwrap();
                    if socket.send(Message::Text(json)).await.is_err() {
                        break;
                    }
                }
            }

            // 接收消息 (心跳/关闭)
            msg = socket.recv() => {
                match msg {
                    Some(Ok(Message::Ping(data))) => {
                        let _ = socket.send(Message::Pong(data)).await;
                    }
                    Some(Ok(Message::Close(_))) | None => break,
                    _ => {}
                }
            }
        }
    }
}

5.2 音频 WebSocket (audio_ws.rs)

pub async fn audio_ws_handler(
    ws: WebSocketUpgrade,
    State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
    ws.on_upgrade(|socket| handle_audio_ws(socket, state))
}

async fn handle_audio_ws(mut socket: WebSocket, state: Arc<AppState>) {
    // 订阅音频帧
    let mut rx = state.audio.subscribe();

    loop {
        tokio::select! {
            // 发送音频帧
            result = rx.recv() => {
                if let Ok(frame) = result {
                    if socket.send(Message::Binary(frame.data.to_vec())).await.is_err() {
                        break;
                    }
                }
            }

            // 处理关闭
            msg = socket.recv() => {
                match msg {
                    Some(Ok(Message::Close(_))) | None => break,
                    _ => {}
                }
            }
        }
    }
}

6. MJPEG 流

pub async fn mjpeg_stream(
    State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
    let boundary = "frame";

    // 订阅视频帧
    let rx = state.stream_manager.subscribe_mjpeg();

    // 创建流
    let stream = async_stream::stream! {
        let mut rx = rx;
        while let Ok(frame) = rx.recv().await {
            let header = format!(
                "--{}\r\nContent-Type: image/jpeg\r\nContent-Length: {}\r\n\r\n",
                boundary,
                frame.data.len()
            );
            yield Ok::<_, std::io::Error>(Bytes::from(header));
            yield Ok(frame.data.clone());
            yield Ok(Bytes::from("\r\n"));
        }
    };

    (
        [(
            header::CONTENT_TYPE,
            format!("multipart/x-mixed-replace; boundary={}", boundary),
        )],
        Body::from_stream(stream),
    )
}

7. 错误处理

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::AuthError => (StatusCode::UNAUTHORIZED, "Authentication failed"),
            AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized"),
            AppError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden"),
            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.as_str()),
            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
            AppError::Internal(err) => {
                tracing::error!("Internal error: {:?}", err);
                (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
            }
            // ...
        };

        (status, Json(json!({ "error": message }))).into_response()
    }
}

8. 请求提取器

// 从 Cookie 获取会话
pub struct AuthUser(pub Session);

#[async_trait]
impl<S> FromRequestParts<S> for AuthUser
where
    S: Send + Sync,
{
    type Rejection = AppError;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        let cookies = Cookies::from_request_parts(parts, state).await?;
        let token = cookies
            .get("session_id")
            .map(|c| c.value().to_string())
            .ok_or(AppError::Unauthorized)?;

        let state = parts.extensions.get::<Arc<AppState>>().unwrap();
        let session = state.sessions
            .get_session(&token)
            .ok_or(AppError::Unauthorized)?;

        Ok(AuthUser(session))
    }
}

9. 中间件

9.1 认证中间件

pub async fn auth_middleware(
    State(state): State<Arc<AppState>>,
    cookies: Cookies,
    mut request: Request,
    next: Next,
) -> Response {
    let token = cookies
        .get("session_id")
        .map(|c| c.value().to_string());

    if let Some(session) = token.and_then(|t| state.sessions.get_session(&t)) {
        request.extensions_mut().insert(session);
        next.run(request).await
    } else {
        StatusCode::UNAUTHORIZED.into_response()
    }
}

9.2 Admin 中间件

pub async fn admin_middleware(
    Extension(session): Extension<Session>,
    request: Request,
    next: Next,
) -> Response {
    if session.role == UserRole::Admin {
        next.run(request).await
    } else {
        StatusCode::FORBIDDEN.into_response()
    }
}

10. HTTPS 支持

// 使用 axum-server 提供 TLS
let tls_config = RustlsConfig::from_pem_file(cert_path, key_path).await?;

axum_server::bind_rustls(addr, tls_config)
    .serve(app.into_make_service())
    .await?;

// 或自动生成自签名证书
let (cert, key) = generate_self_signed_cert()?;
let tls_config = RustlsConfig::from_pem(cert, key).await?;