mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 18:59:50 +08:00
feat(proxy): 重构 Proxy 节点管理与隧道系统
- 重构 proxy_nodes 管理端,支持节点注册、心跳、隧道生命周期管理 - 增强 tunnel 嵌入式 hub 和隧道协议 - 重构 aether-proxy 配置、隧道客户端、心跳和调度机制 - 调整 admin OAuth/配额/导入等处理器的参数传递 - 扩展数据迁移模块 - 补充 proxy nodes、OAuth、配额、系统导入等测试 - 更新前端 proxy nodes 视图和 API
This commit is contained in:
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user