添加qiming-rcoder模块

This commit is contained in:
Codex
2026-06-01 13:54:52 +08:00
parent 8092c4b1f8
commit 4b1a580132
539 changed files with 151650 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
//! 代理配置模块
//!
//! 定义了端口反向代理的配置结构体和相关功能。
use structopt::StructOpt;
/// 端口反向代理配置
#[derive(Debug, Clone, StructOpt)]
pub struct ProxyConfig {
/// 监听端口
#[structopt(long, default_value = "8080")]
pub listen_port: u16,
/// 默认后端端口(当 URL 中没有 port 参数时使用)
#[structopt(long, default_value = "3000")]
pub default_backend_port: u16,
/// 后端服务主机(默认为 localhost
#[structopt(long, default_value = "127.0.0.1")]
pub backend_host: String,
/// URL 中端口参数的名称
#[structopt(long, default_value = "port")]
pub port_param: String,
/// Pingora 配置文件路径
#[structopt(long)]
pub config_file: Option<String>,
/// 启用详细日志
#[structopt(long)]
pub verbose: bool,
}
impl Default for ProxyConfig {
fn default() -> Self {
Self {
listen_port: 8080,
default_backend_port: 3000,
backend_host: "127.0.0.1".to_string(),
port_param: "port".to_string(),
config_file: None,
verbose: false,
}
}
}
impl ProxyConfig {
/// 验证配置的有效性
pub fn validate(&self) -> Result<(), String> {
if self.listen_port == 0 {
return Err("Listen port cannot be 0".to_string());
}
if self.default_backend_port == 0 {
return Err("Default backend port cannot be 0".to_string());
}
if self.backend_host.is_empty() {
return Err("Backend host address cannot be empty".to_string());
}
if self.port_param.is_empty() {
return Err("Port parameter name cannot be empty".to_string());
}
Ok(())
}
/// 创建默认配置
pub fn new() -> Self {
Self::default()
}
/// 创建自定义监听端口的配置
pub fn with_listen_port(port: u16) -> Self {
Self {
listen_port: port,
..Self::default()
}
}
/// 设置后端主机
pub fn with_backend_host(mut self, host: impl Into<String>) -> Self {
self.backend_host = host.into();
self
}
/// 设置端口参数名
pub fn with_port_param(mut self, param: impl Into<String>) -> Self {
self.port_param = param.into();
self
}
}

View File

@@ -0,0 +1,255 @@
//! Pingora 反向代理库
//!
//! 这个库提供了一个基于端口参数的反向代理服务,可以通过 URL 参数中的端口信息
//! 将请求代理到对应的后端服务。主要用于 Docker 容器中统一端口访问多个前端应用。
//!
//! # 特性
//!
//! - **端口路由**: 支持通过查询参数 (`?port=3000`) 或路径 (`/proxy/3000/path`) 指定目标端口
//! - **动态后端管理**: 运行时添加、移除和管理后端服务
//! - **高性能**: 基于 Axum 和 Reqwest 的异步处理
//! - **灵活配置**: 支持命令行参数和配置文件
//! - **完整日志**: 详细的操作日志和错误处理
//!
//! # 快速开始
//!
//! ```rust,ignore
//! use rcoder_proxy::{ProxyConfig, ProxyServer};
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//! let config = ProxyConfig {
//! listen_port: 8080,
//! default_backend_port: 3000,
//! backend_host: "127.0.0.1".to_string(),
//! port_param: "port".to_string(),
//! config_file: None,
//! verbose: false,
//! };
//!
//! let server = ProxyServer::new(config);
//! server.start().await?;
//!
//! Ok(())
//! }
//! ```
//!
//! # 使用方式
//!
//! ## 查询参数方式
//!
//! ```bash
//! curl "http://localhost:8080?port=3000"
//! curl "http://localhost:8080/api/data?port=3001&format=json"
//! ```
//!
//! ## 路径方式
//!
//! ```bash
//! curl "http://localhost:8080/proxy/3000/"
//! curl "http://localhost:8080/proxy/3001/api/users"
//! ```
//!
//! ## 默认端口
//!
//! ```bash
//! curl "http://localhost:8080/" # 自动路由到默认端口 3000
//! ```
// 导入模块
pub mod config;
pub mod pingora_server;
pub mod router;
pub mod server;
pub mod service;
pub mod vnc_resolver;
// 重新导出公共接口
pub use config::ProxyConfig;
pub use pingora_server::PingoraServerManager;
pub use router::{RouteType, create_router, get_routes_documentation};
pub use server::{PingoraServerRunner, ProxyServer, ProxyServerBuilder};
pub use service::{PingoraProxyService, PortProxyService}; // PortProxyService 是别名
pub use vnc_resolver::{
DynVncBackendResolver, VncBackendInfo, VncBackendResolver, VncResolveError,
};
// 库级别的常量和类型
pub const DEFAULT_PORT: u16 = 8080;
pub const DEFAULT_BACKEND_PORT: u16 = 3000;
pub const DEFAULT_BACKEND_HOST: &str = "127.0.0.1";
pub const DEFAULT_PORT_PARAM: &str = "port";
/// 库版本信息
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
/// 库的主要特性
pub mod features {
/// 支持查询参数端口提取
pub const QUERY_PARAM_ROUTING: bool = true;
/// 支持路径端口提取
pub const PATH_ROUTING: bool = true;
/// 支持动态后端管理
pub const DYNAMIC_BACKENDS: bool = true;
/// 支持配置文件
pub const CONFIG_FILE_SUPPORT: bool = true;
/// 支持详细日志
pub const VERBOSE_LOGGING: bool = true;
}
/// 便捷函数:创建默认配置
pub fn default_config() -> ProxyConfig {
ProxyConfig::default()
}
/// 便捷函数:创建指定端口的配置
pub fn config_with_port(port: u16) -> ProxyConfig {
ProxyConfig::with_listen_port(port)
}
/// 便捷函数:创建默认代理服务器
pub fn default_server() -> ProxyServer {
ProxyServer::default()
}
/// 便捷函数:创建指定端口的代理服务器
pub fn server_with_port(port: u16) -> ProxyServer {
ProxyServer::with_listen_port(port)
}
/// 便捷函数:创建自定义配置的代理服务器
pub fn server_with_config<F>(config_fn: F) -> ProxyServer
where
F: FnOnce(ProxyConfig) -> ProxyConfig,
{
let config = config_fn(default_config());
ProxyServer::new(config)
}
/// 快速启动代理服务器
///
/// # 参数
///
/// * `listen_port` - 监听端口
/// * `default_backend_port` - 默认后端端口
/// * `backend_host` - 后端主机地址
///
/// # 示例
///
/// ```rust,ignore
/// use rcoder_proxy::quick_start;
///
/// #[tokio::main]
/// async fn main() -> anyhow::Result<()> {
/// quick_start(8080, 3000, "127.0.0.1").await?;
/// Ok(())
/// }
/// ```
pub async fn quick_start(
listen_port: u16,
default_backend_port: u16,
backend_host: &str,
) -> anyhow::Result<()> {
let config = ProxyConfig {
listen_port,
default_backend_port,
backend_host: backend_host.to_string(),
port_param: DEFAULT_PORT_PARAM.to_string(),
config_file: None,
verbose: false,
};
let server = ProxyServer::new(config);
server.start().await
}
/// Proxy error type
#[derive(Debug, thiserror::Error)]
pub enum ProxyError {
#[error("config error: {0}")]
Config(String),
#[error("network error: {0}")]
Network(String),
#[error("backend error: {0}")]
Backend(String),
#[error("port extraction error: {0}")]
PortExtraction(String),
#[error("request handling error: {0}")]
RequestHandling(String),
}
impl From<anyhow::Error> for ProxyError {
fn from(err: anyhow::Error) -> Self {
ProxyError::RequestHandling(err.to_string())
}
}
/// 代理结果类型
pub type ProxyResult<T> = Result<T, ProxyError>;
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_library_constants() {
assert_eq!(DEFAULT_PORT, 8080);
assert_eq!(DEFAULT_BACKEND_PORT, 3000);
assert_eq!(DEFAULT_BACKEND_HOST, "127.0.0.1");
assert_eq!(DEFAULT_PORT_PARAM, "port");
assert!(!VERSION.is_empty());
}
#[test]
fn test_convenience_functions() {
// 测试默认配置
let config = default_config();
assert_eq!(config.listen_port, 8080);
// 测试指定端口配置
let config = config_with_port(9090);
assert_eq!(config.listen_port, 9090);
// 测试默认服务器
let server = default_server();
assert_eq!(server.listen_port(), 8080);
// 测试指定端口服务器
let server = server_with_port(9090);
assert_eq!(server.listen_port(), 9090);
// 测试自定义配置服务器
let server = server_with_config(|mut config| {
config.listen_port = 8080;
config.backend_host = "localhost".to_string();
config
});
assert_eq!(server.config().backend_host, "localhost");
}
#[test]
fn test_features() {
assert!(features::QUERY_PARAM_ROUTING);
assert!(features::PATH_ROUTING);
assert!(features::DYNAMIC_BACKENDS);
assert!(features::CONFIG_FILE_SUPPORT);
assert!(features::VERBOSE_LOGGING);
}
#[test]
fn test_proxy_error() {
let error = ProxyError::Config("Invalid port".to_string());
assert!(error.to_string().contains("配置错误"));
let error = ProxyError::Network("Connection failed".to_string());
assert!(error.to_string().contains("网络错误"));
}
}

View File

@@ -0,0 +1,242 @@
//! Pingora 服务器启动和管理模块
//!
//! 提供基于 Pingora 库的完整反向代理服务器启动功能,支持 HTTP/1.1 和 HTTP/2。
use anyhow::Result;
use arc_swap::ArcSwap;
use dashmap::DashMap;
use std::sync::Arc;
use tokio::sync::oneshot;
use tracing::{error, info};
use pingora_core::Result as PingoraResult;
use pingora_core::protocols::Digest;
use pingora_core::server::Server;
use pingora_core::server::configuration::Opt;
use pingora_core::upstreams::peer::HttpPeer;
use crate::config::ProxyConfig;
use crate::service::{PingoraProxyService, PortProxy};
use shared_types::ModelProviderConfig;
/// Pingora 服务器管理器
pub struct PingoraServerManager {
config: ProxyConfig,
service: Arc<PingoraProxyService>,
}
impl PingoraServerManager {
/// 创建新的 Pingora 服务器管理器
pub fn new(config: ProxyConfig) -> Self {
let service = Arc::new(PingoraProxyService::new(config.clone()));
Self { config, service }
}
/// 设置共享的 API 密钥管理器
///
/// 这个方法允许从外部传入一个共享的 DashMap使 agent_runner 和 Pingora
/// 能够共享 API 密钥配置。
///
/// # 参数
///
/// * `api_key_manager` - 共享的 DashMap<String, ModelProviderConfig>
pub fn with_api_key_manager(
mut self,
api_key_manager: Arc<DashMap<String, ModelProviderConfig>>,
) -> Self {
// 由于 Arc 需要先解包再重新包装,使用 Arc::try_unwrap 或创建新的 service
// 简单起见,我们创建新的 PingoraProxyService
let new_service = (*self.service)
.clone()
.with_api_key_manager(api_key_manager);
self.service = Arc::new(new_service);
self
}
/// 设置 API Key 鉴权配置
///
/// 传入共享的 API Key 配置,使 Pingora 层也能进行 API Key 验证。
/// 使用 ArcSwap 实现无锁读取,提升并发性能。
///
/// # 参数
///
/// * `config` - 共享的 Arc<ArcSwap<ApiKeyAuthConfig>>
pub fn with_api_key_config(
mut self,
config: Arc<ArcSwap<shared_types::ApiKeyAuthConfig>>,
) -> Self {
let new_service = (*self.service).clone().with_api_key_config(config);
self.service = Arc::new(new_service);
self
}
/// 启动 Pingora 服务器
///
/// 接受一个 `shutdown_rx` 用于接收外部关闭信号。
/// 当 `shutdown_rx` 收到信号(或 sender 被 drop`start()` 返回。
/// Pingora 服务器线程运行 `run_forever()`,由进程退出时 OS 清理。
pub async fn start(&mut self, shutdown_rx: oneshot::Receiver<()>) -> Result<()> {
info!("starting Pingora proxy server...");
info!("📡 listening on: 0.0.0.0:{}", self.config.listen_port);
info!("route: /proxy/{{port}}{{/path}}");
// 创建 Pingora 服务器配置
let opt = Opt::default();
// 创建 Pingora 服务器
let mut my_server = Server::new(Some(opt))?;
my_server.bootstrap();
// 创建代理服务实例
let proxy_service = self.service.create_pingora_proxy().map_err(|e| {
error!("[PINGORA] create proxy failed: {}", e);
e
})?;
let proxy_service = Arc::new(proxy_service);
// 创建 HTTP 代理服务
let mut http_proxy = pingora_proxy::http_proxy_service(
&my_server.configuration,
ProxyServiceWrapper {
inner: proxy_service.clone(),
},
);
// 添加 TCP 监听器
http_proxy.add_tcp(&format!("0.0.0.0:{}", self.config.listen_port));
// 将服务添加到服务器
my_server.add_service(http_proxy);
// 在独立线程中运行服务器(使用 std::thread 而不是 spawn_blocking
// spawn_blocking 在某些环境下可能有调度延迟问题
info!("🔧 created Pingora proxy service...");
let server_thread = std::thread::spawn(move || {
info!("🎯 Pingora proxy starting...");
my_server.run_forever();
});
info!("Pingora server already created");
// 等待外部关闭信号sender 被 drop 或显式发送信号都会触发)
let _ = shutdown_rx.await;
info!("📴 shutdown signal received, Pingora proxy cleanup by OS");
// 不再 join 线程 — run_forever() 永不返回join() 会导致永久阻塞
// detach 线程,让进程退出时自动清理
drop(server_thread);
Ok(())
}
/// 获取服务引用
pub fn service(&self) -> Arc<PingoraProxyService> {
self.service.clone()
}
}
/// 包装器结构体,用于实现 Pingora 的 ProxyHttp trait
struct ProxyServiceWrapper {
inner: Arc<PortProxy>,
}
#[async_trait::async_trait]
impl pingora_proxy::ProxyHttp for ProxyServiceWrapper {
type CTX = crate::service::TrackingCtx;
fn new_ctx(&self) -> Self::CTX {
crate::service::TrackingCtx::new()
}
async fn upstream_peer(
&self,
session: &mut pingora_proxy::Session,
_ctx: &mut Self::CTX,
) -> PingoraResult<Box<HttpPeer>> {
// 委托给内部的 PortProxy 实现
self.inner.upstream_peer(session, _ctx).await
}
async fn upstream_request_filter(
&self,
session: &mut pingora_proxy::Session,
upstream_request: &mut pingora_http::RequestHeader,
ctx: &mut Self::CTX,
) -> PingoraResult<()> {
// 委托给内部的 PortProxy 实现
self.inner
.upstream_request_filter(session, upstream_request, ctx)
.await
}
async fn connected_to_upstream(
&self,
session: &mut pingora_proxy::Session,
reused: bool,
peer: &HttpPeer,
#[cfg(unix)] fd: std::os::unix::io::RawFd,
#[cfg(windows)] sock: std::os::windows::io::RawSocket,
digest: Option<&Digest>,
ctx: &mut Self::CTX,
) -> PingoraResult<()> {
// 委托给内部的 PortProxy 实现
self.inner
.connected_to_upstream(
session,
reused,
peer,
#[cfg(unix)]
fd,
#[cfg(windows)]
sock,
digest,
ctx,
)
.await
}
async fn response_filter(
&self,
session: &mut pingora_proxy::Session,
upstream_response: &mut pingora_http::ResponseHeader,
ctx: &mut Self::CTX,
) -> PingoraResult<()> {
// 委托给内部的 PortProxy 实现
self.inner
.response_filter(session, upstream_response, ctx)
.await
}
}
/// 便捷函数:快速启动 Pingora 代理服务器
///
/// 注意:此函数启动后会阻塞直到进程退出,因为内部创建的 shutdown 通道
/// 的 sender 会在函数结束时立即 drop导致 `start()` 立即返回。
/// 如需长时间运行,请使用 `PingoraServerManager::new()` + `start(shutdown_rx)` 组合。
pub async fn start_pingora_proxy(config: ProxyConfig) -> Result<()> {
let (_shutdown_tx, shutdown_rx) = oneshot::channel();
let mut manager = PingoraServerManager::new(config);
manager.start(shutdown_rx).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_manager_creation() {
let config = ProxyConfig::default();
let manager = PingoraServerManager::new(config);
// 测试创建管理器
assert_eq!(manager.config.listen_port, 8080);
assert_eq!(manager.config.default_backend_port, 3000);
}
#[tokio::test]
async fn test_start_stop_server() {
let _manager = PingoraServerManager::new(ProxyConfig::with_listen_port(8081));
// 测试启动和停止(在测试中可能需要更复杂的逻辑)
// 这里只是验证方法调用不 panic
// 在实际测试中需要更完善的设置
}
}

View File

@@ -0,0 +1,597 @@
//! 路由配置模块
//!
//! 集中管理所有 Pingora 代理服务的路由定义,方便查看和维护。
//!
//! ## 支持的路由
//!
//! | 路径模式 | 路由类型 | 说明 |
//! |---------|---------|------|
//! | `/computer/vnc/{user_id}/{project_id}/{*path}` | VNC WebSocket 代理 | 代理到容器的 noVNC 服务 (端口 6080) |
//! | `/proxy/{port}/{*path}` | 端口反向代理 | 动态端口路由到后端服务 |
//!
//! ## 路由语法
//!
//! - `{param}`: 命名参数,匹配单个路径段(例如 `{user_id}` 匹配 "user_123"
//! - `{*path}`: 通配符参数,匹配剩余所有路径(必须在最后)
//!
//! ## 使用示例
//!
//! ```rust,ignore
//! use rcoder_proxy::router::create_router;
//!
//! let router = create_router();
//! let matched = router.at("/computer/vnc/user_123/proj_456/vnc.html").unwrap();
//! ```
use matchit::Router;
/// 路由类型枚举
///
/// 定义 Pingora 代理支持的所有路由类型,用于类型安全的路由分发
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RouteType {
/// VNC WebSocket 代理: `/computer/vnc/{user_id}/{project_id}/{*path}`
///
/// - `user_id`: 用户标识符
/// - `project_id`: 项目标识符
/// - `path`: 剩余路径(如 `vnc.html`, `websockify` 等)
///
/// **目标**: 容器内的 noVNC 服务(端口 6080
///
/// **示例**:
/// - `/computer/vnc/user_123/proj_456/vnc.html`
/// - `/computer/vnc/alice/myproject/websockify`
VncProxy,
/// 端口反向代理: `/proxy/{port}/{*path}`
///
/// - `port`: 目标后端端口号
/// - `path`: 剩余路径
///
/// **目标**: 指定端口的后端服务(默认 127.0.0.1
///
/// **示例**:
/// - `/proxy/8080/api/status`
/// - `/proxy/3000/`
PortProxy,
/// 健康检查: `/health`
///
/// **功能**: 返回 Pingora 代理服务的健康状态
///
/// **响应**: JSON 格式的健康状态信息
///
/// **示例**:
/// - `/health` → `{"status":"ok","service":"pingora-proxy"}`
HealthCheck,
/// 🔒 API 密钥代理: `/api/{service_name}/{*path}`
///
/// **功能**: 拦截 AI API 请求,注入真实 API 密钥后转发到真实 API 端点
///
/// **参数**:
/// - `service_name`: 服务名称(如 `anthropic`, `openai`),用于查找密钥配置
/// - `path`: API 路径(如 `v1/messages`
///
/// **安全特性**:
/// - 移除客户端传入的占位密钥
/// - 从 ApiKeyManager 读取真实密钥并注入请求头
/// - 重写 URI 到真实 API 端点
///
/// **示例**:
/// - `/api/anthropic/v1/messages` → `https://api.anthropic.com/v1/messages` (带真实密钥)
/// - `/api/openai/v1/chat/completions` → `https://api.openai.com/v1/chat/completions`
ApiProxy,
/// 🎵 音频流代理: `/computer/audio/{user_id}/{project_id}/{*path}`
///
/// **功能**: 代理到用户容器的音频流服务
///
/// **参数**:
/// - `user_id`: 用户标识符,用于查找对应的容器 IP
/// - `project_id`: 项目标识符(用于日志和追踪)
/// - `path`: 剩余路径
/// - `ws` 或 `ws/*`: WebSocket 音频流(端口 6089
/// - 其他: HTTP 静态文件(端口 6090
///
/// **目标**: 容器内的音频流服务
/// - HTTP 端口 6090: 静态文件/播放器页面
/// - WebSocket 端口 6089: Opus 音频流
///
/// **限制**: matchit 的 `{*path}` 通配符要求至少一个字符,尾斜杠路径不匹配
///
/// **示例**:
/// - `/computer/audio/user_123/proj_456/index.html` → 容器IP:6090/index.html
/// - `/computer/audio/user_123/proj_456/ws` → 容器IP:6089/ws
/// - ❌ `/computer/audio/user_123/proj_456/` → 404 (尾斜杠不匹配)
AudioProxy,
/// ⌨️ IME 输入法代理: `/computer/ime/{user_id}/{project_id}/{*path}`
///
/// **功能**: 代理到用户容器的 IME 输入法透传服务
///
/// **参数**:
/// - `user_id`: 用户标识符,用于查找对应的容器 IP
/// - `project_id`: 项目标识符(用于日志和追踪)
/// - `path`: 剩余路径(通常为空)
///
/// **目标**: 容器内的 IME 输入法服务WebSocket 端口 6091
///
/// **限制**: matchit 的 `{*path}` 通配符要求至少一个字符
///
/// **示例**:
/// - `/computer/ime/user_123/proj_456/connect` → 容器IP:6091/connect
/// - ❌ `/computer/ime/user_123/proj_456/` → 404 (尾斜杠不匹配)
ImeProxy,
}
/// 创建路由表
///
/// 初始化并配置所有支持的路由规则。
///
/// # 路由优先级
///
/// matchit 使用 radix tree 结构,路由匹配遵循以下优先级:
/// 1. 静态路径段优先于参数
/// 2. 命名参数 `{param}` 优先于通配符 `{*path}`
/// 3. 先注册的路由在同级别时优先
///
/// # 错误处理
///
/// 如果路由配置有冲突,会在插入时 panic通常在开发阶段就能发现
///
/// # 返回
///
/// 返回配置好的 `Router<RouteType>`
///
/// # 示例
///
/// ```rust,ignore
/// use rcoder_proxy::router::{create_router, RouteType};
///
/// let router = create_router();
///
/// // 测试 VNC 路由
/// let matched = router.at("/computer/vnc/user_123/proj_456/vnc.html").unwrap();
/// assert_eq!(*matched.value, RouteType::VncProxy);
/// assert_eq!(matched.params.get("user_id"), Some("user_123"));
///
/// // 测试端口代理路由
/// let matched = router.at("/proxy/8080/api").unwrap();
/// assert_eq!(*matched.value, RouteType::PortProxy);
/// assert_eq!(matched.params.get("port"), Some("8080"));
/// ```
pub fn create_router() -> Result<Router<RouteType>, anyhow::Error> {
let mut router = Router::new();
// ========================================================================
// VNC WebSocket 代理路由
// ========================================================================
//
// 路径格式: /computer/vnc/{user_id}/{project_id}/{*path}
//
// 功能: 将 WebSocket 请求代理到用户容器的 noVNC 服务(端口 6080
//
// 参数:
// - user_id: 用户标识符,用于查找对应的容器 IP
// - project_id: 项目标识符(用于日志和追踪)
// - path: 剩余路径,转发到 noVNC 服务
//
// 示例:
// - /computer/vnc/user_123/proj_456/vnc.html -> 容器IP:6080/vnc.html
// - /computer/vnc/user_123/proj_456/websockify -> 容器IP:6080/websockify (WebSocket)
//
router
.insert(
"/computer/vnc/{user_id}/{project_id}/{*path}",
RouteType::VncProxy,
)
.map_err(|e| {
tracing::error!("[ROUTER] VNC route config failed: {}", e);
anyhow::anyhow!("VNC route configuration error: {}", e)
})?;
// ========================================================================
// 端口反向代理路由
// ========================================================================
//
// 路径格式: /proxy/{port}/{*path}
//
// 功能: 根据端口号动态路由到后端服务
//
// 参数:
// - port: 目标后端端口号1-65535
// - path: 剩余路径,转发到后端服务
//
// 示例:
// - /proxy/8080/api/status -> 127.0.0.1:8080/api/status
// - /proxy/3000/ -> 127.0.0.1:3000/
//
router
.insert("/proxy/{port}/{*path}", RouteType::PortProxy)
.map_err(|e| {
tracing::error!("[ROUTER] port proxy route config failed: {}", e);
anyhow::anyhow!("Port proxy route configuration error: {}", e)
})?;
// ========================================================================
// 健康检查路由
// ========================================================================
//
// 路径格式: /health
//
// 功能: 返回 Pingora 代理服务的健康状态,用于验证服务是否正常运行
//
// 返回: JSON 格式的健康状态
//
// 示例:
// - /health → {"status":"ok","service":"pingora-proxy","timestamp":1234567890}
//
router
.insert("/health", RouteType::HealthCheck)
.map_err(|e| {
tracing::error!("[ROUTER] health check route config failed: {}", e);
anyhow::anyhow!("Health check route configuration error: {}", e)
})?;
// ========================================================================
// 🔒 API 密钥代理路由
// ========================================================================
//
// 路径格式: /api/{service_name}/{*path}
//
// 功能: 拦截 AI API 请求,注入真实 API 密钥后转发
//
// 安全机制:
// 1. 客户端使用占位密钥 (sk-placeholder) 发送请求到本地代理
// 2. Pingora 从 ApiKeyManager 读取真实密钥
// 3. 移除占位密钥,注入真实密钥到请求头
// 4. 重写 URI 到真实 API 端点
//
// 参数:
// - service_name: 服务名称anthropic, openai 等)
// - path: API 路径v1/messages, v1/chat/completions 等)
//
// 示例:
// - /api/anthropic/v1/messages -> https://api.anthropic.com/v1/messages (注入真实 x-api-key)
// - /api/openai/v1/chat/completions -> https://api.openai.com/v1/chat/completions (注入真实 Authorization)
//
router
.insert("/api/{service_name}/{*path}", RouteType::ApiProxy)
.map_err(|e| {
tracing::error!("[ROUTER] API proxy route config failed: {}", e);
anyhow::anyhow!("API proxy route configuration error: {}", e)
})?;
// Fallback: 匹配 /api/{service_name}(无额外路径段)
// 用于 Claude Code 启动时对 base URL 的 HEAD 连通性检查
router
.insert("/api/{service_name}", RouteType::ApiProxy)
.map_err(|e| {
tracing::error!("[ROUTER] API proxy fallback route config failed: {}", e);
anyhow::anyhow!("API proxy fallback route configuration error: {}", e)
})?;
// ========================================================================
// 🎵 音频流代理路由
// ========================================================================
//
// 路径格式: /computer/audio/{user_id}/{project_id}/{*path}
//
// 功能: 将 HTTP 和 WebSocket 请求代理到用户容器的音频流服务
//
// 参数:
// - user_id: 用户标识符,用于查找对应的容器 IP
// - project_id: 项目标识符(用于日志和追踪)
// - path: 剩余路径
// - "ws" 或 "ws/*": WebSocket 音频流(端口 6089
// - 其他(包括空路径): HTTP 静态文件(端口 6090
//
// 示例:
// - /computer/audio/user_123/proj_456/ -> 容器IP:6090/ (播放器页面)
// - /computer/audio/user_123/proj_456/ws -> 容器IP:6089/ws (音频流)
//
router
.insert(
"/computer/audio/{user_id}/{project_id}/{*path}",
RouteType::AudioProxy,
)
.map_err(|e| {
tracing::error!("[ROUTER] audio proxy route config failed: {}", e);
anyhow::anyhow!("Audio proxy route configuration error: {}", e)
})?;
// ========================================================================
// ⌨️ IME 输入法代理路由
// ========================================================================
//
// 路径格式: /computer/ime/{user_id}/{project_id}/{*path}
//
// 功能: 将 WebSocket 请求代理到用户容器的 IME 输入法服务
//
// 参数:
// - user_id: 用户标识符,用于查找对应的容器 IP
// - project_id: 项目标识符(用于日志和追踪)
// - path: 剩余路径(通常为空)
//
// 示例:
// - /computer/ime/user_123/proj_456/ -> 容器IP:6091/ (WebSocket)
//
router
.insert(
"/computer/ime/{user_id}/{project_id}/{*path}",
RouteType::ImeProxy,
)
.map_err(|e| {
tracing::error!("[ROUTER] IME proxy route config failed: {}", e);
anyhow::anyhow!("IME proxy route configuration error: {}", e)
})?;
Ok(router)
}
/// 获取所有路由的文档信息
///
/// 返回人类可读的路由列表,用于调试和文档生成
pub fn get_routes_documentation() -> Vec<(String, String, String)> {
vec![
(
"/computer/vnc/{user_id}/{project_id}/{*path}".to_string(),
"VNC WebSocket proxy".to_string(),
"Proxy to user's container noVNC service (port 6080), supports WebSocket upgrade".to_string(),
),
(
"/proxy/{port}/{*path}".to_string(),
"Port reverse proxy".to_string(),
"Dynamic routing to backend service on specified port".to_string(),
),
(
"/health".to_string(),
"Health check".to_string(),
"Returns Pingora proxy service health status".to_string(),
),
(
"/api/{service_name}/{*path}".to_string(),
"🔒 API key proxy".to_string(),
"Intercept AI API requests, inject real key and forward to actual API endpoint".to_string(),
),
(
"/api/{service_name}".to_string(),
"🔒 API key proxy (fallback)".to_string(),
"Fallback for API proxy health check (HEAD requests without path suffix)".to_string(),
),
(
"/computer/audio/{user_id}/{project_id}/{*path}".to_string(),
"🎵 Audio stream proxy".to_string(),
"Proxy to user's container audio stream service (HTTP 6090 / WebSocket 6089)".to_string(),
),
(
"/computer/ime/{user_id}/{project_id}/{*path}".to_string(),
"⌨️ IME proxy".to_string(),
"Proxy to user's container IME input service (WebSocket 6091)".to_string(),
),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_router() {
let router = create_router().unwrap();
// 测试 VNC 路由
let matched = router
.at("/computer/vnc/user_123/proj_456/vnc.html")
.unwrap();
assert_eq!(*matched.value, RouteType::VncProxy);
assert_eq!(matched.params.get("user_id"), Some("user_123"));
assert_eq!(matched.params.get("project_id"), Some("proj_456"));
assert_eq!(matched.params.get("path"), Some("vnc.html"));
// 测试端口代理路由
let matched = router.at("/proxy/8080/api/status").unwrap();
assert_eq!(*matched.value, RouteType::PortProxy);
assert_eq!(matched.params.get("port"), Some("8080"));
assert_eq!(matched.params.get("path"), Some("api/status"));
}
#[test]
fn test_vnc_route_variations() {
let router = create_router().unwrap();
// WebSocket 路径
let matched = router
.at("/computer/vnc/user_123/proj_456/websockify")
.unwrap();
assert_eq!(*matched.value, RouteType::VncProxy);
assert_eq!(matched.params.get("path"), Some("websockify"));
// 多级子路径
let matched = router
.at("/computer/vnc/user_123/proj_456/api/v1/status")
.unwrap();
assert_eq!(*matched.value, RouteType::VncProxy);
assert_eq!(matched.params.get("path"), Some("api/v1/status"));
}
#[test]
fn test_port_proxy_route_variations() {
let router = create_router().unwrap();
// 不同端口
for port in [3000, 8080, 9000, 5173] {
let path = format!("/proxy/{}/api", port);
let matched = router.at(&path).unwrap();
assert_eq!(*matched.value, RouteType::PortProxy);
assert_eq!(matched.params.get("port"), Some(port.to_string().as_str()));
}
}
#[test]
fn test_route_not_found() {
let router = create_router().unwrap();
// 不匹配的路径应该返回错误
assert!(router.at("/unknown/path").is_err());
assert!(router.at("/computer/desktop").is_err());
// 注意:/api/xxx/yyy 现在会匹配到 ApiProxy 路由
}
#[test]
fn test_api_proxy_route() {
let router = create_router().unwrap();
// 测试 Anthropic API 路由
let matched = router.at("/api/anthropic/v1/messages").unwrap();
assert_eq!(*matched.value, RouteType::ApiProxy);
assert_eq!(matched.params.get("service_name"), Some("anthropic"));
assert_eq!(matched.params.get("path"), Some("v1/messages"));
// 测试 OpenAI API 路由
let matched = router.at("/api/openai/v1/chat/completions").unwrap();
assert_eq!(*matched.value, RouteType::ApiProxy);
assert_eq!(matched.params.get("service_name"), Some("openai"));
assert_eq!(matched.params.get("path"), Some("v1/chat/completions"));
// 测试多级路径
let matched = router.at("/api/custom/v2/org/project/messages").unwrap();
assert_eq!(*matched.value, RouteType::ApiProxy);
assert_eq!(matched.params.get("service_name"), Some("custom"));
assert_eq!(matched.params.get("path"), Some("v2/org/project/messages"));
}
#[test]
fn test_get_routes_documentation() {
let docs = get_routes_documentation();
assert_eq!(docs.len(), 7);
// 验证 VNC 路由文档
assert!(docs[0].0.contains("vnc"));
assert!(docs[0].1.contains("VNC"));
// 验证端口代理路由文档
assert!(docs[1].0.contains("proxy"));
assert!(docs[1].1.contains("Port"));
// 验证健康检查路由文档
assert!(docs[2].0.contains("health"));
assert!(docs[2].1.contains("Health"));
// 验证 API 代理路由文档
assert!(docs[3].0.contains("api"));
assert!(docs[3].1.contains("API"));
// 验证 API 代理 fallback 路由文档
assert!(docs[4].0.contains("api"));
assert!(docs[4].1.contains("API"));
// 验证音频代理路由文档
assert!(docs[5].0.contains("audio"));
assert!(docs[5].1.contains("Audio"));
// 验证 IME 代理路由文档
assert!(docs[6].0.contains("ime"));
assert!(docs[6].1.contains("IME"));
}
#[test]
fn test_route_type_equality() {
assert_eq!(RouteType::VncProxy, RouteType::VncProxy);
assert_eq!(RouteType::PortProxy, RouteType::PortProxy);
assert_eq!(RouteType::ApiProxy, RouteType::ApiProxy);
assert_eq!(RouteType::HealthCheck, RouteType::HealthCheck);
assert_eq!(RouteType::AudioProxy, RouteType::AudioProxy);
assert_eq!(RouteType::ImeProxy, RouteType::ImeProxy);
assert_ne!(RouteType::VncProxy, RouteType::PortProxy);
assert_ne!(RouteType::ApiProxy, RouteType::PortProxy);
assert_ne!(RouteType::AudioProxy, RouteType::ImeProxy);
}
#[test]
fn test_route_type_debug() {
let vnc = RouteType::VncProxy;
let port = RouteType::PortProxy;
let api = RouteType::ApiProxy;
let health = RouteType::HealthCheck;
let audio = RouteType::AudioProxy;
let ime = RouteType::ImeProxy;
let vnc_str = format!("{:?}", vnc);
let port_str = format!("{:?}", port);
let api_str = format!("{:?}", api);
let health_str = format!("{:?}", health);
let audio_str = format!("{:?}", audio);
let ime_str = format!("{:?}", ime);
assert!(vnc_str.contains("VncProxy"));
assert!(port_str.contains("PortProxy"));
assert!(api_str.contains("ApiProxy"));
assert!(health_str.contains("HealthCheck"));
assert!(audio_str.contains("AudioProxy"));
assert!(ime_str.contains("ImeProxy"));
}
#[test]
fn test_audio_route_matching() {
let router = create_router().unwrap();
// 测试音频 WebSocket 路由
let matched = router.at("/computer/audio/user_123/proj_456/ws").unwrap();
assert_eq!(*matched.value, RouteType::AudioProxy);
assert_eq!(matched.params.get("user_id"), Some("user_123"));
assert_eq!(matched.params.get("project_id"), Some("proj_456"));
assert_eq!(matched.params.get("path"), Some("ws"));
// 测试音频 HTTP 路由 (带文件名)
let matched = router
.at("/computer/audio/user_123/proj_456/index.html")
.unwrap();
assert_eq!(*matched.value, RouteType::AudioProxy);
assert_eq!(matched.params.get("path"), Some("index.html"));
// 测试带子路径的 WebSocket
let matched = router
.at("/computer/audio/user_123/proj_456/ws/token")
.unwrap();
assert_eq!(*matched.value, RouteType::AudioProxy);
assert_eq!(matched.params.get("path"), Some("ws/token"));
// 注意:尾斜杠路径 (如 "/computer/audio/user_123/proj_456/") 不匹配 {*path} 通配符
// 这是 matchit 的限制,{*path} 需要至少一个字符
// 实际场景中客户端通常不会发送尾斜杠到这些路径
}
#[test]
fn test_ime_route_matching() {
let router = create_router().unwrap();
// 测试带子路径的 IME 路由
let matched = router.at("/computer/ime/alice/myproject/connect").unwrap();
assert_eq!(*matched.value, RouteType::ImeProxy);
assert_eq!(matched.params.get("user_id"), Some("alice"));
assert_eq!(matched.params.get("project_id"), Some("myproject"));
assert_eq!(matched.params.get("path"), Some("connect"));
// 注意:尾斜杠路径不匹配 {*path} 通配符,需要至少一个字符
}
#[test]
fn test_audio_and_ime_route_not_conflict() {
let router = create_router().unwrap();
// 确保音频和 IME 路由不会互相干扰
let audio_matched = router.at("/computer/audio/user_123/proj_456/ws").unwrap();
assert_eq!(*audio_matched.value, RouteType::AudioProxy);
let ime_matched = router
.at("/computer/ime/user_123/proj_456/connect")
.unwrap();
assert_eq!(*ime_matched.value, RouteType::ImeProxy);
// 确保不同的路径参数被正确解析
assert_eq!(audio_matched.params.get("path"), Some("ws"));
assert_eq!(ime_matched.params.get("path"), Some("connect"));
}
}

View File

@@ -0,0 +1,377 @@
//! 基于 Pingora 的代理服务器模块
//!
//! 提供使用 Cloudflare Pingora 库的高性能代理服务器启动、管理和请求处理功能。
use anyhow::Result;
use std::sync::Arc;
use tracing::{error, info};
use crate::config::ProxyConfig;
use crate::service::PingoraProxyService;
/// 基于 Pingora 的代理服务器管理器
#[derive(Clone)]
pub struct ProxyServer {
config: ProxyConfig,
service: Arc<PingoraProxyService>,
}
impl ProxyServer {
/// 创建新的代理服务器
pub fn new(config: ProxyConfig) -> Self {
let service = Arc::new(PingoraProxyService::new(config.clone()));
Self { config, service }
}
/// 启动代理服务器
pub async fn start(self) -> Result<()> {
// 验证配置
self.config
.validate()
.map_err(|e| anyhow::anyhow!("Configuration validation failed: {}", e))?;
info!(
"starting Pingora-based port proxy server, listening on port: {}",
self.config.listen_port
);
self.log_startup_info();
// 注意:这是一个库,实际的 Pingora 服务器需要由调用者启动
// 这里只是准备服务实例
let pingora_proxy = self.service.create_pingora_proxy().map_err(|e| {
error!("[SERVER] created Pingora proxy failed: {}", e);
e
})?;
info!("Pingora proxy already initialized");
info!(
"Load balancing algorithm: {}",
if pingora_proxy.use_round_robin {
"Round Robin"
} else {
"Ketama Consistent"
}
);
Ok(())
}
/// 记录启动信息
fn log_startup_info(&self) {
info!(" Pingora proxy config:");
info!(" listen port: {}", self.config.listen_port);
info!(
" default backend port: {}",
self.config.default_backend_port
);
info!(" backend host: {}", self.config.backend_host);
info!(" port param: {}", self.config.port_param);
info!("proxy examples:");
info!(" /proxy/3000/path - proxy to port 3000");
info!(" ?port=3000 - proxy params to port 3000 (query)");
info!("Pingora features:");
info!(" - load balancing (Round Robin/Ketama)");
info!(" - health check");
info!(" - connection and connection reuse");
info!(" - HTTP/1.1 and HTTP/2");
info!(" - async I/O");
}
/// 获取服务实例
pub fn service(&self) -> Arc<PingoraProxyService> {
self.service.clone()
}
/// 获取配置的只读引用
pub fn config(&self) -> &ProxyConfig {
&self.config
}
/// 获取监听端口
pub fn listen_port(&self) -> u16 {
self.config.listen_port
}
/// 获取默认后端端口
pub fn default_backend_port(&self) -> u16 {
self.config.default_backend_port
}
/// 检查服务器是否可以启动
pub fn can_start(&self) -> Result<(), String> {
self.config.validate()
}
/// 预启动检查(不实际启动服务器)
pub async fn pre_start_check(&self) -> Result<()> {
// 检查配置
self.config
.validate()
.map_err(|e| anyhow::anyhow!("Configuration validation failed: {}", e))?;
info!("Pingora proxy pre-start check passed");
Ok(())
}
/// 创建带有自定义配置的代理服务器
pub fn with_config(config: ProxyConfig) -> Self {
Self::new(config)
}
/// 使用默认配置创建代理服务器
pub fn default() -> Self {
Self::new(ProxyConfig::default())
}
/// 使用指定监听端口创建代理服务器
pub fn with_listen_port(port: u16) -> Self {
Self::new(ProxyConfig::with_listen_port(port))
}
/// 设置后端主机
pub fn with_backend_host(mut self, host: impl Into<String>) -> Self {
self.config.backend_host = host.into();
self
}
/// 设置端口参数名
pub fn with_port_param(mut self, param: impl Into<String>) -> Self {
self.config.port_param = param.into();
self
}
/// 设置默认后端端口
pub fn with_default_backend_port(mut self, port: u16) -> Self {
self.config.default_backend_port = port;
self
}
/// 设置负载均衡算法
pub fn with_load_balancing(mut self, use_round_robin: bool) -> Self {
// 创建新的服务实例
let service = Arc::new(
self.service
.as_ref()
.clone()
.with_load_balancing(use_round_robin),
);
self.service = service;
self
}
}
/// 代理服务器构建器
pub struct ProxyServerBuilder {
config: ProxyConfig,
use_round_robin: bool,
}
impl ProxyServerBuilder {
/// 创建新的构建器
pub fn new() -> Self {
Self {
config: ProxyConfig::default(),
use_round_robin: true,
}
}
/// 设置监听端口
pub fn listen_port(mut self, port: u16) -> Self {
self.config.listen_port = port;
self
}
/// 设置默认后端端口
pub fn default_backend_port(mut self, port: u16) -> Self {
self.config.default_backend_port = port;
self
}
/// 设置后端主机
pub fn backend_host(mut self, host: impl Into<String>) -> Self {
self.config.backend_host = host.into();
self
}
/// 设置端口参数名
pub fn port_param(mut self, param: impl Into<String>) -> Self {
self.config.port_param = param.into();
self
}
/// 设置配置文件路径
pub fn config_file(mut self, path: impl Into<String>) -> Self {
self.config.config_file = Some(path.into());
self
}
/// 启用详细日志
pub fn verbose(mut self, verbose: bool) -> Self {
self.config.verbose = verbose;
self
}
/// 设置负载均衡算法
pub fn load_balancing(mut self, use_round_robin: bool) -> Self {
self.use_round_robin = use_round_robin;
self
}
/// 构建代理服务器
pub fn build(self) -> ProxyServer {
let mut server = ProxyServer::new(self.config);
server.service = Arc::new(
server
.service
.as_ref()
.clone()
.with_load_balancing(self.use_round_robin),
);
server
}
}
impl Default for ProxyServerBuilder {
fn default() -> Self {
Self::new()
}
}
/// Pingora 代理服务器运行器
///
/// 提供更直接的 Pingora 服务器控制方式
pub struct PingoraServerRunner {
service: PingoraProxyService,
}
impl PingoraServerRunner {
/// 创建新的运行器
pub fn new(config: ProxyConfig) -> Self {
Self {
service: PingoraProxyService::new(config),
}
}
/// 创建带负载均衡的运行器
pub fn with_load_balancing(config: ProxyConfig, use_round_robin: bool) -> Self {
Self {
service: PingoraProxyService::new(config).with_load_balancing(use_round_robin),
}
}
/// 获取服务引用
pub fn service(&self) -> &PingoraProxyService {
&self.service
}
/// 获取 Pingora 代理实例
pub fn create_pingora_proxy(&self) -> anyhow::Result<crate::service::PortProxy> {
self.service.create_pingora_proxy()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ProxyConfig;
#[test]
fn test_server_creation() {
let config = ProxyConfig {
listen_port: 8080,
default_backend_port: 3000,
backend_host: "127.0.0.1".to_string(),
port_param: "port".to_string(),
config_file: None,
verbose: false,
};
let server = ProxyServer::new(config);
assert_eq!(server.listen_port(), 8080);
assert_eq!(server.default_backend_port(), 3000);
}
#[test]
fn test_server_builder() {
let server = ProxyServerBuilder::new()
.listen_port(9090)
.default_backend_port(3001)
.backend_host("localhost")
.port_param("target_port")
.verbose(true)
.load_balancing(false)
.build();
assert_eq!(server.listen_port(), 9090);
assert_eq!(server.default_backend_port(), 3001);
assert_eq!(server.config().backend_host, "localhost");
assert_eq!(server.config().port_param, "target_port");
assert!(server.config().verbose);
}
#[test]
fn test_server_convenience_methods() {
let server = ProxyServer::with_listen_port(8080)
.with_backend_host("example.com")
.with_port_param("service_port")
.with_default_backend_port(80)
.with_load_balancing(false);
assert_eq!(server.listen_port(), 8080);
assert_eq!(server.config().backend_host, "example.com");
assert_eq!(server.config().port_param, "service_port");
assert_eq!(server.config().default_backend_port, 80);
}
#[tokio::test]
async fn test_pre_start_check() {
let server = ProxyServer::default();
// 默认配置应该通过预启动检查
let result = server.pre_start_check().await;
assert!(result.is_ok());
}
#[test]
fn test_config_validation() {
// 测试有效配置
let valid_config = ProxyConfig::default();
let server = ProxyServer::new(valid_config);
assert!(server.can_start().is_ok());
// 测试无效配置端口为0
let mut invalid_config = ProxyConfig::default();
invalid_config.listen_port = 0;
let server = ProxyServer::new(invalid_config);
assert!(server.can_start().is_err());
}
#[test]
fn test_service_access() {
let server = ProxyServer::default();
let service = server.service();
// 测试可以通过服务器访问服务
assert_eq!(service.config().default_backend_port, 3000);
assert_eq!(service.config().listen_port, 8080);
}
#[test]
fn test_pingora_server_runner() {
let config = ProxyConfig::default();
let runner = PingoraServerRunner::new(config);
// 测试创建运行器
assert_eq!(runner.service().config().listen_port, 8080);
assert_eq!(runner.service().config().default_backend_port, 3000);
}
#[test]
fn test_pingora_server_runner_with_lb() {
let config = ProxyConfig::default();
let runner = PingoraServerRunner::with_load_balancing(config, false);
assert!(!runner.service().use_round_robin);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,399 @@
//! 集成测试模块
//!
//! 提供代理功能的集成测试和端到端测试。
use axum::{
body::Body,
http::{Method, Request, StatusCode},
response::Response,
};
use std::collections::HashMap;
use std::time::Duration;
use tokio::time::sleep;
use crate::config::ProxyConfig;
use crate::service::PortProxyService;
use crate::server::ProxyServer;
/// 创建测试配置
fn create_test_config() -> ProxyConfig {
ProxyConfig {
listen_port: 0, // 使用随机可用端口
default_backend_port: 3000,
backend_host: "127.0.0.1".to_string(),
port_param: "port".to_string(),
config_file: None,
verbose: false,
}
}
/// 创建完整的测试服务器配置
fn create_test_server_config(listen_port: u16) -> ProxyConfig {
ProxyConfig {
listen_port,
default_backend_port: 3000,
backend_host: "127.0.0.1".to_string(),
port_param: "port".to_string(),
config_file: None,
verbose: false,
}
}
#[tokio::test]
async fn test_config_validation() {
// 测试有效配置
let valid_config = ProxyConfig::default();
assert!(valid_config.validate().is_ok());
// 测试无效监听端口
let mut invalid_config = ProxyConfig::default();
invalid_config.listen_port = 0;
assert!(invalid_config.validate().is_err());
// 测试无效后端端口
let mut invalid_config = ProxyConfig::default();
invalid_config.default_backend_port = 0;
assert!(invalid_config.validate().is_err());
// 测试空主机地址
let mut invalid_config = ProxyConfig::default();
invalid_config.backend_host = String::new();
assert!(invalid_config.validate().is_err());
// 测试空端口参数名
let mut invalid_config = ProxyConfig::default();
invalid_config.port_param = String::new();
assert!(invalid_config.validate().is_err());
}
#[tokio::test]
async fn test_service_backend_management() {
let config = create_test_config();
let service = PortProxyService::new(config);
// 初始状态应该有默认后端
assert_eq!(service.backend_count().await, 1);
assert!(service.has_backend(3000).await);
assert!(!service.has_backend(3001).await);
// 添加后端
service.add_backend(3001, "localhost".to_string()).await;
assert_eq!(service.backend_count().await, 2);
assert!(service.has_backend(3001).await);
// 获取后端列表
let backends = service.list_backends().await;
assert_eq!(backends.len(), 2);
assert!(backends.contains_key(&3000));
assert!(backends.contains_key(&3001));
// 移除后端
service.remove_backend(3001).await;
assert_eq!(service.backend_count().await, 1);
assert!(!service.has_backend(3001).await);
}
#[tokio::test]
async fn test_port_extraction_scenarios() {
let config = create_test_config();
let service = PortProxyService::new(config);
// 测试场景1: 从查询参数提取端口
let request = Request::builder()
.uri("/api/users?port=8080&format=json")
.body(Body::empty())
.unwrap();
let port = service.extract_target_port(&request).unwrap();
assert_eq!(port, 8080);
// 测试场景2: 从路径提取端口
let request = Request::builder()
.uri("/proxy/9000/api/v1/data")
.body(Body::empty())
.unwrap();
let port = service.extract_target_port(&request).unwrap();
assert_eq!(port, 9000);
// 测试场景3: 多个查询参数
let request = Request::builder()
.uri("/api/search?q=test&port=7000&page=1")
.body(Body::empty())
.unwrap();
let port = service.extract_target_port(&request).unwrap();
assert_eq!(port, 7000);
// 测试场景4: 端口参数格式错误
let request = Request::builder()
.uri("/api/data?port=invalid")
.body(Body::empty())
.unwrap();
let port = service.extract_target_port(&request).unwrap();
assert_eq!(port, 3000); // 应该回退到默认端口
// 测试场景5: 路径格式错误
let request = Request::builder()
.uri("/proxy/not_a_number/api")
.body(Body::empty())
.unwrap();
let port = service.extract_target_port(&request).unwrap();
assert_eq!(port, 3000); // 应该回退到默认端口
// 测试场景6: 没有端口参数
let request = Request::builder()
.uri("/api/data")
.body(Body::empty())
.unwrap();
let port = service.extract_target_port(&request).unwrap();
assert_eq!(port, 3000); // 应该使用默认端口
}
#[tokio::test]
async fn test_uri_building() {
let config = create_test_config();
let service = PortProxyService::new(config);
// 测试普通路径
let uri = service.build_target_uri("localhost", 3000, "/api/data").unwrap();
assert_eq!(uri, "http://localhost:3000/api/data");
// 测试带查询参数的路径
let uri = service.build_target_uri("localhost", 3000, "/api/data?format=json").unwrap();
assert_eq!(uri, "http://localhost:3000/api/data?format=json");
// 测试代理路径前缀
let uri = service.build_target_uri("localhost", 8080, "/proxy/8080/api/data").unwrap();
assert_eq!(uri, "http://localhost:8080/api/data");
// 测试代理路径前缀带查询参数
let uri = service.build_target_uri("localhost", 8080, "/proxy/8080/api/data?format=json").unwrap();
assert_eq!(uri, "http://localhost:8080/api/data?format=json");
// 测试根路径
let uri = service.build_target_uri("localhost", 3000, "/").unwrap();
assert_eq!(uri, "http://localhost:3000/");
// 测试代理根路径
let uri = service.build_target_uri("localhost", 8080, "/proxy/8080/").unwrap();
assert_eq!(uri, "http://localhost:8080/");
}
#[tokio::test]
async fn test_server_builder() {
// 测试默认构建器
let server = ProxyServerBuilder::new().build();
assert_eq!(server.listen_port(), 8080);
assert_eq!(server.default_backend_port(), 3000);
// 测试自定义配置
let server = ProxyServerBuilder::new()
.listen_port(9000)
.default_backend_port(4000)
.backend_host("example.com")
.port_param("target")
.verbose(true)
.build();
assert_eq!(server.listen_port(), 9000);
assert_eq!(server.default_backend_port(), 4000);
assert_eq!(server.config().backend_host, "example.com");
assert_eq!(server.config().port_param, "target");
assert!(server.config().verbose);
}
#[tokio::test]
async fn test_server_convenience_methods() {
// 测试默认服务器
let server = ProxyServer::default();
assert_eq!(server.listen_port(), 8080);
assert_eq!(server.default_backend_port(), 3000);
// 测试自定义端口
let server = ProxyServer::with_listen_port(9090);
assert_eq!(server.listen_port(), 9090);
assert_eq!(server.default_backend_port(), 3000);
// 测试链式配置
let server = ProxyServer::with_listen_port(8080)
.with_backend_host("localhost")
.with_port_param("port")
.with_default_backend_port(3000);
assert_eq!(server.config().backend_host, "localhost");
assert_eq!(server.config().port_param, "port");
assert_eq!(server.config().default_backend_port, 3000);
}
#[tokio::test]
async fn test_server_pre_start_check() {
let server = ProxyServer::default();
// 默认配置应该通过预启动检查
let result = server.pre_start_check().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_proxy_headers() {
let config = create_test_config();
let service = PortProxyService::new(config);
let mut request = Request::builder()
.uri("/api/data")
.body(Body::empty())
.unwrap();
// 添加代理头
service.add_proxy_headers(&mut request).unwrap();
// 验证头信息
assert_eq!(request.headers().get("X-Forwarded-Proto").unwrap(), "http");
assert_eq!(request.headers().get("X-Port-Proxy").unwrap(), "simple-proxy");
}
// 这个测试需要实际的网络连接,只在集成测试中运行
#[ignore]
#[tokio::test]
async fn test_end_to_end_proxy_request() {
// 注意这个测试需要有一个在端口3000运行的实际服务
// 在实际运行时需要先启动一个简单的HTTP服务器
let config = create_test_server_config(8080);
let service = PortProxyService::new(config);
// 创建测试请求
let request = Request::builder()
.method(Method::GET)
.uri("/?port=3000")
.body(Body::empty())
.unwrap();
// 尝试代理请求(可能会失败,因为可能没有实际的服务在运行)
let result = service.proxy_request(request).await;
// 检查是否正确解析了端口(即使连接失败)
// 这主要验证端口提取和URI构建逻辑
match result {
Ok(_) => println!("proxyrequestsucceeded"),
Err(e) => println!("proxyrequestfailed( message ): {}", e),
}
}
// 性能测试
#[tokio::test]
async fn test_concurrent_backend_operations() {
let config = create_test_config();
let service = PortProxyService::new(config);
// 并发添加多个后端
let mut handles = Vec::new();
for i in 1..=10 {
let service_clone = service.clone();
let handle = tokio::spawn(async move {
service_clone.add_backend(3000 + i, format!("host{}", i)).await;
});
handles.push(handle);
}
// 等待所有操作完成
for handle in handles {
handle.await.unwrap();
}
// 验证所有后端都已添加
assert_eq!(service.backend_count().await, 11); // 1个默认 + 10个新增
// 并发读取后端列表
let mut handles = Vec::new();
for _ in 0..5 {
let service_clone = service.clone();
let handle = tokio::spawn(async move {
service_clone.list_backends().await
});
handles.push(handle);
}
// 验证并发读取的一致性
for handle in handles {
let backends = handle.await.unwrap();
assert_eq!(backends.len(), 11);
}
}
// 边界条件测试
#[tokio::test]
async fn test_edge_cases() {
let config = create_test_config();
let service = PortProxyService::new(config);
// 测试添加和移除相同的端口
service.add_backend(3001, "host1".to_string()).await;
service.add_backend(3001, "host2".to_string()).await; // 覆盖
let backend = service.get_target_backend(3001).await.unwrap();
assert_eq!(backend, "host2"); // 应该是最后的值
// 测试移除不存在的端口
service.remove_backend(9999).await; // 不应该崩溃
// 测试获取不存在的后端
let result = service.get_target_backend(9999).await;
assert!(result.is_err());
// 测试空配置
let empty_config = ProxyConfig {
listen_port: 8080,
default_backend_port: 0, // 无效端口
backend_host: "127.0.0.1".to_string(),
port_param: "port".to_string(),
config_file: None,
verbose: false,
};
assert!(empty_config.validate().is_err());
}
#[test]
fn test_config_default_methods() {
// 测试默认配置
let default_config = ProxyConfig::default();
assert_eq!(default_config.listen_port, 8080);
assert_eq!(default_config.default_backend_port, 3000);
assert_eq!(default_config.backend_host, "127.0.0.1");
assert_eq!(default_config.port_param, "port");
assert!(default_config.config_file.is_none());
assert!(!default_config.verbose);
// 测试便捷方法
let config = ProxyConfig::new();
assert_eq!(config.listen_port, 8080);
let config = ProxyConfig::with_listen_port(9090);
assert_eq!(config.listen_port, 9090);
assert_eq!(config.default_backend_port, 3000); // 其他应该保持默认值
let config = ProxyConfig::with_listen_port(8080)
.with_backend_host("example.com")
.with_port_param("service_port");
assert_eq!(config.listen_port, 8080);
assert_eq!(config.backend_host, "example.com");
assert_eq!(config.port_param, "service_port");
}
// 测试模块集成
#[test]
fn test_module_integration() {
// 验证各模块之间的集成是否正常
let config = ProxyConfig::default();
let service = PortProxyService::new(config.clone());
let server = ProxyServer::new(config);
// 验证配置一致性
assert_eq!(service.config().default_backend_port, server.default_backend_port());
assert_eq!(service.config().port_param, server.config().port_param);
// 验证服务访问
let server_service = server.service();
let service_backends = service.backends();
// 两个服务实例应该是独立的
assert_ne!(service.config() as *const _, server_service.config() as *const _);
}

View File

@@ -0,0 +1,134 @@
//! VNC backend resolver module
//!
//! Provides dynamic resolution of VNC backend container IP for transparent proxy.
//! Uses trait abstraction to decouple pingora-proxy and docker_manager.
use async_trait::async_trait;
use std::sync::Arc;
/// VNC backend resolve error
#[derive(Debug, thiserror::Error)]
pub enum VncResolveError {
/// Container not found
#[error("container not found: user_id={0}")]
ContainerNotFound(String),
/// Container not running
#[error("container not running: user_id={0}")]
ContainerNotRunning(String),
/// Unable to get container IP
#[error("unable to get container IP: {0}")]
IpNotAvailable(String),
/// Query failed
#[error("query failed: {0}")]
QueryFailed(String),
}
/// VNC backend info
#[derive(Debug, Clone)]
pub struct VncBackendInfo {
/// Container IP
pub container_ip: String,
/// VNC port (usually 6080)
pub vnc_port: u16,
/// Container is running
pub is_running: bool,
}
impl VncBackendInfo {
/// Create new VNC backend info
pub fn new(container_ip: String, vnc_port: u16, is_running: bool) -> Self {
Self {
container_ip,
vnc_port,
is_running,
}
}
/// Get full backend address (IP:port)
pub fn backend_addr(&self) -> String {
format!("{}:{}", self.container_ip, self.vnc_port)
}
}
/// VNC backend resolver trait
///
/// Used to decouple pingora-proxy and docker_manager,
/// allows different implementation strategies (direct Docker query, cached query, etc.)
#[async_trait]
pub trait VncBackendResolver: Send + Sync {
/// Resolve VNC backend info by user_id
///
/// # Arguments
/// * `user_id` - User ID (container identifier in ComputerAgentRunner mode)
///
/// # Returns
/// * `Ok(VncBackendInfo)` - Successfully resolved VNC backend
/// * `Err(VncResolveError)` - Resolution failed
async fn resolve(&self, user_id: &str) -> Result<VncBackendInfo, VncResolveError>;
/// Check if user has corresponding container (fast check, no detailed info)
///
/// Used to quickly determine if 404 should be returned
async fn exists(&self, user_id: &str) -> bool;
}
/// Arc wrapper for type erasure
pub type DynVncBackendResolver = Arc<dyn VncBackendResolver>;
#[cfg(test)]
mod tests {
use super::*;
/// Mock resolver for testing
struct MockResolver {
users: std::collections::HashMap<String, VncBackendInfo>,
}
#[async_trait]
impl VncBackendResolver for MockResolver {
async fn resolve(&self, user_id: &str) -> Result<VncBackendInfo, VncResolveError> {
self.users
.get(user_id)
.cloned()
.ok_or_else(|| VncResolveError::ContainerNotFound(user_id.to_string()))
}
async fn exists(&self, user_id: &str) -> bool {
self.users.contains_key(user_id)
}
}
#[tokio::test]
async fn test_mock_resolver() {
let mut users = std::collections::HashMap::new();
users.insert(
"user_123".to_string(),
VncBackendInfo::new("172.17.0.5".to_string(), 6080, true),
);
let resolver = MockResolver { users };
// Test successful resolution
let info = resolver.resolve("user_123").await.unwrap();
assert_eq!(info.container_ip, "172.17.0.5");
assert_eq!(info.vnc_port, 6080);
assert!(info.is_running);
// Test resolution failure
let err = resolver.resolve("nonexistent").await.unwrap_err();
assert!(matches!(err, VncResolveError::ContainerNotFound(_)));
// Test existence check
assert!(resolver.exists("user_123").await);
assert!(!resolver.exists("nonexistent").await);
}
#[test]
fn test_backend_addr() {
let info = VncBackendInfo::new("192.168.1.100".to_string(), 6080, true);
assert_eq!(info.backend_addr(), "192.168.1.100:6080");
}
}