Merge branch 'main' into main

This commit is contained in:
SilentWind
2026-02-20 14:19:38 +08:00
committed by GitHub
111 changed files with 7290 additions and 1787 deletions

View File

@@ -7,7 +7,11 @@ use std::sync::Arc;
use crate::config::*;
use crate::error::{AppError, Result};
use crate::events::SystemEvent;
use crate::rtsp::RtspService;
use crate::state::AppState;
use crate::video::codec_constraints::{
enforce_constraints_with_stream_manager, StreamCodecConstraints,
};
/// 应用 Video 配置变更
pub async fn apply_video_config(
@@ -191,9 +195,7 @@ pub async fn apply_hid_config(
// Low-endpoint UDCs (e.g., musb) cannot handle consumer control endpoints reliably
if new_config.backend == HidBackend::Otg {
if let Some(udc) =
crate::otg::configfs::resolve_udc_name(new_config.otg_udc.as_deref())
{
if let Some(udc) = crate::otg::configfs::resolve_udc_name(new_config.otg_udc.as_deref()) {
if crate::otg::configfs::is_low_endpoint_udc(&udc) && new_hid_functions.consumer {
tracing::warn!(
"UDC {} has low endpoint resources, disabling consumer control",
@@ -446,6 +448,15 @@ 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);
let enforcement =
enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await?;
Ok(enforcement.message)
}
/// 应用 RustDesk 配置变更
pub async fn apply_rustdesk_config(
state: &Arc<AppState>,
@@ -455,6 +466,7 @@ pub async fn apply_rustdesk_config(
tracing::info!("Applying RustDesk config changes...");
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 {
@@ -466,7 +478,6 @@ pub async fn apply_rustdesk_config(
tracing::info!("RustDesk service stopped");
}
*rustdesk_guard = None;
return Ok(());
}
// Check if service needs to be started or restarted
@@ -475,8 +486,6 @@ pub async fn apply_rustdesk_config(
|| old_config.device_id != new_config.device_id
|| old_config.device_password != new_config.device_password;
let mut credentials_to_save = None;
if rustdesk_guard.is_none() {
// Create new service
tracing::info!("Initializing RustDesk service...");
@@ -509,28 +518,82 @@ pub async fn apply_rustdesk_config(
}
}
}
}
// 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...");
if let Err(e) = state
.config
.update(|cfg| {
cfg.rustdesk.public_key = updated_config.public_key.clone();
cfg.rustdesk.private_key = updated_config.private_key.clone();
cfg.rustdesk.signing_public_key = updated_config.signing_public_key.clone();
cfg.rustdesk.signing_private_key = updated_config.signing_private_key.clone();
cfg.rustdesk.uuid = updated_config.uuid.clone();
})
.await
{
tracing::warn!("Failed to save RustDesk credentials: {}", e);
} else {
tracing::info!("RustDesk credentials saved successfully");
}
// 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...");
if let Err(e) = state
.config
.update(|cfg| {
cfg.rustdesk.public_key = updated_config.public_key.clone();
cfg.rustdesk.private_key = updated_config.private_key.clone();
cfg.rustdesk.signing_public_key = updated_config.signing_public_key.clone();
cfg.rustdesk.signing_private_key = updated_config.signing_private_key.clone();
cfg.rustdesk.uuid = updated_config.uuid.clone();
})
.await
{
tracing::warn!("Failed to save RustDesk credentials: {}", e);
} else {
tracing::info!("RustDesk credentials saved successfully");
}
}
if let Some(message) = enforce_stream_codec_constraints(state).await? {
tracing::info!("{}", message);
}
Ok(())
}
/// 应用 RTSP 配置变更
pub async fn apply_rtsp_config(
state: &Arc<AppState>,
old_config: &RtspConfig,
new_config: &RtspConfig,
) -> Result<()> {
tracing::info!("Applying RTSP config changes...");
let mut rtsp_guard = state.rtsp.write().await;
if old_config.enabled && !new_config.enabled {
if let Some(ref service) = *rtsp_guard {
if let Err(e) = service.stop().await {
tracing::error!("Failed to stop RTSP service: {}", e);
}
}
*rtsp_guard = None;
}
if new_config.enabled {
let need_restart = old_config.bind != new_config.bind
|| old_config.port != new_config.port
|| old_config.path != new_config.path
|| old_config.codec != new_config.codec
|| old_config.username != new_config.username
|| old_config.password != new_config.password
|| old_config.allow_one_client != new_config.allow_one_client;
if rtsp_guard.is_none() {
let service = RtspService::new(new_config.clone(), state.stream_manager.clone());
service.start().await?;
tracing::info!("RTSP service started");
*rtsp_guard = Some(Arc::new(service));
} else if need_restart {
if let Some(ref service) = *rtsp_guard {
service.restart(new_config.clone()).await?;
tracing::info!("RTSP service restarted");
}
}
}
drop(rtsp_guard);
if let Some(message) = enforce_stream_codec_constraints(state).await? {
tracing::info!("{}", message);
}
Ok(())
}

View File

@@ -24,6 +24,7 @@ mod audio;
mod auth;
mod hid;
mod msd;
mod rtsp;
mod rustdesk;
mod stream;
pub(crate) mod video;
@@ -35,6 +36,7 @@ pub use audio::{get_audio_config, update_audio_config};
pub use auth::{get_auth_config, update_auth_config};
pub use hid::{get_hid_config, update_hid_config};
pub use msd::{get_msd_config, update_msd_config};
pub use rtsp::{get_rtsp_config, get_rtsp_status, update_rtsp_config};
pub use rustdesk::{
get_device_password, get_rustdesk_config, get_rustdesk_status, regenerate_device_id,
regenerate_device_password, update_rustdesk_config,
@@ -50,10 +52,29 @@ use std::sync::Arc;
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;
config.rustdesk.private_key = None;
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();
// 不暴露敏感信息
config.auth.totp_secret = None;
sanitize_config_for_api(&mut config);
Json(config)
}

View File

@@ -0,0 +1,70 @@
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::error::{AppError, Result};
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 = {
let guard = state.rtsp.read().await;
if let Some(ref service) = *guard {
service.status().await
} else {
crate::rtsp::RtspServiceStatus::Stopped
}
};
Json(RtspStatusResponse::new(&config, status))
}
/// Update RTSP config
pub async fn update_rtsp_config(
State(state): State<Arc<AppState>>,
Json(req): Json<RtspConfigUpdate>,
) -> Result<Json<RtspConfigResponse>> {
req.validate()?;
let old_config = state.config.get().rtsp.clone();
state
.config
.update(|config| {
req.apply_to(&mut config.rtsp);
})
.await?;
let new_config = state.config.get().rtsp.clone();
if let Err(err) = apply_rtsp_config(&state, &old_config, &new_config).await {
tracing::error!("Failed to apply RTSP config: {}", err);
if let Err(rollback_err) = state
.config
.update(|config| {
config.rtsp = old_config.clone();
})
.await
{
tracing::error!(
"Failed to rollback RTSP config after apply failure: {}",
rollback_err
);
return Err(AppError::ServiceUnavailable(format!(
"RTSP apply failed: {}; rollback failed: {}",
err, rollback_err
)));
}
return Err(err);
}
Ok(Json(RtspConfigResponse::from(&new_config)))
}

View File

@@ -106,6 +106,15 @@ pub async fn update_rustdesk_config(
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!(
"Stream codec constraints active after RustDesk update: {}",
constraints.reason
);
}
Ok(Json(RustDeskConfigResponse::from(&new_config)))
}
@@ -139,7 +148,7 @@ 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

@@ -42,5 +42,10 @@ pub async fn update_stream_config(
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);
}
Ok(Json(StreamConfigResponse::from(&new_stream_config)))
}

View File

@@ -1,5 +1,6 @@
use crate::config::*;
use crate::error::AppError;
use crate::rtsp::RtspServiceStatus;
use crate::rustdesk::config::RustDeskConfig;
use crate::video::encoder::BitratePreset;
use serde::Deserialize;
@@ -604,6 +605,124 @@ impl RustDeskConfigUpdate {
}
}
// ===== RTSP Config =====
#[typeshare]
#[derive(Debug, serde::Serialize)]
pub struct RtspConfigResponse {
pub enabled: bool,
pub bind: String,
pub port: u16,
pub path: String,
pub allow_one_client: bool,
pub codec: RtspCodec,
pub username: Option<String>,
pub has_password: bool,
}
impl From<&RtspConfig> for RtspConfigResponse {
fn from(config: &RtspConfig) -> Self {
Self {
enabled: config.enabled,
bind: config.bind.clone(),
port: config.port,
path: config.path.clone(),
allow_one_client: config.allow_one_client,
codec: config.codec.clone(),
username: config.username.clone(),
has_password: config.password.is_some(),
}
}
}
#[typeshare]
#[derive(Debug, serde::Serialize)]
pub struct RtspStatusResponse {
pub config: RtspConfigResponse,
pub service_status: String,
}
impl RtspStatusResponse {
pub fn new(config: &RtspConfig, status: RtspServiceStatus) -> Self {
Self {
config: RtspConfigResponse::from(config),
service_status: status.to_string(),
}
}
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct RtspConfigUpdate {
pub enabled: Option<bool>,
pub bind: Option<String>,
pub port: Option<u16>,
pub path: Option<String>,
pub allow_one_client: Option<bool>,
pub codec: Option<RtspCodec>,
pub username: Option<String>,
pub password: Option<String>,
}
impl RtspConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
if let Some(port) = self.port {
if port == 0 {
return Err(AppError::BadRequest("RTSP port cannot be 0".into()));
}
}
if let Some(ref bind) = self.bind {
if bind.parse::<std::net::IpAddr>().is_err() {
return Err(AppError::BadRequest("RTSP bind must be a valid IP".into()));
}
}
if let Some(ref path) = self.path {
let normalized = path.trim_matches('/');
if normalized.is_empty() {
return Err(AppError::BadRequest("RTSP path cannot be empty".into()));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut RtspConfig) {
if let Some(enabled) = self.enabled {
config.enabled = enabled;
}
if let Some(ref bind) = self.bind {
config.bind = bind.clone();
}
if let Some(port) = self.port {
config.port = port;
}
if let Some(ref path) = self.path {
config.path = path.trim_matches('/').to_string();
}
if let Some(allow_one_client) = self.allow_one_client {
config.allow_one_client = allow_one_client;
}
if let Some(codec) = self.codec.clone() {
config.codec = codec;
}
if let Some(ref username) = self.username {
config.username = if username.is_empty() {
None
} else {
Some(username.clone())
};
}
if let Some(ref password) = self.password {
config.password = if password.is_empty() {
None
} else {
Some(password.clone())
};
}
}
}
// ===== Web Config =====
#[typeshare]
#[derive(Debug, Deserialize)]

View File

@@ -86,7 +86,7 @@ pub async fn start_extension(
// Start the extension
mgr.start(ext_id, &config.extensions)
.await
.map_err(|e| AppError::Internal(e))?;
.map_err(AppError::Internal)?;
// Return updated status
Ok(Json(ExtensionInfo {
@@ -108,7 +108,7 @@ pub async fn stop_extension(
let mgr = &state.extensions;
// Stop the extension
mgr.stop(ext_id).await.map_err(|e| AppError::Internal(e))?;
mgr.stop(ext_id).await.map_err(AppError::Internal)?;
// Return updated status
Ok(Json(ExtensionInfo {
@@ -156,7 +156,6 @@ pub struct TtydConfigUpdate {
pub enabled: Option<bool>,
pub port: Option<u16>,
pub shell: Option<String>,
pub credential: Option<String>,
}
/// Update gostc config
@@ -203,9 +202,6 @@ pub async fn update_ttyd_config(
if let Some(ref shell) = req.shell {
ttyd.shell = shell.clone();
}
if req.credential.is_some() {
ttyd.credential = req.credential.clone();
}
})
.await?;
@@ -263,14 +259,16 @@ pub async fn update_gostc_config(
if was_enabled && !is_enabled {
state.extensions.stop(ExtensionId::Gostc).await.ok();
} else if !was_enabled && is_enabled && has_key {
if state.extensions.check_available(ExtensionId::Gostc) {
state
.extensions
.start(ExtensionId::Gostc, &new_config.extensions)
.await
.ok();
}
} else if !was_enabled
&& is_enabled
&& has_key
&& state.extensions.check_available(ExtensionId::Gostc)
{
state
.extensions
.start(ExtensionId::Gostc, &new_config.extensions)
.await
.ok();
}
Ok(Json(new_config.extensions.gostc.clone()))
@@ -312,14 +310,16 @@ pub async fn update_easytier_config(
if was_enabled && !is_enabled {
state.extensions.stop(ExtensionId::Easytier).await.ok();
} else if !was_enabled && is_enabled && has_name {
if state.extensions.check_available(ExtensionId::Easytier) {
state
.extensions
.start(ExtensionId::Easytier, &new_config.extensions)
.await
.ok();
}
} else if !was_enabled
&& is_enabled
&& has_name
&& state.extensions.check_available(ExtensionId::Easytier)
{
state
.extensions
.start(ExtensionId::Easytier, &new_config.extensions)
.await
.ok();
}
Ok(Json(new_config.extensions.easytier.clone()))

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/stream/mode", post(handlers::stream_mode_set))
.route("/stream/bitrate", post(handlers::stream_set_bitrate))
.route("/stream/codecs", get(handlers::stream_codecs_list))
.route("/stream/constraints", get(handlers::stream_constraints_get))
// WebRTC endpoints
.route("/webrtc/session", post(handlers::webrtc_create_session))
.route("/webrtc/offer", post(handlers::webrtc_offer))
@@ -59,6 +60,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/webrtc/close", post(handlers::webrtc_close_session))
// HID endpoints
.route("/hid/status", get(handlers::hid_status))
.route("/hid/otg/self-check", get(handlers::hid_otg_self_check))
.route("/hid/reset", post(handlers::hid_reset))
// WebSocket HID endpoint (for MJPEG mode)
.route("/ws/hid", any(ws_hid_handler))
@@ -120,6 +122,13 @@ pub fn create_router(state: Arc<AppState>) -> Router {
"/config/rustdesk/regenerate-password",
post(handlers::config::regenerate_device_password),
)
// RTSP configuration endpoints
.route("/config/rtsp", get(handlers::config::get_rtsp_config))
.route("/config/rtsp", patch(handlers::config::update_rtsp_config))
.route(
"/config/rtsp/status",
get(handlers::config::get_rtsp_status),
)
// Web server configuration
.route("/config/web", get(handlers::config::get_web_config))
.route("/config/web", patch(handlers::config::update_web_config))
@@ -128,6 +137,9 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/config/auth", patch(handlers::config::update_auth_config))
// System control
.route("/system/restart", post(handlers::system_restart))
.route("/update/overview", get(handlers::update_overview))
.route("/update/upgrade", post(handlers::update_upgrade))
.route("/update/status", get(handlers::update_status))
// MSD (Mass Storage Device) endpoints
.route("/msd/status", get(handlers::msd_status))
.route("/msd/images", get(handlers::msd_images_list))
@@ -158,6 +170,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/atx/status", get(handlers::atx_status))
.route("/atx/power", post(handlers::atx_power))
.route("/atx/wol", post(handlers::atx_wol))
.route("/atx/wol/history", get(handlers::atx_wol_history))
// Device discovery endpoints
.route("/devices/atx", get(handlers::devices::list_atx_devices))
// Extension management endpoints

View File

@@ -127,14 +127,14 @@ fn try_serve_file(path: &str) -> Option<Response<Body>> {
.first_or_octet_stream()
.to_string();
return Some(
Some(
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime)
.header(header::CACHE_CONTROL, "public, max-age=86400")
.body(Body::from(data))
.unwrap(),
);
)
}
Err(e) => {
tracing::debug!(
@@ -143,7 +143,7 @@ fn try_serve_file(path: &str) -> Option<Response<Body>> {
file_path.display(),
e
);
return None;
None
}
}
}