mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
- 后端:切换事务+transition_id,/stream/mode 返回 switching/transition_id 与实际 codec - 事件:新增 mode_switching/mode_ready,config/webrtc_ready/mode_changed 关联事务 - 编码/格式:扩展 NV21/NV16/NV24/RGB/BGR 输入与转换链路,RKMPP direct input 优化 - 前端:useVideoSession 统一切换,失败回退真实切回 MJPEG,菜单格式同步修复 - 清理:useVideoStream 降级为 MJPEG-only
242 lines
7.7 KiB
Rust
242 lines
7.7 KiB
Rust
//! Terminal proxy handler - reverse proxy to ttyd via Unix socket
|
|
|
|
use axum::{
|
|
body::Body,
|
|
extract::{
|
|
ws::{Message as AxumMessage, WebSocket, WebSocketUpgrade},
|
|
OriginalUri, Path, State,
|
|
},
|
|
http::{Request, StatusCode},
|
|
response::Response,
|
|
};
|
|
use futures::{SinkExt, StreamExt};
|
|
use std::sync::Arc;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::net::UnixStream;
|
|
use tokio_tungstenite::tungstenite::{
|
|
client::IntoClientRequest, http::HeaderValue, Message as TungsteniteMessage,
|
|
};
|
|
|
|
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,
|
|
ws: WebSocketUpgrade,
|
|
) -> Response {
|
|
let query_string = original_uri
|
|
.query()
|
|
.map(|q| format!("?{}", q))
|
|
.unwrap_or_default();
|
|
|
|
// Use the tty subprotocol that ttyd expects
|
|
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) => {
|
|
tracing::error!("Failed to connect to ttyd socket: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// 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,
|
|
Err(e) => {
|
|
tracing::error!("Failed to create WebSocket request: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
request
|
|
.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) => {
|
|
tracing::error!("Failed to establish WebSocket with ttyd: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// 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 {
|
|
Ok(AxumMessage::Text(text)) => TungsteniteMessage::Text(text.to_string().into()),
|
|
Ok(AxumMessage::Binary(data)) => TungsteniteMessage::Binary(data),
|
|
Ok(AxumMessage::Ping(data)) => TungsteniteMessage::Ping(data),
|
|
Ok(AxumMessage::Pong(data)) => TungsteniteMessage::Pong(data),
|
|
Ok(AxumMessage::Close(_)) => {
|
|
let _ = ttyd_tx.send(TungsteniteMessage::Close(None)).await;
|
|
break;
|
|
}
|
|
Err(_) => break,
|
|
};
|
|
|
|
if ttyd_tx.send(ttyd_msg).await.is_err() {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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 {
|
|
Ok(TungsteniteMessage::Text(text)) => AxumMessage::Text(text.to_string().into()),
|
|
Ok(TungsteniteMessage::Binary(data)) => AxumMessage::Binary(data),
|
|
Ok(TungsteniteMessage::Ping(data)) => AxumMessage::Ping(data),
|
|
Ok(TungsteniteMessage::Pong(data)) => AxumMessage::Pong(data),
|
|
Ok(TungsteniteMessage::Close(_)) => {
|
|
let _ = client_tx.send(AxumMessage::Close(None)).await;
|
|
break;
|
|
}
|
|
Ok(TungsteniteMessage::Frame(_)) => continue,
|
|
Err(_) => break,
|
|
};
|
|
|
|
if client_tx.send(client_msg).await.is_err() {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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>>,
|
|
req: Request<Body>,
|
|
) -> 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()
|
|
.query()
|
|
.map(|q| format!("?{}", q))
|
|
.unwrap_or_default();
|
|
let uri_path = if path_str.is_empty() {
|
|
format!("/api/terminal/{}", query)
|
|
} else {
|
|
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() {
|
|
let name_lower = name.as_str().to_lowercase();
|
|
if !matches!(
|
|
name_lower.as_str(),
|
|
"connection" | "keep-alive" | "transfer-encoding" | "upgrade"
|
|
) {
|
|
headers_str.push_str(&format!("{}: {}\r\n", name, v));
|
|
}
|
|
}
|
|
}
|
|
|
|
let http_request = format!(
|
|
"{} {} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n{}\r\n",
|
|
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")
|
|
.ok_or_else(|| AppError::Internal("Invalid HTTP response".to_string()))?;
|
|
|
|
let headers_part = &response_str[..header_end];
|
|
let body_start = header_end + 4;
|
|
|
|
// Parse status line
|
|
let status_line = headers_part
|
|
.lines()
|
|
.next()
|
|
.ok_or_else(|| AppError::Internal("Missing status line".to_string()))?;
|
|
let status_code: u16 = status_line
|
|
.split_whitespace()
|
|
.nth(1)
|
|
.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();
|
|
let value = value.trim();
|
|
if !matches!(
|
|
name.to_lowercase().as_str(),
|
|
"connection" | "keep-alive" | "transfer-encoding"
|
|
) {
|
|
builder = builder.header(name, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
let body = if body_start < response_buf.len() {
|
|
Body::from(response_buf[body_start..].to_vec())
|
|
} else {
|
|
Body::empty()
|
|
};
|
|
|
|
builder
|
|
.body(body)
|
|
.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>,
|
|
) -> Result<Response, AppError> {
|
|
terminal_proxy(State(state), None, req).await
|
|
}
|