feat(proxy): 重构 Proxy 节点管理与隧道系统

- 重构 proxy_nodes 管理端,支持节点注册、心跳、隧道生命周期管理
- 增强 tunnel 嵌入式 hub 和隧道协议
- 重构 aether-proxy 配置、隧道客户端、心跳和调度机制
- 调整 admin OAuth/配额/导入等处理器的参数传递
- 扩展数据迁移模块
- 补充 proxy nodes、OAuth、配额、系统导入等测试
- 更新前端 proxy nodes 视图和 API
This commit is contained in:
fawney19
2026-04-14 22:51:02 +08:00
parent fb31928e44
commit a4e7ac1df6
55 changed files with 3477 additions and 892 deletions
@@ -1,7 +1,7 @@
use super::*;
use crate::handlers::admin::provider::oauth::errors::build_internal_control_error_response;
use aether_contracts::{
ExecutionPlan, ExecutionResult, ExecutionTimeouts, RequestBody,
ExecutionPlan, ExecutionResult, ExecutionTimeouts, ProxySnapshot, RequestBody,
EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER,
};
use aether_data::repository::provider_oauth::{
@@ -21,6 +21,7 @@ use url::Url;
const KIRO_IDC_AMZ_USER_AGENT: &str =
"aws-sdk-js/3.738.0 ua/2.1 os/other lang/js md/browser#unknown_unknown api/sso-oidc#3.738.0 m/E KiroIDE";
const ADMIN_PROVIDER_OAUTH_TIMEOUT_MS: u64 = 30_000;
const ADMIN_PROVIDER_OAUTH_PROXY_TIMEOUT_MS: u64 = 60_000;
pub(crate) struct AdminProviderOAuthHttpResponse {
pub(crate) status: http::StatusCode,
@@ -132,7 +133,7 @@ impl<'a> AdminAppState<'a> {
code: &str,
state_nonce: &str,
pkce_verifier: Option<&str>,
proxy_node_id: Option<&str>,
proxy: Option<ProxySnapshot>,
) -> Result<serde_json::Value, Response<Body>> {
crate::handlers::admin::provider::oauth::state::exchange_admin_provider_oauth_code(
self,
@@ -140,7 +141,7 @@ impl<'a> AdminAppState<'a> {
code,
state_nonce,
pkce_verifier,
proxy_node_id,
proxy,
)
.await
}
@@ -149,13 +150,13 @@ impl<'a> AdminAppState<'a> {
&self,
template: AdminProviderOAuthTemplate,
refresh_token: &str,
proxy_node_id: Option<&str>,
proxy: Option<ProxySnapshot>,
) -> Result<serde_json::Value, Response<Body>> {
crate::handlers::admin::provider::oauth::state::exchange_admin_provider_oauth_refresh_token(
self,
template,
refresh_token,
proxy_node_id,
proxy,
)
.await
}
@@ -315,7 +316,7 @@ impl<'a> AdminAppState<'a> {
&self,
region: &str,
start_url: &str,
proxy_node_id: Option<&str>,
proxy: Option<ProxySnapshot>,
) -> Result<serde_json::Value, Response<Body>> {
let payload = post_kiro_device_oidc_json(
self,
@@ -337,7 +338,7 @@ impl<'a> AdminAppState<'a> {
],
"issuerUrl": start_url,
}),
proxy_node_id,
proxy,
)
.await?;
if payload
@@ -364,7 +365,7 @@ impl<'a> AdminAppState<'a> {
client_id: &str,
client_secret: &str,
start_url: &str,
proxy_node_id: Option<&str>,
proxy: Option<ProxySnapshot>,
) -> Result<serde_json::Value, Response<Body>> {
let payload = post_kiro_device_oidc_json(
self,
@@ -375,7 +376,7 @@ impl<'a> AdminAppState<'a> {
"clientSecret": client_secret,
"startUrl": start_url,
}),
proxy_node_id,
proxy,
)
.await?;
if payload
@@ -402,7 +403,7 @@ impl<'a> AdminAppState<'a> {
client_id: &str,
client_secret: &str,
device_code: &str,
proxy_node_id: Option<&str>,
proxy: Option<ProxySnapshot>,
) -> Result<serde_json::Value, Response<Body>> {
post_kiro_device_oidc_json(
self,
@@ -414,11 +415,35 @@ impl<'a> AdminAppState<'a> {
"grantType": "urn:ietf:params:oauth:grant-type:device_code",
"deviceCode": device_code,
}),
proxy_node_id,
proxy,
)
.await
}
pub(crate) async fn resolve_admin_provider_oauth_operation_proxy_snapshot(
&self,
temporary_proxy_node_id: Option<&str>,
configured_proxies: &[Option<&serde_json::Value>],
) -> Option<ProxySnapshot> {
if let Some(snapshot) = self
.resolve_admin_proxy_node_snapshot(temporary_proxy_node_id)
.await
{
return Some(snapshot);
}
for proxy in configured_proxies {
if let Some(snapshot) = self
.app
.resolve_configured_proxy_snapshot_with_tunnel_affinity(*proxy)
.await
{
return Some(snapshot);
}
}
self.app.resolve_system_proxy_snapshot().await
}
pub(crate) async fn find_duplicate_provider_oauth_key(
&self,
provider_id: &str,
@@ -504,7 +529,7 @@ async fn post_kiro_device_oidc_json(
endpoint_key: &str,
default_url: String,
body: serde_json::Value,
proxy_node_id: Option<&str>,
proxy: Option<ProxySnapshot>,
) -> Result<serde_json::Value, Response<Body>> {
let url = state.provider_oauth_token_url(endpoint_key, &default_url);
let host = Url::parse(&url)
@@ -539,7 +564,7 @@ async fn post_kiro_device_oidc_json(
Some("application/json"),
Some(body),
None,
proxy_node_id,
proxy,
)
.await
.map_err(|_| {
@@ -570,7 +595,7 @@ impl<'a> AdminAppState<'a> {
content_type: Option<&str>,
json_body: Option<serde_json::Value>,
body_bytes: Option<Vec<u8>>,
proxy_node_id: Option<&str>,
proxy: Option<ProxySnapshot>,
) -> Result<AdminProviderOAuthHttpResponse, String> {
let body = if let Some(json_body) = json_body {
RequestBody::from_json(json_body)
@@ -581,6 +606,7 @@ impl<'a> AdminAppState<'a> {
body_ref: None,
}
};
let timeout_ms = admin_provider_oauth_timeout_ms(proxy.as_ref());
let plan = ExecutionPlan {
request_id: request_id.to_string(),
candidate_id: None,
@@ -601,21 +627,14 @@ impl<'a> AdminAppState<'a> {
client_api_format: "provider_oauth:exchange".to_string(),
provider_api_format: "provider_oauth:exchange".to_string(),
model_name: Some("oauth-exchange".to_string()),
proxy: if proxy_node_id
.map(str::trim)
.is_some_and(|value| !value.is_empty())
{
self.resolve_admin_proxy_node_snapshot(proxy_node_id).await
} else {
self.app.resolve_system_proxy_snapshot().await
},
proxy,
tls_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(ADMIN_PROVIDER_OAUTH_TIMEOUT_MS),
read_ms: Some(ADMIN_PROVIDER_OAUTH_TIMEOUT_MS),
write_ms: Some(ADMIN_PROVIDER_OAUTH_TIMEOUT_MS),
pool_ms: Some(ADMIN_PROVIDER_OAUTH_TIMEOUT_MS),
total_ms: Some(ADMIN_PROVIDER_OAUTH_TIMEOUT_MS),
connect_ms: Some(timeout_ms),
read_ms: Some(timeout_ms),
write_ms: Some(timeout_ms),
pool_ms: Some(timeout_ms),
total_ms: Some(timeout_ms),
..ExecutionTimeouts::default()
}),
};
@@ -632,6 +651,14 @@ impl<'a> AdminAppState<'a> {
}
}
fn admin_provider_oauth_timeout_ms(proxy: Option<&ProxySnapshot>) -> u64 {
if proxy.is_some() {
ADMIN_PROVIDER_OAUTH_PROXY_TIMEOUT_MS
} else {
ADMIN_PROVIDER_OAUTH_TIMEOUT_MS
}
}
fn maybe_insert_host_header(
mut headers: reqwest::header::HeaderMap,
host: &str,
@@ -82,6 +82,15 @@ impl<'a> AdminAppState<'a> {
.await
}
pub(crate) async fn resolve_transport_proxy_source_with_tunnel_affinity(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Option<&'static str> {
self.app
.resolve_transport_proxy_source_with_tunnel_affinity(transport)
.await
}
pub(crate) fn fixed_provider_template(
&self,
provider_type: &str,
@@ -115,15 +124,14 @@ impl<'a> AdminAppState<'a> {
return Some(snapshot);
}
if explicit_node_id.is_none() {
if let Some(snapshot) = self.app.resolve_system_proxy_snapshot().await {
return Some(snapshot);
}
let proxy = connector_config
.and_then(|config| config.get("proxy"))
.and_then(admin_provider_transport_proxy_snapshot);
if proxy.is_some() {
return proxy;
}
connector_config
.and_then(|config| config.get("proxy"))
.and_then(admin_provider_transport_legacy_proxy_snapshot)
self.app.resolve_system_proxy_snapshot().await
}
pub(crate) async fn resolve_admin_proxy_node_snapshot(
@@ -256,65 +264,46 @@ impl<'a> AdminAppState<'a> {
}
}
fn admin_provider_transport_legacy_proxy_snapshot(value: &Value) -> Option<ProxySnapshot> {
match value {
Value::String(proxy_url) => {
let proxy_url = proxy_url.trim();
if proxy_url.is_empty() {
return None;
}
Some(ProxySnapshot {
enabled: Some(true),
mode: admin_provider_transport_proxy_mode(Some(proxy_url)),
node_id: None,
label: None,
url: Some(proxy_url.to_string()),
extra: None,
})
}
Value::Object(object) => {
if object.get("enabled").and_then(Value::as_bool) == Some(false) {
return None;
}
let proxy_url = object
.get("url")
.or_else(|| object.get("proxy_url"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?;
let username = object
.get("username")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let password = object
.get("password")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
Some(ProxySnapshot {
enabled: Some(true),
mode: object
.get("mode")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.or_else(|| admin_provider_transport_proxy_mode(Some(proxy_url))),
node_id: None,
label: object
.get("label")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
url: admin_provider_transport_inject_proxy_auth(proxy_url, username, password)
.or_else(|| Some(proxy_url.to_string())),
extra: None,
})
}
_ => None,
fn admin_provider_transport_proxy_snapshot(value: &Value) -> Option<ProxySnapshot> {
let object = value.as_object()?;
if object.get("enabled").and_then(Value::as_bool) == Some(false) {
return None;
}
let proxy_url = object
.get("url")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?;
let username = object
.get("username")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let password = object
.get("password")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
Some(ProxySnapshot {
enabled: Some(true),
mode: object
.get("mode")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.or_else(|| admin_provider_transport_proxy_mode(Some(proxy_url))),
node_id: None,
label: object
.get("label")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
url: admin_provider_transport_inject_proxy_auth(proxy_url, username, password)
.or_else(|| Some(proxy_url.to_string())),
extra: None,
})
}
fn admin_provider_transport_inject_proxy_auth(
@@ -354,3 +343,50 @@ fn admin_provider_transport_string_field(config: &Map<String, Value>, key: &str)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
#[cfg(test)]
mod tests {
use aether_contracts::ProxySnapshot;
use serde_json::json;
use super::admin_provider_transport_proxy_snapshot;
#[test]
fn connector_proxy_snapshot_requires_object_value() {
assert_eq!(
admin_provider_transport_proxy_snapshot(&json!("http://proxy.example:8080")),
None
);
}
#[test]
fn connector_proxy_snapshot_requires_url_field() {
assert_eq!(
admin_provider_transport_proxy_snapshot(&json!({
"proxy_url": "http://proxy.example:8080"
})),
None
);
}
#[test]
fn connector_proxy_snapshot_keeps_current_object_shape() {
assert_eq!(
admin_provider_transport_proxy_snapshot(&json!({
"url": "http://proxy.example:8080",
"username": "alice",
"password": "secret",
"mode": "http",
"label": "manual"
})),
Some(ProxySnapshot {
enabled: Some(true),
mode: Some("http".to_string()),
node_id: None,
label: Some("manual".to_string()),
url: Some("http://alice:[email protected]:8080/".to_string()),
extra: None,
})
);
}
}