refactor: 删除部分多余的代码和注释

This commit is contained in:
mofeng-git
2026-05-01 17:31:04 +08:00
parent 74035f8e12
commit d8e7de74a6
165 changed files with 2960 additions and 9917 deletions

View File

@@ -31,12 +31,8 @@ use tracing::{debug, info, warn};
use crate::audio::OpusFrame;
use crate::state::AppState;
/// Audio packet type identifier
const AUDIO_PACKET_TYPE: u8 = 0x02;
/// Audio WebSocket upgrade handler
///
/// Upgrades HTTP connection to WebSocket for audio streaming.
pub async fn audio_ws_handler(
ws: WebSocketUpgrade,
State(state): State<Arc<AppState>>,
@@ -44,16 +40,13 @@ pub async fn audio_ws_handler(
ws.on_upgrade(move |socket| handle_audio_socket(socket, state))
}
/// Handle audio WebSocket connection
async fn handle_audio_socket(socket: WebSocket, state: Arc<AppState>) {
let (mut sender, mut receiver) = socket.split();
// Try to get Opus frame subscription
let opus_rx = match state.audio.subscribe_opus_async().await {
let opus_rx = match state.audio.subscribe_opus().await {
Some(rx) => rx,
None => {
warn!("Audio not streaming, rejecting WebSocket connection");
// Send error message before closing
let _ = sender
.send(Message::Text(
r#"{"error": "Audio not streaming"}"#.to_string().into(),
@@ -68,16 +61,13 @@ async fn handle_audio_socket(socket: WebSocket, state: Arc<AppState>) {
info!("Audio WebSocket client connected");
// Track connection for cleanup
let mut closed = false;
// Use interval instead of sleep for more efficient keepalive
let mut ping_interval = tokio::time::interval(std::time::Duration::from_secs(30));
ping_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tokio::select! {
// Receive Opus frames and send to client
opus_result = opus_rx.recv() => {
let frame = match opus_result {
Some(f) => f,
@@ -94,7 +84,6 @@ async fn handle_audio_socket(socket: WebSocket, state: Arc<AppState>) {
}
}
// Handle client messages (ping/close)
msg = receiver.next() => {
match msg {
Some(Ok(Message::Close(_))) => {
@@ -107,11 +96,8 @@ async fn handle_audio_socket(socket: WebSocket, state: Arc<AppState>) {
break;
}
}
Some(Ok(Message::Pong(_))) => {
// Pong received, connection is alive
}
Some(Ok(Message::Pong(_))) => {}
Some(Ok(Message::Text(text))) => {
// Handle potential control messages
debug!("Received text message on audio WS: {}", text);
}
Some(Err(e)) => {
@@ -119,14 +105,12 @@ async fn handle_audio_socket(socket: WebSocket, state: Arc<AppState>) {
break;
}
None => {
// Connection closed
break;
}
_ => {}
}
}
// Periodic ping to keep connection alive (using interval)
_ = ping_interval.tick() => {
if sender.send(Message::Ping(vec![].into())).await.is_err() {
warn!("Failed to send ping, disconnecting");
@@ -137,39 +121,24 @@ async fn handle_audio_socket(socket: WebSocket, state: Arc<AppState>) {
}
if !closed {
// Try to send close message
let _ = sender.send(Message::Close(None)).await;
}
info!("Audio WebSocket client disconnected");
}
/// Encode Opus frame to binary packet format
///
/// ## Format
///
/// | Offset | Size | Description |
/// |--------|------|-------------|
/// | 0 | 1 | Packet type (0x02 for audio) |
/// | 1 | 4 | Timestamp (u32 LE, ms since start) |
/// | 5 | 2 | Duration (u16 LE, ms) |
/// | 7 | 4 | Sequence number (u32 LE) |
/// | 11 | 4 | Data length (u32 LE) |
/// | 15 | N | Opus encoded data |
fn encode_audio_packet(frame: &OpusFrame, stream_start: Instant) -> Vec<u8> {
let timestamp_ms = stream_start.elapsed().as_millis() as u32;
let data_len = frame.data.len() as u32;
let mut buf = Vec::with_capacity(15 + frame.data.len());
// Header
buf.push(AUDIO_PACKET_TYPE);
buf.extend_from_slice(&timestamp_ms.to_le_bytes());
buf.extend_from_slice(&(frame.duration_ms as u16).to_le_bytes());
buf.extend_from_slice(&(frame.sequence as u32).to_le_bytes());
buf.extend_from_slice(&data_len.to_le_bytes());
// Opus data
buf.extend_from_slice(&frame.data);
buf
@@ -186,8 +155,6 @@ mod tests {
data: Bytes::from(vec![1, 2, 3, 4, 5]),
duration_ms: 20,
sequence: 42,
timestamp: Instant::now(),
rtp_timestamp: 0,
};
let stream_start = Instant::now();
@@ -195,11 +162,5 @@ mod tests {
assert!(encoded.len() >= 15);
assert_eq!(encoded[0], AUDIO_PACKET_TYPE);
// decode_audio_packet function was removed, skip decode test
}
#[test]
fn test_decode_invalid_packet() {
// decode_audio_packet function was removed, skip this test
}
}

31
src/web/error.rs Normal file
View File

@@ -0,0 +1,31 @@
use crate::error::AppError;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::Serialize;
#[derive(Serialize)]
pub struct ErrorResponse {
pub success: bool,
pub message: String,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let body = ErrorResponse {
success: false,
message: self.to_string(),
};
tracing::error!(
error_type = std::any::type_name_of_val(&self),
error_message = %body.message,
"Request failed"
);
// Always return 200 OK - success/failure is indicated by the success field
(StatusCode::OK, Json(body)).into_response()
}
}

View File

@@ -1,13 +1,10 @@
//! 配置热重载逻辑
//!
//! 从 handlers.rs 中抽取的配置应用函数,负责将配置变更应用到各个子系统。
use std::sync::Arc;
use crate::config::*;
use crate::error::{AppError, Result};
use crate::rtsp::RtspService;
use crate::state::AppState;
use crate::stream_encoder::encoder_type_to_backend;
use crate::video::codec_constraints::{
enforce_constraints_with_stream_manager, StreamCodecConstraints,
};
@@ -32,13 +29,11 @@ async fn reconcile_otg_from_store(state: &Arc<AppState>) -> Result<()> {
.map_err(|e| AppError::Config(format!("OTG reconcile failed: {}", e)))
}
/// 应用 Video 配置变更
pub async fn apply_video_config(
state: &Arc<AppState>,
old_config: &VideoConfig,
new_config: &VideoConfig,
) -> Result<()> {
// 检查配置是否实际变更
if old_config == new_config {
tracing::info!("Video config unchanged, skipping reload");
return Ok(());
@@ -74,7 +69,6 @@ pub async fn apply_video_config(
Ok(())
}
/// 应用 Stream 配置变更
pub async fn apply_stream_config(
state: &Arc<AppState>,
old_config: &StreamConfig,
@@ -82,32 +76,24 @@ pub async fn apply_stream_config(
) -> Result<()> {
tracing::info!("Applying stream config changes...");
// 更新编码器后端
if old_config.encoder != new_config.encoder {
let encoder_backend = new_config.encoder.to_backend();
let encoder_backend = encoder_type_to_backend(new_config.encoder.clone());
tracing::info!(
"Updating encoder backend to: {:?} (from config: {:?})",
encoder_backend,
new_config.encoder
);
state
.stream_manager
.webrtc_streamer()
.update_encoder_backend(encoder_backend)
.await;
state.webrtc.update_encoder_backend(encoder_backend).await;
}
// 更新码率
if old_config.bitrate_preset != new_config.bitrate_preset {
state
.stream_manager
.webrtc_streamer()
.set_bitrate_preset(new_config.bitrate_preset)
.await
.ok(); // Ignore error if no active stream
}
// 更新 ICE 配置 (STUN/TURN)
let ice_changed = old_config.stun_server != new_config.stun_server
|| old_config.turn_server != new_config.turn_server
|| old_config.turn_username != new_config.turn_username
@@ -120,8 +106,7 @@ pub async fn apply_stream_config(
new_config.turn_server
);
state
.stream_manager
.webrtc_streamer()
.webrtc
.update_ice_config(
new_config.stun_server.clone(),
new_config.turn_server.clone(),
@@ -139,7 +124,6 @@ pub async fn apply_stream_config(
Ok(())
}
/// 应用 HID 配置变更
pub async fn apply_hid_config(
state: &Arc<AppState>,
old_config: &HidConfig,
@@ -202,7 +186,6 @@ pub async fn apply_hid_config(
Ok(())
}
/// 应用 MSD 配置变更
pub async fn apply_msd_config(
state: &Arc<AppState>,
old_config: &MsdConfig,
@@ -218,7 +201,6 @@ pub async fn apply_msd_config(
tracing::debug!("Old MSD config: {:?}", old_config);
tracing::debug!("New MSD config: {:?}", new_config);
// Check if MSD enabled state changed
let old_msd_enabled = old_config.enabled;
let new_msd_enabled = new_config.enabled;
let msd_dir_changed = old_config.msd_dir != new_config.msd_dir;
@@ -232,7 +214,6 @@ pub async fn apply_msd_config(
tracing::info!("MSD directory changed: {}", new_config.msd_dir);
}
// Ensure MSD directories exist (msd/images, msd/ventoy)
let msd_dir = new_config.msd_dir_path();
if let Err(e) = std::fs::create_dir_all(msd_dir.join("images")) {
tracing::warn!("Failed to create MSD images directory: {}", e);
@@ -255,7 +236,6 @@ pub async fn apply_msd_config(
reconcile_otg_from_store(state).await?;
// Shutdown existing controller if present
let mut msd_guard = state.msd.write().await;
if let Some(msd) = msd_guard.as_mut() {
if let Err(e) = msd.shutdown().await {
@@ -271,15 +251,12 @@ pub async fn apply_msd_config(
.await
.map_err(|e| AppError::Config(format!("MSD initialization failed: {}", e)))?;
// Set event bus
let events = state.events.clone();
msd.set_event_bus(events).await;
// Store the initialized controller
*state.msd.write().await = Some(msd);
tracing::info!("MSD initialized successfully");
} else {
// MSD disabled - shutdown
tracing::info!("MSD disabled in config, shutting down...");
let mut msd_guard = state.msd.write().await;
@@ -306,7 +283,6 @@ pub async fn apply_msd_config(
Ok(())
}
/// 应用 ATX 配置变更
pub async fn apply_atx_config(
state: &Arc<AppState>,
_old_config: &AtxConfig,
@@ -314,10 +290,8 @@ pub async fn apply_atx_config(
) -> Result<()> {
tracing::info!("Applying ATX config changes...");
// Convert AtxConfig to AtxControllerConfig
let controller_config = new_config.to_controller_config();
// Reload the ATX controller with new configuration
let atx_guard = state.atx.read().await;
if let Some(atx) = atx_guard.as_ref() {
if let Err(e) = atx.reload(controller_config).await {
@@ -326,7 +300,6 @@ pub async fn apply_atx_config(
}
tracing::info!("ATX controller reloaded successfully");
} else {
// ATX controller not initialized, create a new one if enabled
drop(atx_guard);
if new_config.enabled {
@@ -345,7 +318,6 @@ pub async fn apply_atx_config(
Ok(())
}
/// 应用 Audio 配置变更
pub async fn apply_audio_config(
state: &Arc<AppState>,
_old_config: &AudioConfig,
@@ -353,17 +325,14 @@ pub async fn apply_audio_config(
) -> Result<()> {
tracing::info!("Applying audio config changes...");
// Create audio controller config from new config
let audio_config = crate::audio::AudioControllerConfig {
enabled: new_config.enabled,
device: new_config.device.clone(),
quality: crate::audio::AudioQuality::from_str(&new_config.quality),
quality: new_config.quality.parse::<crate::audio::AudioQuality>()?,
};
// Update audio controller
if let Err(e) = state.audio.update_config(audio_config).await {
tracing::error!("Audio config update failed: {}", e);
// Don't fail - audio errors are not critical
} else {
tracing::info!(
"Audio config applied: enabled={}, device={}",
@@ -372,7 +341,6 @@ pub async fn apply_audio_config(
);
}
// Also update WebRTC audio enabled state
if let Err(e) = state
.stream_manager
.set_webrtc_audio_enabled(new_config.enabled)
@@ -383,7 +351,6 @@ pub async fn apply_audio_config(
tracing::info!("WebRTC audio enabled: {}", new_config.enabled);
}
// Reconnect audio sources for existing WebRTC sessions
if new_config.enabled {
state.stream_manager.reconnect_webrtc_audio_sources().await;
}
@@ -391,7 +358,6 @@ pub async fn apply_audio_config(
Ok(())
}
/// Apply stream codec constraints derived from global config.
pub async fn enforce_stream_codec_constraints(state: &Arc<AppState>) -> Result<Option<String>> {
let config = state.config.get();
let constraints = StreamCodecConstraints::from_config(&config);
@@ -400,7 +366,6 @@ pub async fn enforce_stream_codec_constraints(state: &Arc<AppState>) -> Result<O
Ok(enforcement.message)
}
/// 应用 RustDesk 配置变更
pub async fn apply_rustdesk_config(
state: &Arc<AppState>,
old_config: &crate::rustdesk::config::RustDeskConfig,
@@ -411,9 +376,7 @@ pub async fn apply_rustdesk_config(
let mut rustdesk_guard = state.rustdesk.write().await;
let mut credentials_to_save = None;
// Check if service needs to be stopped
if old_config.enabled && !new_config.enabled {
// Disable service
if let Some(ref service) = *rustdesk_guard {
if let Err(e) = service.stop().await {
tracing::error!("Failed to stop RustDesk service: {}", e);
@@ -423,14 +386,12 @@ pub async fn apply_rustdesk_config(
*rustdesk_guard = None;
}
// Check if service needs to be started or restarted
if new_config.enabled {
let need_restart = old_config.rendezvous_server != new_config.rendezvous_server
|| old_config.device_id != new_config.device_id
|| old_config.device_password != new_config.device_password;
if rustdesk_guard.is_none() {
// Create new service
tracing::info!("Initializing RustDesk service...");
let service = crate::rustdesk::RustDeskService::new(
new_config.clone(),
@@ -442,12 +403,10 @@ pub async fn apply_rustdesk_config(
tracing::error!("Failed to start RustDesk service: {}", e);
} else {
tracing::info!("RustDesk service started with ID: {}", new_config.device_id);
// Save generated keypair and UUID to config
credentials_to_save = service.save_credentials();
}
*rustdesk_guard = Some(std::sync::Arc::new(service));
} else if need_restart {
// Restart existing service with new config
if let Some(ref service) = *rustdesk_guard {
if let Err(e) = service.restart(new_config.clone()).await {
tracing::error!("Failed to restart RustDesk service: {}", e);
@@ -456,14 +415,12 @@ pub async fn apply_rustdesk_config(
"RustDesk service restarted with ID: {}",
new_config.device_id
);
// Save generated keypair and UUID to config
credentials_to_save = service.save_credentials();
}
}
}
}
// Save credentials to persistent config store (outside the lock)
drop(rustdesk_guard);
if let Some(updated_config) = credentials_to_save {
tracing::info!("Saving RustDesk credentials to config store...");
@@ -491,7 +448,6 @@ pub async fn apply_rustdesk_config(
Ok(())
}
/// 应用 RTSP 配置变更
pub async fn apply_rtsp_config(
state: &Arc<AppState>,
old_config: &RtspConfig,

View File

@@ -1,5 +1,3 @@
//! ATX configuration handlers
use axum::{extract::State, Json};
use std::sync::Arc;
@@ -11,29 +9,23 @@ use crate::state::AppState;
use super::apply::apply_atx_config;
use super::types::AtxConfigUpdate;
/// Get ATX configuration
pub async fn get_atx_config(State(state): State<Arc<AppState>>) -> Json<AtxConfig> {
Json(state.config.get().atx.clone())
}
/// Update ATX configuration
pub async fn update_atx_config(
State(state): State<Arc<AppState>>,
Json(req): Json<AtxConfigUpdate>,
) -> Result<Json<AtxConfig>> {
// 1. Read current configuration snapshot
let current_config = state.config.get();
let old_atx_config = current_config.atx.clone();
// 2. Validate request, including merged effective serial parameter checks
req.validate_with_current(&old_atx_config)?;
// 3. Ensure ATX serial devices do not conflict with HID CH9329 serial device
let mut merged_atx_config = old_atx_config.clone();
req.apply_to(&mut merged_atx_config);
validate_serial_device_conflict(&merged_atx_config, &current_config.hid)?;
// 4. Persist update into config store
state
.config
.update(|config| {
@@ -41,10 +33,8 @@ pub async fn update_atx_config(
})
.await?;
// 5. Load new config
let new_atx_config = state.config.get().atx.clone();
// 6. Apply to subsystem (hot reload)
if let Err(e) = apply_atx_config(&state, &old_atx_config, &new_atx_config).await {
tracing::error!("Failed to apply ATX config: {}", e);
}

View File

@@ -1,5 +1,3 @@
//! Audio 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
@@ -10,23 +8,18 @@ use crate::state::AppState;
use super::apply::apply_audio_config;
use super::types::AudioConfigUpdate;
/// 获取 Audio 配置
pub async fn get_audio_config(State(state): State<Arc<AppState>>) -> Json<AudioConfig> {
Json(state.config.get().audio.clone())
}
/// 更新 Audio 配置
pub async fn update_audio_config(
State(state): State<Arc<AppState>>,
Json(req): Json<AudioConfigUpdate>,
) -> Result<Json<AudioConfig>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_audio_config = state.config.get().audio.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
@@ -34,10 +27,8 @@ pub async fn update_audio_config(
})
.await?;
// 4. 获取新配置
let new_audio_config = state.config.get().audio.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_audio_config(&state, &old_audio_config, &new_audio_config).await {
tracing::error!("Failed to apply audio config: {}", e);
}

View File

@@ -14,7 +14,6 @@ pub async fn get_auth_config(State(state): State<Arc<AppState>>) -> Json<AuthCon
Json(auth)
}
/// Update auth configuration
pub async fn update_auth_config(
State(state): State<Arc<AppState>>,
Json(update): Json<AuthConfigUpdate>,

View File

@@ -1,5 +1,3 @@
//! HID 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
@@ -10,23 +8,18 @@ use crate::state::AppState;
use super::apply::apply_hid_config;
use super::types::HidConfigUpdate;
/// 获取 HID 配置
pub async fn get_hid_config(State(state): State<Arc<AppState>>) -> Json<HidConfig> {
Json(state.config.get().hid.clone())
}
/// 更新 HID 配置
pub async fn update_hid_config(
State(state): State<Arc<AppState>>,
Json(req): Json<HidConfigUpdate>,
) -> Result<Json<HidConfig>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_hid_config = state.config.get().hid.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
@@ -34,10 +27,8 @@ pub async fn update_hid_config(
})
.await?;
// 4. 获取新配置
let new_hid_config = state.config.get().hid.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_hid_config(&state, &old_hid_config, &new_hid_config).await {
tracing::error!("Failed to apply HID config: {}", e);
}

View File

@@ -1,21 +1,3 @@
//! 配置管理 Handler 模块
//!
//! 提供 RESTful 域分离的配置 API
//! - GET /api/config/video - 获取视频配置
//! - PATCH /api/config/video - 更新视频配置
//! - GET /api/config/stream - 获取流配置
//! - PATCH /api/config/stream - 更新流配置
//! - GET /api/config/hid - 获取 HID 配置
//! - PATCH /api/config/hid - 更新 HID 配置
//! - GET /api/config/msd - 获取 MSD 配置
//! - PATCH /api/config/msd - 更新 MSD 配置
//! - GET /api/config/atx - 获取 ATX 配置
//! - PATCH /api/config/atx - 更新 ATX 配置
//! - GET /api/config/audio - 获取音频配置
//! - PATCH /api/config/audio - 更新音频配置
//! - GET /api/config/rustdesk - 获取 RustDesk 配置
//! - PATCH /api/config/rustdesk - 更新 RustDesk 配置
pub(crate) mod apply;
mod types;
@@ -30,7 +12,6 @@ mod stream;
pub(crate) mod video;
mod web;
// 导出 handler 函数
pub use atx::{get_atx_config, update_atx_config};
pub use audio::{get_audio_config, update_audio_config};
pub use auth::{get_auth_config, update_auth_config};
@@ -45,7 +26,6 @@ pub use stream::{get_stream_config, update_stream_config};
pub use video::{get_video_config, update_video_config};
pub use web::{get_web_config, update_web_config};
// 保留全局配置查询(向后兼容)
use axum::{extract::State, Json};
use std::sync::Arc;
@@ -53,13 +33,10 @@ use crate::config::AppConfig;
use crate::state::AppState;
fn sanitize_config_for_api(config: &mut AppConfig) {
// Auth secrets
config.auth.totp_secret = None;
// Stream secrets
config.stream.turn_password = None;
// RustDesk secrets
config.rustdesk.device_password.clear();
config.rustdesk.relay_key = None;
config.rustdesk.public_key = None;
@@ -67,14 +44,11 @@ fn sanitize_config_for_api(config: &mut AppConfig) {
config.rustdesk.signing_public_key = None;
config.rustdesk.signing_private_key = None;
// RTSP secrets
config.rtsp.password = None;
}
/// 获取完整配置
pub async fn get_all_config(State(state): State<Arc<AppState>>) -> Json<AppConfig> {
let mut config = (*state.config.get()).clone();
// 不暴露敏感信息
sanitize_config_for_api(&mut config);
Json(config)
}

View File

@@ -1,5 +1,3 @@
//! MSD 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
@@ -10,23 +8,18 @@ use crate::state::AppState;
use super::apply::apply_msd_config;
use super::types::MsdConfigUpdate;
/// 获取 MSD 配置
pub async fn get_msd_config(State(state): State<Arc<AppState>>) -> Json<MsdConfig> {
Json(state.config.get().msd.clone())
}
/// 更新 MSD 配置
pub async fn update_msd_config(
State(state): State<Arc<AppState>>,
Json(req): Json<MsdConfigUpdate>,
) -> Result<Json<MsdConfig>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_msd_config = state.config.get().msd.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
@@ -34,10 +27,8 @@ pub async fn update_msd_config(
})
.await?;
// 4. 获取新配置
let new_msd_config = state.config.get().msd.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_msd_config(&state, &old_msd_config, &new_msd_config).await {
tracing::error!("Failed to apply MSD config: {}", e);
}

View File

@@ -7,13 +7,11 @@ use crate::state::AppState;
use super::apply::apply_rtsp_config;
use super::types::{RtspConfigResponse, RtspConfigUpdate, RtspStatusResponse};
/// Get RTSP config
pub async fn get_rtsp_config(State(state): State<Arc<AppState>>) -> Json<RtspConfigResponse> {
let config = state.config.get();
Json(RtspConfigResponse::from(&config.rtsp))
}
/// Get RTSP status (config + service status)
pub async fn get_rtsp_status(State(state): State<Arc<AppState>>) -> Json<RtspStatusResponse> {
let config = state.config.get().rtsp.clone();
let status = {
@@ -28,7 +26,6 @@ pub async fn get_rtsp_status(State(state): State<Arc<AppState>>) -> Json<RtspSta
Json(RtspStatusResponse::new(&config, status))
}
/// Update RTSP config
pub async fn update_rtsp_config(
State(state): State<Arc<AppState>>,
Json(req): Json<RtspConfigUpdate>,

View File

@@ -1,5 +1,3 @@
//! RustDesk 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
@@ -10,18 +8,14 @@ use crate::state::AppState;
use super::apply::apply_rustdesk_config;
use super::types::RustDeskConfigUpdate;
/// RustDesk 配置响应(隐藏敏感信息)
#[derive(Debug, serde::Serialize)]
pub struct RustDeskConfigResponse {
pub enabled: bool,
pub rendezvous_server: String,
pub relay_server: Option<String>,
pub device_id: String,
/// 是否已设置密码
pub has_password: bool,
/// 是否已设置密钥对
pub has_keypair: bool,
/// 是否已设置 relay key
pub has_relay_key: bool,
}
@@ -39,7 +33,6 @@ impl From<&RustDeskConfig> for RustDeskConfigResponse {
}
}
/// RustDesk 状态响应
#[derive(Debug, serde::Serialize)]
pub struct RustDeskStatusResponse {
pub config: RustDeskConfigResponse,
@@ -47,20 +40,17 @@ pub struct RustDeskStatusResponse {
pub rendezvous_status: Option<String>,
}
/// 获取 RustDesk 配置
pub async fn get_rustdesk_config(
State(state): State<Arc<AppState>>,
) -> Json<RustDeskConfigResponse> {
Json(RustDeskConfigResponse::from(&state.config.get().rustdesk))
}
/// 获取 RustDesk 完整状态(配置 + 服务状态)
pub async fn get_rustdesk_status(
State(state): State<Arc<AppState>>,
) -> Json<RustDeskStatusResponse> {
let config = state.config.get().rustdesk.clone();
// 获取服务状态
let (service_status, rendezvous_status) = {
let guard = state.rustdesk.read().await;
if let Some(ref service) = *guard {
@@ -79,18 +69,14 @@ pub async fn get_rustdesk_status(
})
}
/// 更新 RustDesk 配置
pub async fn update_rustdesk_config(
State(state): State<Arc<AppState>>,
Json(req): Json<RustDeskConfigUpdate>,
) -> Result<Json<RustDeskConfigResponse>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_config = state.config.get().rustdesk.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
@@ -98,15 +84,12 @@ pub async fn update_rustdesk_config(
})
.await?;
// 4. 获取新配置
let new_config = state.config.get().rustdesk.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_rustdesk_config(&state, &old_config, &new_config).await {
tracing::error!("Failed to apply RustDesk config: {}", e);
}
// Share a non-sensitive summary for frontend UX
let constraints = state.stream_manager.codec_constraints().await;
if constraints.rustdesk_enabled || constraints.rtsp_enabled {
tracing::info!(
@@ -118,7 +101,6 @@ pub async fn update_rustdesk_config(
Ok(Json(RustDeskConfigResponse::from(&new_config)))
}
/// 重新生成设备 ID
pub async fn regenerate_device_id(
State(state): State<Arc<AppState>>,
) -> Result<Json<RustDeskConfigResponse>> {
@@ -133,7 +115,6 @@ pub async fn regenerate_device_id(
Ok(Json(RustDeskConfigResponse::from(&new_config)))
}
/// 重新生成设备密码
pub async fn regenerate_device_password(
State(state): State<Arc<AppState>>,
) -> Result<Json<RustDeskConfigResponse>> {
@@ -148,7 +129,6 @@ pub async fn regenerate_device_password(
Ok(Json(RustDeskConfigResponse::from(&new_config)))
}
/// 获取设备密码(已认证用户)
pub async fn get_device_password(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
let config = state.config.get().rustdesk.clone();
Json(serde_json::json!({

View File

@@ -1,5 +1,3 @@
//! Stream 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
@@ -9,24 +7,19 @@ use crate::state::AppState;
use super::apply::apply_stream_config;
use super::types::{StreamConfigResponse, StreamConfigUpdate};
/// 获取 Stream 配置
pub async fn get_stream_config(State(state): State<Arc<AppState>>) -> Json<StreamConfigResponse> {
let config = state.config.get();
Json(StreamConfigResponse::from(&config.stream))
}
/// 更新 Stream 配置
pub async fn update_stream_config(
State(state): State<Arc<AppState>>,
Json(req): Json<StreamConfigUpdate>,
) -> Result<Json<StreamConfigResponse>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_stream_config = state.config.get().stream.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
@@ -34,15 +27,12 @@ pub async fn update_stream_config(
})
.await?;
// 4. 获取新配置
let new_stream_config = state.config.get().stream.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_stream_config(&state, &old_stream_config, &new_stream_config).await {
tracing::error!("Failed to apply stream config: {}", e);
}
// 6. Enforce codec constraints after any stream config update
if let Err(e) = super::apply::enforce_stream_codec_constraints(&state).await {
tracing::error!("Failed to enforce stream codec constraints: {}", e);
}

View File

@@ -2,13 +2,11 @@ use crate::config::*;
use crate::error::AppError;
use crate::rtsp::RtspServiceStatus;
use crate::rustdesk::config::RustDeskConfig;
use crate::video::encoder::BitratePreset;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use serde::{Deserialize, Serialize};
use std::path::Path;
use typeshare::typeshare;
// ===== Auth Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct AuthConfigUpdate {
@@ -27,7 +25,6 @@ impl AuthConfigUpdate {
}
}
// ===== Video Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct VideoConfigUpdate {
@@ -92,8 +89,6 @@ impl VideoConfigUpdate {
}
}
// ===== Stream Config =====
/// Stream configuration response (includes has_turn_password)
#[typeshare]
#[derive(Debug, serde::Serialize)]
@@ -212,8 +207,6 @@ impl StreamConfigUpdate {
}
}
// ===== HID Config =====
/// OTG USB device descriptor configuration update
#[typeshare]
#[derive(Debug, Deserialize)]
@@ -364,7 +357,6 @@ impl HidConfigUpdate {
}
}
// ===== MSD Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct MsdConfigUpdate {
@@ -398,8 +390,6 @@ impl MsdConfigUpdate {
}
}
// ===== ATX Config =====
/// Update for a single ATX key configuration
#[typeshare]
#[derive(Debug, Deserialize)]
@@ -626,7 +616,6 @@ impl AtxConfigUpdate {
}
}
// ===== Audio Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct AudioConfigUpdate {
@@ -660,8 +649,6 @@ impl AudioConfigUpdate {
}
}
// ===== RustDesk Config =====
/// hbbs/hbbr `-k` relay key: standard Base64 encoding of exactly 32 bytes (typically 44 chars with padding).
fn validate_rustdesk_relay_key(key: &str) -> Result<(), AppError> {
let decoded = STANDARD.decode(key.as_bytes()).map_err(|_| {
@@ -758,7 +745,6 @@ impl RustDeskConfigUpdate {
}
}
// ===== RTSP Config =====
#[typeshare]
#[derive(Debug, serde::Serialize)]
pub struct RtspConfigResponse {
@@ -876,8 +862,6 @@ impl RtspConfigUpdate {
}
}
// ===== Web Config =====
/// Web server settings returned by `GET` / `PATCH /api/config/web`.
///
/// Public API shape: certificate paths on disk are not exposed. The full stored model is `WebConfig` in `config::schema`.

View File

@@ -1,5 +1,3 @@
//! Video 配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
@@ -10,23 +8,18 @@ use crate::state::AppState;
use super::apply::apply_video_config;
use super::types::VideoConfigUpdate;
/// 获取 Video 配置
pub async fn get_video_config(State(state): State<Arc<AppState>>) -> Json<VideoConfig> {
Json(state.config.get().video.clone())
}
/// 更新 Video 配置
pub async fn update_video_config(
State(state): State<Arc<AppState>>,
Json(req): Json<VideoConfigUpdate>,
) -> Result<Json<VideoConfig>> {
// 1. 验证请求
req.validate()?;
// 2. 获取旧配置
let old_video_config = state.config.get().video.clone();
// 3. 应用更新到配置存储
state
.config
.update(|config| {
@@ -34,10 +27,8 @@ pub async fn update_video_config(
})
.await?;
// 4. 获取新配置
let new_video_config = state.config.get().video.clone();
// 5. 应用到子系统(热重载)
if let Err(e) = apply_video_config(&state, &old_video_config, &new_video_config).await {
tracing::error!("Failed to apply video config: {}", e);
// 根据用户选择,仅记录错误,不回滚

View File

@@ -1,5 +1,3 @@
//! Web 服务器配置 Handler
use axum::{extract::State, Json};
use axum_server::tls_rustls::RustlsConfig;
use std::sync::Arc;
@@ -9,14 +7,10 @@ use crate::state::AppState;
use super::types::{WebConfigResponse, WebConfigUpdate};
/// 获取 Web 配置
pub async fn get_web_config(
State(state): State<Arc<AppState>>,
) -> Json<WebConfigResponse> {
pub async fn get_web_config(State(state): State<Arc<AppState>>) -> Json<WebConfigResponse> {
Json(WebConfigResponse::from_stored(&state.config.get().web))
}
/// 更新 Web 配置(支持 PEM 证书上传)
pub async fn update_web_config(
State(state): State<Arc<AppState>>,
Json(req): Json<WebConfigUpdate>,
@@ -27,9 +21,13 @@ pub async fn update_web_config(
// Some(Some((cert, key))) = write new cert
// Some(None) = clear custom cert
// None = no cert change
let cert_path_update: Option<Option<(String, String)>> =
if let (Some(cert_pem), Some(key_pem)) = (&req.ssl_cert_pem, &req.ssl_key_pem) {
RustlsConfig::from_pem(cert_pem.as_bytes().to_vec(), key_pem.as_bytes().to_vec())
let cert_path_update: Option<Option<(String, String)>> = if let (
Some(cert_pem),
Some(key_pem),
) =
(&req.ssl_cert_pem, &req.ssl_key_pem)
{
RustlsConfig::from_pem(cert_pem.as_bytes().to_vec(), key_pem.as_bytes().to_vec())
.await
.map_err(|e| {
AppError::BadRequest(
@@ -39,30 +37,30 @@ pub async fn update_web_config(
.into(),
)
})?;
let cert_dir = state.data_dir().join("certs");
tokio::fs::create_dir_all(&cert_dir)
.await
.map_err(|e| AppError::Internal(format!("Failed to create cert dir: {e}")))?;
let cert_path = cert_dir.join("custom.crt");
let key_path = cert_dir.join("custom.key");
tokio::fs::write(&cert_path, cert_pem.as_bytes())
.await
.map_err(|e| AppError::Internal(format!("Failed to write certificate: {e}")))?;
tokio::fs::write(&key_path, key_pem.as_bytes())
.await
.map_err(|e| AppError::Internal(format!("Failed to write private key: {e}")))?;
Some(Some((
cert_path.to_string_lossy().into_owned(),
key_path.to_string_lossy().into_owned(),
)))
} else if req.clear_custom_cert.unwrap_or(false) {
let cert_dir = state.data_dir().join("certs");
let _ = tokio::fs::remove_file(cert_dir.join("custom.crt")).await;
let _ = tokio::fs::remove_file(cert_dir.join("custom.key")).await;
Some(None)
} else {
None
};
let cert_dir = state.data_dir().join("certs");
tokio::fs::create_dir_all(&cert_dir)
.await
.map_err(|e| AppError::Internal(format!("Failed to create cert dir: {e}")))?;
let cert_path = cert_dir.join("custom.crt");
let key_path = cert_dir.join("custom.key");
tokio::fs::write(&cert_path, cert_pem.as_bytes())
.await
.map_err(|e| AppError::Internal(format!("Failed to write certificate: {e}")))?;
tokio::fs::write(&key_path, key_pem.as_bytes())
.await
.map_err(|e| AppError::Internal(format!("Failed to write private key: {e}")))?;
Some(Some((
cert_path.to_string_lossy().into_owned(),
key_path.to_string_lossy().into_owned(),
)))
} else if req.clear_custom_cert.unwrap_or(false) {
let cert_dir = state.data_dir().join("certs");
let _ = tokio::fs::remove_file(cert_dir.join("custom.crt")).await;
let _ = tokio::fs::remove_file(cert_dir.join("custom.key")).await;
Some(None)
} else {
None
};
state
.config
@@ -82,7 +80,9 @@ pub async fn update_web_config(
})
.await?;
Ok(Json(WebConfigResponse::from_stored(&state.config.get().web)))
Ok(Json(WebConfigResponse::from_stored(
&state.config.get().web,
)))
}
#[cfg(test)]

View File

@@ -1,7 +1,3 @@
//! Device discovery handlers
//!
//! Provides API endpoints for discovering available hardware devices.
use axum::Json;
use serde::Deserialize;
@@ -9,17 +5,10 @@ use crate::atx::{discover_devices, AtxDevices};
use crate::error::{AppError, Result};
use crate::video::usb_reset;
/// GET /api/devices/atx - List available ATX devices
///
/// Returns lists of available GPIO chips and USB HID relay devices.
pub async fn list_atx_devices() -> Json<AtxDevices> {
Json(discover_devices())
}
/// GET /api/devices/usb - List all USB devices
///
/// Enumerates USB devices from `/sys/bus/usb/devices/` with associated
/// video device mappings.
pub async fn list_usb_devices() -> Json<Vec<usb_reset::UsbDeviceInfo>> {
Json(usb_reset::list_usb_devices())
}
@@ -30,11 +19,6 @@ pub struct UsbResetRequest {
pub dev_num: u32,
}
/// POST /api/devices/usb/reset - Reset a USB device via authorized cycle
///
/// Writes `0` then `1` to the device's `authorized` sysfs attribute,
/// causing the kernel to deauthorize and re-authorize the device.
/// Requires root or write access to sysfs.
pub async fn reset_usb_device(Json(req): Json<UsbResetRequest>) -> Result<Json<serde_json::Value>> {
usb_reset::reset_usb_device(req.bus_num, req.dev_num).map_err(|e| {
AppError::VideoError(format!(

View File

@@ -1,5 +1,3 @@
//! Extension management API handlers
use axum::{
extract::{Path, Query, State},
Json,
@@ -15,12 +13,6 @@ use crate::extensions::{
};
use crate::state::AppState;
// ============================================================================
// Get all extensions status
// ============================================================================
/// Get status of all extensions
/// GET /api/extensions
pub async fn list_extensions(State(state): State<Arc<AppState>>) -> Json<ExtensionsStatus> {
let config = state.config.get();
let mgr = &state.extensions;
@@ -44,12 +36,6 @@ pub async fn list_extensions(State(state): State<Arc<AppState>>) -> Json<Extensi
})
}
// ============================================================================
// Individual extension status
// ============================================================================
/// Get status of a single extension
/// GET /api/extensions/:id
pub async fn get_extension(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
@@ -66,12 +52,6 @@ pub async fn get_extension(
}))
}
// ============================================================================
// Start/Stop extensions
// ============================================================================
/// Start an extension
/// POST /api/extensions/:id/start
pub async fn start_extension(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
@@ -83,20 +63,16 @@ pub async fn start_extension(
let config = state.config.get();
let mgr = &state.extensions;
// Start the extension
mgr.start(ext_id, &config.extensions)
.await
.map_err(AppError::Internal)?;
// Return updated status
Ok(Json(ExtensionInfo {
available: mgr.check_available(ext_id),
status: mgr.status(ext_id).await,
}))
}
/// Stop an extension
/// POST /api/extensions/:id/stop
pub async fn stop_extension(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
@@ -107,29 +83,20 @@ pub async fn stop_extension(
let mgr = &state.extensions;
// Stop the extension
mgr.stop(ext_id).await.map_err(AppError::Internal)?;
// Return updated status
Ok(Json(ExtensionInfo {
available: mgr.check_available(ext_id),
status: mgr.status(ext_id).await,
}))
}
// ============================================================================
// Extension logs
// ============================================================================
/// Query parameters for logs
#[derive(Deserialize, Default)]
pub struct LogsQuery {
/// Number of lines to return (default: 100, max: 500)
pub lines: Option<usize>,
}
/// Get extension logs
/// GET /api/extensions/:id/logs
pub async fn get_extension_logs(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
@@ -145,20 +112,13 @@ pub async fn get_extension_logs(
Ok(Json(ExtensionLogs { id: ext_id, logs }))
}
// ============================================================================
// Update extension config
// ============================================================================
/// Update ttyd config
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct TtydConfigUpdate {
pub enabled: Option<bool>,
pub port: Option<u16>,
pub shell: Option<String>,
}
/// Update gostc config
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct GostcConfigUpdate {
@@ -168,7 +128,6 @@ pub struct GostcConfigUpdate {
pub tls: Option<bool>,
}
/// Update easytier config
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct EasytierConfigUpdate {
@@ -179,16 +138,12 @@ pub struct EasytierConfigUpdate {
pub virtual_ip: Option<String>,
}
/// Update ttyd configuration
/// PATCH /api/extensions/ttyd/config
pub async fn update_ttyd_config(
State(state): State<Arc<AppState>>,
Json(req): Json<TtydConfigUpdate>,
) -> Result<Json<TtydConfig>> {
// Get current config
let was_enabled = state.config.get().extensions.ttyd.enabled;
// Update config
state
.config
.update(|config| {
@@ -196,9 +151,6 @@ pub async fn update_ttyd_config(
if let Some(enabled) = req.enabled {
ttyd.enabled = enabled;
}
if let Some(port) = req.port {
ttyd.port = port;
}
if let Some(ref shell) = req.shell {
ttyd.shell = shell.clone();
}
@@ -208,12 +160,9 @@ pub async fn update_ttyd_config(
let new_config = state.config.get();
let is_enabled = new_config.extensions.ttyd.enabled;
// Handle enable/disable state change
if was_enabled && !is_enabled {
// Was running, now disabled - stop it
state.extensions.stop(ExtensionId::Ttyd).await.ok();
} else if !was_enabled && is_enabled {
// Was disabled, now enabled - start it
if state.extensions.check_available(ExtensionId::Ttyd) {
state
.extensions
@@ -226,8 +175,6 @@ pub async fn update_ttyd_config(
Ok(Json(new_config.extensions.ttyd.clone()))
}
/// Update gostc configuration
/// PATCH /api/extensions/gostc/config
pub async fn update_gostc_config(
State(state): State<Arc<AppState>>,
Json(req): Json<GostcConfigUpdate>,
@@ -276,8 +223,6 @@ pub async fn update_gostc_config(
Ok(Json(new_config.extensions.gostc.clone()))
}
/// Update easytier configuration
/// PATCH /api/extensions/easytier/config
pub async fn update_easytier_config(
State(state): State<Arc<AppState>>,
Json(req): Json<EasytierConfigUpdate>,

View File

@@ -14,16 +14,13 @@ use crate::config::{AppConfig, StreamMode};
use crate::error::{AppError, Result};
use crate::state::AppState;
use crate::update::{UpdateChannel, UpdateOverviewResponse, UpdateStatusResponse, UpgradeRequest};
use crate::utils::{hostname_uname, list_dir_names, read_trimmed};
use crate::video::codec_constraints::codec_to_id;
use crate::video::encoder::{
build_hardware_self_check_runtime_error, run_hardware_self_check, BitratePreset,
VideoEncoderSelfCheckResponse,
};
// ============================================================================
// Health & Info
// ============================================================================
/// Health check response
#[derive(Serialize)]
pub struct HealthResponse {
@@ -169,7 +166,7 @@ fn get_device_info() -> DeviceInfo {
let mem_info = get_meminfo();
DeviceInfo {
hostname: get_hostname(),
hostname: hostname_uname(),
cpu_model: get_cpu_model(),
cpu_usage: get_cpu_usage(),
memory_total: mem_info.total,
@@ -178,13 +175,6 @@ fn get_device_info() -> DeviceInfo {
}
}
/// Get system hostname
fn get_hostname() -> String {
nix::unistd::gethostname()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|_| "unknown".to_string())
}
/// Get CPU model name from /proc/cpuinfo, fallback to device-tree model
fn get_cpu_model() -> String {
let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").ok();
@@ -451,10 +441,6 @@ mod tests {
}
}
// ============================================================================
// Authentication
// ============================================================================
#[derive(Deserialize)]
pub struct LoginRequest {
pub username: String,
@@ -550,9 +536,9 @@ pub async fn auth_check(
axum::Extension(session): axum::Extension<Session>,
) -> Json<AuthCheckResponse> {
// Get user info from user_id
let username = match state.users.get(&session.user_id).await {
Ok(Some(user)) => Some(user.username),
_ => Some(session.user_id.clone()), // Fallback to user_id if user not found
let username = match state.users.single_user().await {
Ok(Some(user)) if user.id == session.user_id => Some(user.username),
_ => None,
};
Json(AuthCheckResponse {
@@ -561,10 +547,6 @@ pub async fn auth_check(
})
}
// ============================================================================
// Setup
// ============================================================================
#[derive(Serialize)]
pub struct SetupStatus {
pub initialized: bool,
@@ -630,7 +612,10 @@ pub async fn setup_init(
}
// Create single system user
state.users.create(&req.username, &req.password).await?;
state
.users
.create_first_user(&req.username, &req.password)
.await?;
// Update config
state
@@ -780,7 +765,10 @@ pub async fn setup_init(
let audio_config = crate::audio::AudioControllerConfig {
enabled: true,
device: new_config.audio.device.clone(),
quality: crate::audio::AudioQuality::from_str(&new_config.audio.quality),
quality: new_config
.audio
.quality
.parse::<crate::audio::AudioQuality>()?,
};
if let Err(e) = state.audio.update_config(audio_config).await {
tracing::warn!("Failed to start audio during setup: {}", e);
@@ -804,10 +792,6 @@ pub async fn setup_init(
}))
}
// ============================================================================
// Configuration
// ============================================================================
#[derive(Deserialize)]
pub struct UpdateConfigRequest {
#[serde(flatten)]
@@ -962,10 +946,6 @@ fn merge_json(
}
}
// ============================================================================
// Devices
// ============================================================================
#[derive(Serialize)]
pub struct DeviceList {
pub video: Vec<VideoDevice>,
@@ -1165,10 +1145,6 @@ pub async fn list_devices(State(state): State<Arc<AppState>>) -> Json<DeviceList
})
}
// ============================================================================
// Stream Control
// ============================================================================
use crate::video::streamer::StreamerStats;
use axum::{
body::Body,
@@ -1224,11 +1200,7 @@ pub async fn stream_mode_get(State(state): State<Arc<AppState>>) -> Json<StreamM
StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => {
use crate::video::encoder::VideoCodecType;
let codec = state
.stream_manager
.webrtc_streamer()
.current_video_codec()
.await;
let codec = state.stream_manager.current_video_codec().await;
match codec {
VideoCodecType::H264 => "h264".to_string(),
VideoCodecType::H265 => "h265".to_string(),
@@ -1300,11 +1272,7 @@ pub async fn stream_mode_set(
// switch_mode_transaction treats this as "no switch needed" since StreamMode
// is still WebRTC, so we handle codec change + event emission here.
let current_mode = state.stream_manager.current_mode().await;
let prev_codec = state
.stream_manager
.webrtc_streamer()
.current_video_codec()
.await;
let prev_codec = state.stream_manager.current_video_codec().await;
let codec_changed = video_codec.is_some_and(|c| c != prev_codec);
let is_codec_only_switch =
@@ -1312,12 +1280,7 @@ pub async fn stream_mode_set(
if let Some(codec) = video_codec {
info!("Setting WebRTC video codec to {:?}", codec);
if let Err(e) = state
.stream_manager
.webrtc_streamer()
.set_video_codec(codec)
.await
{
if let Err(e) = state.stream_manager.set_video_codec(codec).await {
warn!("Failed to set video codec: {}", e);
}
}
@@ -1349,11 +1312,7 @@ pub async fn stream_mode_set(
let active_mode_str = match state.stream_manager.current_mode().await {
StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => {
let codec = state
.stream_manager
.webrtc_streamer()
.current_video_codec()
.await;
let codec = state.stream_manager.current_video_codec().await;
match codec {
VideoCodecType::H264 => "h264".to_string(),
VideoCodecType::H265 => "h265".to_string(),
@@ -1452,11 +1411,7 @@ pub async fn stream_constraints_get(
let current_mode = match current_mode {
StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => {
let codec = state
.stream_manager
.webrtc_streamer()
.current_video_codec()
.await;
let codec = state.stream_manager.current_video_codec().await;
match codec {
VideoCodecType::H264 => "h264".to_string(),
VideoCodecType::H265 => "h265".to_string(),
@@ -1509,7 +1464,6 @@ pub async fn stream_set_bitrate(
// Apply to WebRTC streamer (real-time adjustment)
if let Err(e) = state
.stream_manager
.webrtc_streamer()
.set_bitrate_preset(req.bitrate_preset)
.await
{
@@ -1850,10 +1804,6 @@ fn create_mjpeg_part(jpeg_data: &[u8]) -> bytes::Bytes {
buf.freeze()
}
// ============================================================================
// WebRTC
// ============================================================================
use crate::webrtc::signaling::{AnswerResponse, IceCandidateRequest, OfferRequest};
/// Create WebRTC session
@@ -1872,11 +1822,7 @@ pub async fn webrtc_create_session(
));
}
let session_id = state
.stream_manager
.webrtc_streamer()
.create_session()
.await?;
let session_id = state.webrtc.create_session().await?;
Ok(Json(CreateSessionResponse { session_id }))
}
@@ -1894,7 +1840,7 @@ pub async fn webrtc_offer(
// Backward compatibility: `client_id` is treated as an existing session_id hint.
// New clients should not pass it; each offer creates a fresh session.
let webrtc = state.stream_manager.webrtc_streamer();
let webrtc = &state.webrtc;
let session_id = if let Some(client_id) = &req.client_id {
// Reuse only when it matches an active session ID.
if webrtc.get_session(client_id).await.is_some() {
@@ -1923,8 +1869,7 @@ pub async fn webrtc_ice_candidate(
Json(req): Json<IceCandidateRequest>,
) -> Result<Json<LoginResponse>> {
state
.stream_manager
.webrtc_streamer()
.webrtc
.add_ice_candidate(&req.session_id, req.candidate)
.await?;
@@ -1948,7 +1893,7 @@ pub struct WebRtcStatus {
}
pub async fn webrtc_status(State(state): State<Arc<AppState>>) -> Json<WebRtcStatus> {
let sessions = state.stream_manager.webrtc_streamer().list_sessions().await;
let sessions = state.webrtc.list_sessions().await;
Json(WebRtcStatus {
session_count: sessions.len(),
sessions: sessions
@@ -1971,11 +1916,7 @@ pub async fn webrtc_close_session(
State(state): State<Arc<AppState>>,
Json(req): Json<CloseSessionRequest>,
) -> Result<Json<LoginResponse>> {
state
.stream_manager
.webrtc_streamer()
.close_session(&req.session_id)
.await?;
state.webrtc.close_session(&req.session_id).await?;
Ok(Json(LoginResponse {
success: true,
@@ -2066,10 +2007,6 @@ pub async fn webrtc_ice_servers(State(state): State<Arc<AppState>>) -> Json<IceS
})
}
// ============================================================================
// HID Control
// ============================================================================
/// HID status response
#[derive(Serialize)]
pub struct HidStatus {
@@ -2144,24 +2081,6 @@ fn push_otg_check(
});
}
fn list_dir_names(path: &std::path::Path) -> Vec<String> {
let mut names = std::fs::read_dir(path)
.ok()
.into_iter()
.flatten()
.flatten()
.filter_map(|entry| entry.file_name().into_string().ok())
.collect::<Vec<_>>();
names.sort();
names
}
fn read_trimmed(path: &std::path::Path) -> Option<String> {
std::fs::read_to_string(path)
.ok()
.map(|value| value.trim().to_string())
}
fn proc_modules_has(module_name: &str) -> bool {
std::fs::read_to_string("/proc/modules")
.ok()
@@ -2870,10 +2789,6 @@ pub async fn hid_reset(State(state): State<Arc<AppState>>) -> Result<Json<LoginR
}))
}
// ============================================================================
// MSD (Mass Storage Device)
// ============================================================================
use crate::msd::{
DownloadProgress, DriveFile, DriveInfo, DriveInitRequest, ImageDownloadRequest, ImageInfo,
ImageManager, MsdConnectRequest, MsdMode, MsdState, VentoyDrive,
@@ -3073,10 +2988,6 @@ pub async fn msd_disconnect(State(state): State<Arc<AppState>>) -> Result<Json<L
}))
}
// ============================================================================
// MSD Virtual Drive
// ============================================================================
/// Get drive info
pub async fn msd_drive_info(State(state): State<Arc<AppState>>) -> Result<Json<DriveInfo>> {
let config = state.config.get();
@@ -3261,10 +3172,6 @@ pub async fn msd_drive_mkdir(
}))
}
// ============================================================================
// ATX (Power Control)
// ============================================================================
use crate::atx::{AtxState, PowerStatus};
const WOL_HISTORY_MAX_ENTRIES: i64 = 50;
@@ -3421,7 +3328,7 @@ async fn record_wol_history(state: &Arc<AppState>, mac_address: &str) -> Result<
"#,
)
.bind(mac_address)
.execute(state.config.pool())
.execute(state.db.pool())
.await?;
sqlx::query(
@@ -3435,7 +3342,7 @@ async fn record_wol_history(state: &Arc<AppState>, mac_address: &str) -> Result<
"#,
)
.bind(WOL_HISTORY_MAX_ENTRIES)
.execute(state.config.pool())
.execute(state.db.pool())
.await?;
Ok(())
@@ -3488,7 +3395,7 @@ pub async fn atx_wol_history(
"#,
)
.bind(limit as i64)
.fetch_all(state.config.pool())
.fetch_all(state.db.pool())
.await?;
let history = rows
@@ -3502,10 +3409,6 @@ pub async fn atx_wol_history(
Ok(Json(WolHistoryResponse { history }))
}
// ============================================================================
// Audio Control
// ============================================================================
use crate::audio::{AudioQuality, AudioStatus};
/// Audio status response (re-exports AudioStatus from audio module)
@@ -3554,7 +3457,7 @@ pub async fn set_audio_quality(
State(state): State<Arc<AppState>>,
Json(req): Json<SetAudioQualityRequest>,
) -> Result<Json<LoginResponse>> {
let quality = AudioQuality::from_str(&req.quality);
let quality = req.quality.parse::<AudioQuality>()?;
state.audio.set_quality(quality).await?;
Ok(Json(LoginResponse {
success: true,
@@ -3588,10 +3491,6 @@ pub async fn list_audio_devices(
Ok(Json(devices))
}
// ============================================================================
// Password Management
// ============================================================================
/// Change password request
#[derive(Deserialize)]
pub struct ChangePasswordRequest {
@@ -3607,10 +3506,14 @@ pub async fn change_password(
) -> Result<Json<LoginResponse>> {
let current_user = state
.users
.get(&session.user_id)
.single_user()
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if current_user.id != session.user_id {
return Err(AppError::AuthError("Invalid session".to_string()));
}
if req.new_password.len() < 4 {
return Err(AppError::BadRequest(
"Password must be at least 4 characters".to_string(),
@@ -3654,10 +3557,14 @@ pub async fn change_username(
) -> Result<Json<LoginResponse>> {
let current_user = state
.users
.get(&session.user_id)
.single_user()
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if current_user.id != session.user_id {
return Err(AppError::AuthError("Invalid session".to_string()));
}
if req.username.len() < 2 {
return Err(AppError::BadRequest(
"Username must be at least 2 characters".to_string(),
@@ -3688,10 +3595,6 @@ pub async fn change_username(
}))
}
// ============================================================================
// System Control
// ============================================================================
/// Restart the application
pub async fn system_restart(State(state): State<Arc<AppState>>) -> Json<LoginResponse> {
info!("System restart requested via API");
@@ -3738,10 +3641,6 @@ pub async fn system_restart(State(state): State<Arc<AppState>>) -> Json<LoginRes
})
}
// ============================================================================
// Online Update
// ============================================================================
#[derive(Deserialize)]
pub struct UpdateOverviewQuery {
pub channel: Option<UpdateChannel>,

View File

@@ -1,5 +1,3 @@
//! Terminal proxy handler - reverse proxy to ttyd via Unix socket
use axum::{
body::Body,
extract::{
@@ -21,7 +19,6 @@ use crate::error::AppError;
use crate::extensions::TTYD_SOCKET_PATH;
use crate::state::AppState;
/// Handle WebSocket upgrade for terminal
pub async fn terminal_ws(
State(_state): State<Arc<AppState>>,
OriginalUri(original_uri): OriginalUri,
@@ -32,14 +29,12 @@ pub async fn terminal_ws(
.map(|q| format!("?{}", q))
.unwrap_or_default();
// Use the tty subprotocol that ttyd expects
// ttyd expects the `tty` WebSocket subprotocol
ws.protocols(["tty"])
.on_upgrade(move |socket| handle_terminal_websocket(socket, query_string))
}
/// Handle terminal WebSocket connection - bridge browser and ttyd
async fn handle_terminal_websocket(client_ws: WebSocket, query_string: String) {
// Connect to ttyd Unix socket
let unix_stream = match UnixStream::connect(TTYD_SOCKET_PATH).await {
Ok(s) => s,
Err(e) => {
@@ -48,7 +43,6 @@ async fn handle_terminal_websocket(client_ws: WebSocket, query_string: String) {
}
};
// Build WebSocket request for ttyd with tty subprotocol
let uri_str = format!("ws://localhost/api/terminal/ws{}", query_string);
let mut request = match uri_str.into_client_request() {
Ok(r) => r,
@@ -62,7 +56,6 @@ async fn handle_terminal_websocket(client_ws: WebSocket, query_string: String) {
.headers_mut()
.insert("Sec-WebSocket-Protocol", HeaderValue::from_static("tty"));
// Create WebSocket connection to ttyd
let ws_stream = match tokio_tungstenite::client_async(request, unix_stream).await {
Ok((ws, _)) => ws,
Err(e) => {
@@ -71,11 +64,9 @@ async fn handle_terminal_websocket(client_ws: WebSocket, query_string: String) {
}
};
// Split both WebSocket connections
let (mut client_tx, mut client_rx) = client_ws.split();
let (mut ttyd_tx, mut ttyd_rx) = ws_stream.split();
// Forward messages from browser to ttyd
let client_to_ttyd = tokio::spawn(async move {
while let Some(msg) = client_rx.next().await {
let ttyd_msg = match msg {
@@ -96,7 +87,6 @@ async fn handle_terminal_websocket(client_ws: WebSocket, query_string: String) {
}
});
// Forward messages from ttyd to browser
let ttyd_to_client = tokio::spawn(async move {
while let Some(msg) = ttyd_rx.next().await {
let client_msg = match msg {
@@ -118,14 +108,12 @@ async fn handle_terminal_websocket(client_ws: WebSocket, query_string: String) {
}
});
// Wait for either direction to complete
tokio::select! {
_ = client_to_ttyd => {}
_ = ttyd_to_client => {}
}
}
/// Proxy HTTP requests to ttyd
pub async fn terminal_proxy(
State(_state): State<Arc<AppState>>,
path: Option<Path<String>>,
@@ -133,12 +121,10 @@ pub async fn terminal_proxy(
) -> Result<Response, AppError> {
let path_str = path.map(|p| p.0).unwrap_or_default();
// Connect to ttyd Unix socket
let mut unix_stream = UnixStream::connect(TTYD_SOCKET_PATH)
.await
.map_err(|e| AppError::ServiceUnavailable(format!("ttyd not running: {}", e)))?;
// Build HTTP request to forward
let method = req.method().as_str();
let query = req
.uri()
@@ -151,7 +137,6 @@ pub async fn terminal_proxy(
format!("/api/terminal/{}{}", path_str, query)
};
// Forward relevant headers
let mut headers_str = String::new();
for (name, value) in req.headers() {
if let Ok(v) = value.to_str() {
@@ -170,20 +155,17 @@ pub async fn terminal_proxy(
method, uri_path, headers_str
);
// Send request
unix_stream
.write_all(http_request.as_bytes())
.await
.map_err(|e| AppError::Internal(format!("Failed to send request: {}", e)))?;
// Read response
let mut response_buf = Vec::new();
unix_stream
.read_to_end(&mut response_buf)
.await
.map_err(|e| AppError::Internal(format!("Failed to read response: {}", e)))?;
// Parse HTTP response
let response_str = String::from_utf8_lossy(&response_buf);
let header_end = response_str
.find("\r\n\r\n")
@@ -192,7 +174,6 @@ pub async fn terminal_proxy(
let headers_part = &response_str[..header_end];
let body_start = header_end + 4;
// Parse status line
let status_line = headers_part
.lines()
.next()
@@ -203,11 +184,9 @@ pub async fn terminal_proxy(
.and_then(|s| s.parse().ok())
.unwrap_or(200);
// Build response
let mut builder =
Response::builder().status(StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK));
// Forward response headers
for line in headers_part.lines().skip(1) {
if let Some((name, value)) = line.split_once(':') {
let name = name.trim();
@@ -232,7 +211,6 @@ pub async fn terminal_proxy(
.map_err(|e| AppError::Internal(format!("Failed to build response: {}", e)))
}
/// Terminal index page
pub async fn terminal_index(
State(state): State<Arc<AppState>>,
req: Request<Body>,

View File

@@ -1,12 +1,13 @@
mod audio_ws;
mod error;
mod handlers;
mod routes;
mod static_files;
mod ws;
pub use audio_ws::audio_ws_handler;
pub use error::ErrorResponse;
pub use routes::create_router;
// StaticAssets is only available in release mode (embedded assets)
#[cfg(not(debug_assertions))]
pub use static_files::StaticAssets;
pub use ws::ws_handler;

View File

@@ -17,7 +17,6 @@ use crate::auth::auth_middleware;
use crate::hid::websocket::ws_hid_handler;
use crate::state::AppState;
/// Create the main application router
pub fn create_router(state: Arc<AppState>) -> Router {
let cors = CorsLayer::new()
.allow_origin(Any)

View File

@@ -9,38 +9,30 @@ use std::path::PathBuf;
#[cfg(debug_assertions)]
use std::sync::OnceLock;
// Only embed assets in release mode
#[cfg(not(debug_assertions))]
use rust_embed::Embed;
#[cfg(not(debug_assertions))]
/// Embedded static assets (frontend files) - only in release mode
#[derive(Embed)]
#[folder = "web/dist"]
#[prefix = ""]
pub struct StaticAssets;
/// Get the base directory for static files
/// In debug mode: relative to executable directory
/// In release mode: not used (embedded assets)
#[cfg(debug_assertions)]
fn get_static_base_dir() -> PathBuf {
static BASE_DIR: OnceLock<PathBuf> = OnceLock::new();
BASE_DIR
.get_or_init(|| {
// Try to get executable directory
if let Ok(exe_path) = std::env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
return exe_dir.join("web").join("dist");
}
}
// Fallback to current directory
PathBuf::from("web/dist")
})
.clone()
}
/// Create router for static file serving
pub fn static_file_router<S>() -> Router<S>
where
S: Clone + Send + Sync + 'static,
@@ -50,29 +42,23 @@ where
.route("/{*path}", get(static_handler))
}
/// Serve index.html for root path
async fn index_handler() -> Response<Body> {
serve_file("index.html")
}
/// Serve static files
async fn static_handler(uri: Uri) -> Response<Body> {
let path = uri.path().trim_start_matches('/');
// Try to serve the exact file
if let Some(response) = try_serve_file(path) {
return response;
}
// For SPA routing, serve index.html for non-asset paths
if !path.contains('.') {
if let Some(response) = try_serve_file("index.html") {
return response;
}
}
// If no embedded assets found, return placeholder page
// This happens when web/dist was not built before compilation
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/html; charset=utf-8")
@@ -82,7 +68,6 @@ async fn static_handler(uri: Uri) -> Response<Body> {
fn serve_file(path: &str) -> Response<Body> {
try_serve_file(path).unwrap_or_else(|| {
// If index.html not found in embedded assets, return placeholder
if path == "index.html" {
Response::builder()
.status(StatusCode::OK)
@@ -101,17 +86,14 @@ fn serve_file(path: &str) -> Response<Body> {
fn try_serve_file(path: &str) -> Option<Response<Body>> {
#[cfg(debug_assertions)]
{
// Debug mode: read from file system
let base_dir = get_static_base_dir();
let file_path = base_dir.join(path);
// Check if file exists and is within base directory (prevent directory traversal)
if !file_path.starts_with(&base_dir) {
tracing::warn!("Path traversal attempt blocked: {}", path);
return None;
}
// Normalize path to prevent directory traversal (only if file exists)
if let (Ok(normalized_path), Ok(normalized_base)) =
(file_path.canonicalize(), base_dir.canonicalize())
{
@@ -150,7 +132,6 @@ fn try_serve_file(path: &str) -> Option<Response<Body>> {
#[cfg(not(debug_assertions))]
{
// Release mode: use embedded assets
let asset = StaticAssets::get(path)?;
let mime = mime_guess::from_path(path)
@@ -168,7 +149,6 @@ fn try_serve_file(path: &str) -> Option<Response<Body>> {
}
}
/// Placeholder index.html when frontend is not built
pub fn placeholder_html() -> &'static str {
r#"<!DOCTYPE html>
<html lang="en">

View File

@@ -1,11 +1,3 @@
//! WebSocket handler for real-time event streaming
//!
//! This module provides a WebSocket endpoint at `/api/ws` that:
//! - Broadcasts system events to connected clients
//! - Supports topic-based event filtering
//! - Handles client subscription management
//! - Includes heartbeat (ping/pong) mechanism
use axum::{
extract::{
ws::{Message, WebSocket, WebSocketUpgrade},
@@ -132,48 +124,36 @@ fn rebuild_event_tasks(
}
}
/// Client-to-server message
#[derive(Debug, Deserialize)]
#[serde(tag = "type", content = "payload")]
enum ClientMessage {
/// Subscribe to event topics
#[serde(rename = "subscribe")]
Subscribe { topics: Vec<String> },
/// Unsubscribe from event topics
#[serde(rename = "unsubscribe")]
Unsubscribe { topics: Vec<String> },
/// Ping (keep-alive)
#[serde(rename = "ping")]
Ping,
}
/// WebSocket upgrade handler
///
/// This is the entry point for WebSocket connections at `/api/ws`.
/// Authentication is handled by the middleware.
pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>) -> Response {
ws.on_upgrade(move |socket| handle_socket(socket, state))
}
/// Handle WebSocket connection
async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
let (mut sender, mut receiver) = socket.split();
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
let mut event_tasks: Vec<JoinHandle<()>> = Vec::new();
// Track subscribed topics (default: none until client subscribes)
let mut subscribed_topics: Vec<String> = vec![];
info!("WebSocket client connected");
// Heartbeat interval (30 seconds)
let mut heartbeat_interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
loop {
tokio::select! {
// Receive message from client
msg = receiver.next() => {
match msg {
Some(Ok(Message::Text(text))) => {
@@ -189,7 +169,6 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
}
}
Some(Ok(Message::Ping(_))) => {
// WebSocket automatically handles ping/pong
debug!("Received ping from client");
}
Some(Ok(Message::Pong(_))) => {
@@ -207,11 +186,9 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
}
}
// Receive event from event bus
event = event_rx.recv() => {
match event {
Some(BusMessage::Event(event)) => {
// Filter event based on subscribed topics
if let Ok(json) = serialize_event(&event) {
if sender.send(Message::Text(json.into())).await.is_err() {
warn!("Failed to send event to client, disconnecting");
@@ -224,7 +201,6 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
"WebSocket client lagged by {} events on topic {}",
count, topic
);
// Send error notification to client using SystemEvent::Error
let error_event = SystemEvent::Error {
message: format!("Lagged by {} events", count),
};
@@ -239,7 +215,6 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
}
}
// Heartbeat
_ = heartbeat_interval.tick() => {
if sender.send(Message::Ping(vec![].into())).await.is_err() {
warn!("Failed to send ping, disconnecting");
@@ -256,7 +231,6 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
info!("WebSocket handler exiting");
}
/// Handle message from client
async fn handle_client_message(
text: &str,
topics: &mut Vec<String>,
@@ -282,7 +256,6 @@ async fn handle_client_message(
Ok(())
}
/// Serialize event to JSON string
fn serialize_event(event: &SystemEvent) -> Result<String, serde_json::Error> {
serde_json::to_string(event)
}