mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 03:09:50 +08:00
feat(proxy): 实现代理节点批量升级回滚、隧道重定向跟随及远程配置管理
核心功能: - 新增代理节点批量升级回滚工作流,支持分批升级、健康探针、跳过/重试/取消等操作 - proxy 隧道流处理器支持 HTTP 重定向跟随(最多 10 跳),区分 307/308 可重播与不可重播请求体 - proxy 协议新增 follow_redirects / http1_only 字段,网关侧同步支持 - 新增代理节点远端配置变更接口(名称、允许端口、调度状态、升级目标等) - 新增代理节点注册/反注册/心跳的 Admin API,及节点过期清理维护任务 - gateway 隧道 owner-relay 支持流式代理大请求体,新增 5 MiB 默认限制 - 新增 ProxyNodeRegistrationMutation / ProxyNodeRemoteConfigMutation 数据类型 - proxy 配置新增重定向重播预算、心跳间隔等参数,TUI 安装向导同步更新 - 前端 ProxyNodes 页面新增批量升级操作面板及滚动进度展示
This commit is contained in:
@@ -126,6 +126,7 @@ pub(super) async fn execute_admin_provider_oauth_batch_import(
|
||||
state,
|
||||
template,
|
||||
entry.refresh_token.as_str(),
|
||||
proxy_node_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
+305
-12
@@ -1,6 +1,5 @@
|
||||
use super::parse::{AdminProviderOAuthBatchImportEntry, AdminProviderOAuthBatchImportOutcome};
|
||||
use crate::handlers::admin::provider::oauth::duplicates::find_duplicate_provider_oauth_key;
|
||||
use crate::handlers::admin::provider::oauth::errors::build_internal_control_error_response;
|
||||
use crate::handlers::admin::provider::oauth::provisioning::{
|
||||
create_provider_oauth_catalog_key, provider_oauth_active_api_formats,
|
||||
provider_oauth_key_proxy_value, update_existing_provider_oauth_catalog_key,
|
||||
@@ -8,9 +7,8 @@ use crate::handlers::admin::provider::oauth::provisioning::{
|
||||
use crate::handlers::admin::provider::oauth::runtime::refresh_provider_oauth_account_state_after_update;
|
||||
use crate::handlers::admin::provider::oauth::state::decode_jwt_claims;
|
||||
use crate::handlers::admin::provider::shared::support::ADMIN_PROVIDER_OAUTH_DATA_UNAVAILABLE_DETAIL;
|
||||
use crate::handlers::admin::request::{
|
||||
AdminAppState, AdminKiroAuthConfig, AdminKiroOAuthRefreshAdapter,
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminKiroAuthConfig};
|
||||
use crate::provider_transport::kiro::generate_machine_id;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::provider::oauth::{
|
||||
build_kiro_batch_import_key_name, coerce_admin_provider_oauth_import_str,
|
||||
@@ -20,6 +18,9 @@ use serde_json::{json, Map, Value};
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
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";
|
||||
|
||||
fn admin_provider_oauth_kiro_refresh_base_url_override(
|
||||
state: &AdminAppState<'_>,
|
||||
override_key: &str,
|
||||
@@ -29,6 +30,293 @@ fn admin_provider_oauth_kiro_refresh_base_url_override(
|
||||
(!normalized.is_empty()).then(|| normalized.to_string())
|
||||
}
|
||||
|
||||
fn admin_provider_oauth_kiro_build_refresh_url(
|
||||
auth_config: &AdminKiroAuthConfig,
|
||||
override_base_url: Option<&str>,
|
||||
path: &str,
|
||||
default_host: impl FnOnce(&str) -> String,
|
||||
) -> String {
|
||||
if let Some(base_url) = override_base_url
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return format!("{}/{}", base_url.trim_end_matches('/'), path);
|
||||
}
|
||||
let region = auth_config.effective_auth_region();
|
||||
default_host(region)
|
||||
}
|
||||
|
||||
fn admin_provider_oauth_kiro_effective_host(url: &str, fallback_host: String) -> String {
|
||||
reqwest::Url::parse(url)
|
||||
.ok()
|
||||
.and_then(|value| value.host_str().map(ToOwned::to_owned))
|
||||
.unwrap_or(fallback_host)
|
||||
}
|
||||
|
||||
fn admin_provider_oauth_kiro_ide_tag(kiro_version: &str, machine_id: &str) -> String {
|
||||
if machine_id.trim().is_empty() {
|
||||
format!("KiroIDE-{kiro_version}")
|
||||
} else {
|
||||
format!("KiroIDE-{kiro_version}-{machine_id}")
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_provider_oauth_kiro_refresh_expires_at(payload: &Value) -> u64 {
|
||||
let expires_in = payload
|
||||
.get("expiresIn")
|
||||
.and_then(|value| {
|
||||
value
|
||||
.as_u64()
|
||||
.or_else(|| value.as_str()?.parse::<u64>().ok())
|
||||
})
|
||||
.unwrap_or(3600);
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|value| value.as_secs())
|
||||
.unwrap_or_default()
|
||||
.saturating_add(expires_in)
|
||||
}
|
||||
|
||||
fn admin_provider_oauth_kiro_refresh_response_json(
|
||||
body_text: &str,
|
||||
json_body: Option<Value>,
|
||||
) -> Result<Value, String> {
|
||||
json_body
|
||||
.or_else(|| serde_json::from_str::<Value>(body_text).ok())
|
||||
.ok_or_else(|| "refresh 接口返回了非 JSON 响应".to_string())
|
||||
}
|
||||
|
||||
fn admin_provider_oauth_kiro_refresh_error_detail(
|
||||
status: http::StatusCode,
|
||||
body_text: &str,
|
||||
) -> String {
|
||||
let detail = body_text.trim();
|
||||
if detail.is_empty() {
|
||||
format!("HTTP {}", status.as_u16())
|
||||
} else {
|
||||
detail.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_admin_provider_oauth_kiro_auth_config(
|
||||
state: &AdminAppState<'_>,
|
||||
auth_config: &AdminKiroAuthConfig,
|
||||
proxy_node_id: Option<&str>,
|
||||
social_refresh_base_url: Option<&str>,
|
||||
idc_refresh_base_url: Option<&str>,
|
||||
) -> Result<AdminKiroAuthConfig, String> {
|
||||
if auth_config.is_idc_auth() {
|
||||
let fallback_host = format!("oidc.{}.amazonaws.com", auth_config.effective_auth_region());
|
||||
let url = admin_provider_oauth_kiro_build_refresh_url(
|
||||
auth_config,
|
||||
idc_refresh_base_url,
|
||||
"token",
|
||||
|region| format!("https://oidc.{region}.amazonaws.com/token"),
|
||||
);
|
||||
let host = admin_provider_oauth_kiro_effective_host(&url, fallback_host);
|
||||
let headers = reqwest::header::HeaderMap::from_iter([
|
||||
(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
),
|
||||
(
|
||||
reqwest::header::HOST,
|
||||
reqwest::header::HeaderValue::from_str(&host)
|
||||
.map_err(|_| "IDC host 无效".to_string())?,
|
||||
),
|
||||
(
|
||||
reqwest::header::HeaderName::from_static("x-amz-user-agent"),
|
||||
reqwest::header::HeaderValue::from_static(KIRO_IDC_AMZ_USER_AGENT),
|
||||
),
|
||||
(
|
||||
reqwest::header::USER_AGENT,
|
||||
reqwest::header::HeaderValue::from_static("node"),
|
||||
),
|
||||
(
|
||||
reqwest::header::ACCEPT,
|
||||
reqwest::header::HeaderValue::from_static("*/*"),
|
||||
),
|
||||
]);
|
||||
let response = state
|
||||
.execute_admin_provider_oauth_http_request(
|
||||
"kiro_batch_refresh:idc",
|
||||
reqwest::Method::POST,
|
||||
&url,
|
||||
&headers,
|
||||
Some("application/json"),
|
||||
Some(json!({
|
||||
"clientId": auth_config
|
||||
.client_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or_default(),
|
||||
"clientSecret": auth_config
|
||||
.client_secret
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or_default(),
|
||||
"refreshToken": auth_config
|
||||
.refresh_token
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or_default(),
|
||||
"grantType": "refresh_token",
|
||||
})),
|
||||
None,
|
||||
proxy_node_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("IDC refresh 请求失败: {err}"))?;
|
||||
if !response.status.is_success() {
|
||||
return Err(format!(
|
||||
"IDC refresh 失败: {}",
|
||||
admin_provider_oauth_kiro_refresh_error_detail(
|
||||
response.status,
|
||||
&response.body_text
|
||||
)
|
||||
));
|
||||
}
|
||||
let payload = admin_provider_oauth_kiro_refresh_response_json(
|
||||
&response.body_text,
|
||||
response.json_body,
|
||||
)?;
|
||||
let access_token = payload
|
||||
.get("accessToken")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| "IDC refresh 返回了空 accessToken".to_string())?;
|
||||
|
||||
let mut refreshed = auth_config.clone();
|
||||
refreshed.access_token = Some(access_token.to_string());
|
||||
refreshed.expires_at = Some(admin_provider_oauth_kiro_refresh_expires_at(&payload));
|
||||
if refreshed
|
||||
.machine_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_none_or(|value| value.is_empty())
|
||||
{
|
||||
refreshed.machine_id = generate_machine_id(auth_config, None);
|
||||
}
|
||||
if let Some(refresh_token) = payload
|
||||
.get("refreshToken")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
refreshed.refresh_token = Some(refresh_token.to_string());
|
||||
}
|
||||
return Ok(refreshed);
|
||||
}
|
||||
|
||||
let machine_id = generate_machine_id(auth_config, None)
|
||||
.ok_or_else(|| "缺少 machine_id 种子,无法刷新 social token".to_string())?;
|
||||
let fallback_host = format!(
|
||||
"prod.{}.auth.desktop.kiro.dev",
|
||||
auth_config.effective_auth_region()
|
||||
);
|
||||
let url = admin_provider_oauth_kiro_build_refresh_url(
|
||||
auth_config,
|
||||
social_refresh_base_url,
|
||||
"refreshToken",
|
||||
|region| format!("https://prod.{region}.auth.desktop.kiro.dev/refreshToken"),
|
||||
);
|
||||
let host = admin_provider_oauth_kiro_effective_host(&url, fallback_host);
|
||||
let user_agent =
|
||||
admin_provider_oauth_kiro_ide_tag(auth_config.effective_kiro_version(), &machine_id);
|
||||
let headers = reqwest::header::HeaderMap::from_iter([
|
||||
(
|
||||
reqwest::header::USER_AGENT,
|
||||
reqwest::header::HeaderValue::from_str(&user_agent)
|
||||
.map_err(|_| "Kiro User-Agent 无效".to_string())?,
|
||||
),
|
||||
(
|
||||
reqwest::header::HOST,
|
||||
reqwest::header::HeaderValue::from_str(&host)
|
||||
.map_err(|_| "Kiro host 无效".to_string())?,
|
||||
),
|
||||
(
|
||||
reqwest::header::ACCEPT,
|
||||
reqwest::header::HeaderValue::from_static("application/json, text/plain, */*"),
|
||||
),
|
||||
(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
),
|
||||
(
|
||||
reqwest::header::CONNECTION,
|
||||
reqwest::header::HeaderValue::from_static("close"),
|
||||
),
|
||||
(
|
||||
reqwest::header::ACCEPT_ENCODING,
|
||||
reqwest::header::HeaderValue::from_static("gzip, compress, deflate, br"),
|
||||
),
|
||||
]);
|
||||
let response = state
|
||||
.execute_admin_provider_oauth_http_request(
|
||||
"kiro_batch_refresh:social",
|
||||
reqwest::Method::POST,
|
||||
&url,
|
||||
&headers,
|
||||
Some("application/json"),
|
||||
Some(json!({
|
||||
"refreshToken": auth_config
|
||||
.refresh_token
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or_default(),
|
||||
})),
|
||||
None,
|
||||
proxy_node_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("social refresh 请求失败: {err}"))?;
|
||||
if !response.status.is_success() {
|
||||
return Err(format!(
|
||||
"social refresh 失败: {}",
|
||||
admin_provider_oauth_kiro_refresh_error_detail(response.status, &response.body_text)
|
||||
));
|
||||
}
|
||||
let payload =
|
||||
admin_provider_oauth_kiro_refresh_response_json(&response.body_text, response.json_body)?;
|
||||
let access_token = payload
|
||||
.get("accessToken")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| "social refresh 返回了空 accessToken".to_string())?;
|
||||
|
||||
let mut refreshed = auth_config.clone();
|
||||
refreshed.access_token = Some(access_token.to_string());
|
||||
refreshed.expires_at = Some(admin_provider_oauth_kiro_refresh_expires_at(&payload));
|
||||
if refreshed
|
||||
.machine_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_none_or(|value| value.is_empty())
|
||||
{
|
||||
refreshed.machine_id = Some(machine_id);
|
||||
}
|
||||
if let Some(refresh_token) = payload
|
||||
.get("refreshToken")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
refreshed.refresh_token = Some(refresh_token.to_string());
|
||||
}
|
||||
if let Some(profile_arn) = payload
|
||||
.get("profileArn")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
refreshed.profile_arn = Some(profile_arn.to_string());
|
||||
}
|
||||
Ok(refreshed)
|
||||
}
|
||||
|
||||
pub(super) async fn execute_admin_provider_oauth_kiro_batch_import(
|
||||
state: &AdminAppState<'_>,
|
||||
provider_id: &str,
|
||||
@@ -65,10 +353,10 @@ pub(super) async fn execute_admin_provider_oauth_kiro_batch_import(
|
||||
.list_provider_catalog_endpoints_by_provider_ids(&[provider_id.to_string()])
|
||||
.await?;
|
||||
let key_proxy = provider_oauth_key_proxy_value(proxy_node_id);
|
||||
let adapter = AdminKiroOAuthRefreshAdapter::default().with_refresh_base_urls(
|
||||
admin_provider_oauth_kiro_refresh_base_url_override(state, "kiro_social_refresh"),
|
||||
admin_provider_oauth_kiro_refresh_base_url_override(state, "kiro_idc_refresh"),
|
||||
);
|
||||
let social_refresh_base_url =
|
||||
admin_provider_oauth_kiro_refresh_base_url_override(state, "kiro_social_refresh");
|
||||
let idc_refresh_base_url =
|
||||
admin_provider_oauth_kiro_refresh_base_url_override(state, "kiro_idc_refresh");
|
||||
let mut results = Vec::with_capacity(entries.len());
|
||||
let mut success = 0usize;
|
||||
let mut failed = 0usize;
|
||||
@@ -101,9 +389,14 @@ pub(super) async fn execute_admin_provider_oauth_kiro_batch_import(
|
||||
continue;
|
||||
}
|
||||
|
||||
refreshed_auth_config = match adapter
|
||||
.refresh_auth_config(state.http_client(), &refreshed_auth_config)
|
||||
.await
|
||||
refreshed_auth_config = match refresh_admin_provider_oauth_kiro_auth_config(
|
||||
state,
|
||||
&refreshed_auth_config,
|
||||
proxy_node_id,
|
||||
social_refresh_base_url.as_deref(),
|
||||
idc_refresh_base_url.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
@@ -111,7 +404,7 @@ pub(super) async fn execute_admin_provider_oauth_kiro_batch_import(
|
||||
results.push(json!({
|
||||
"index": index,
|
||||
"status": "error",
|
||||
"error": format!("Token 验证失败: {err:?}"),
|
||||
"error": format!("Token 验证失败: {err}"),
|
||||
"replaced": false,
|
||||
}));
|
||||
continue;
|
||||
|
||||
@@ -130,6 +130,7 @@ pub(super) async fn handle_admin_provider_oauth_complete_key(
|
||||
&callback.code,
|
||||
&callback.state_nonce,
|
||||
state_data.pkce_verifier.as_deref(),
|
||||
payload.proxy_node_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -121,6 +121,7 @@ pub(super) async fn handle_admin_provider_oauth_complete_provider(
|
||||
&callback.code,
|
||||
&callback.state_nonce,
|
||||
state_data.pkce_verifier.as_deref(),
|
||||
payload.proxy_node_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -85,7 +85,11 @@ pub(super) async fn handle_admin_provider_oauth_device_authorize(
|
||||
};
|
||||
|
||||
let client_registration = match state
|
||||
.register_admin_kiro_device_oidc_client(®ion, &start_url)
|
||||
.register_admin_kiro_device_oidc_client(
|
||||
®ion,
|
||||
&start_url,
|
||||
payload.proxy_node_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(payload) => payload,
|
||||
@@ -105,7 +109,13 @@ pub(super) async fn handle_admin_provider_oauth_device_authorize(
|
||||
};
|
||||
|
||||
let device_authorization = match state
|
||||
.start_admin_kiro_device_authorization(®ion, &client_id, &client_secret, &start_url)
|
||||
.start_admin_kiro_device_authorization(
|
||||
®ion,
|
||||
&client_id,
|
||||
&client_secret,
|
||||
&start_url,
|
||||
payload.proxy_node_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(payload) => payload,
|
||||
|
||||
@@ -132,6 +132,7 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
|
||||
&session.client_id,
|
||||
&session.client_secret,
|
||||
&session.device_code,
|
||||
session.proxy_node_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -98,7 +98,11 @@ pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
|
||||
};
|
||||
|
||||
let token_payload = match state
|
||||
.exchange_admin_provider_oauth_refresh_token(template, refresh_token_input)
|
||||
.exchange_admin_provider_oauth_refresh_token(
|
||||
template,
|
||||
refresh_token_input,
|
||||
proxy_node_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(payload) => payload,
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::super::errors::{
|
||||
use super::json_non_empty_string;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminProviderOAuthTemplate};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
use url::form_urlencoded;
|
||||
|
||||
pub(crate) async fn exchange_admin_provider_oauth_code(
|
||||
state: &AdminAppState<'_>,
|
||||
@@ -11,9 +12,9 @@ pub(crate) async fn exchange_admin_provider_oauth_code(
|
||||
code: &str,
|
||||
state_nonce: &str,
|
||||
pkce_verifier: Option<&str>,
|
||||
proxy_node_id: Option<&str>,
|
||||
) -> Result<serde_json::Value, Response<Body>> {
|
||||
let token_url = state.provider_oauth_token_url(template.provider_type, template.token_url);
|
||||
let request = state.http_client().post(token_url);
|
||||
let response = if template.provider_type == "claude_code" {
|
||||
let mut body = serde_json::Map::from_iter([
|
||||
(
|
||||
@@ -43,44 +44,78 @@ pub(crate) async fn exchange_admin_provider_oauth_code(
|
||||
serde_json::Value::String(verifier.to_string()),
|
||||
);
|
||||
}
|
||||
request
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.json(&serde_json::Value::Object(body))
|
||||
.send()
|
||||
let headers = reqwest::header::HeaderMap::from_iter([
|
||||
(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
),
|
||||
(
|
||||
reqwest::header::ACCEPT,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
),
|
||||
]);
|
||||
state
|
||||
.execute_admin_provider_oauth_http_request(
|
||||
"provider-oauth:exchange-code",
|
||||
reqwest::Method::POST,
|
||||
&token_url,
|
||||
&headers,
|
||||
Some("application/json"),
|
||||
Some(serde_json::Value::Object(body)),
|
||||
None,
|
||||
proxy_node_id,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
let mut form = vec![
|
||||
("grant_type", "authorization_code".to_string()),
|
||||
("client_id", template.client_id.to_string()),
|
||||
("redirect_uri", template.redirect_uri.to_string()),
|
||||
("code", code.to_string()),
|
||||
];
|
||||
if !template.client_secret.trim().is_empty() {
|
||||
form.push(("client_secret", template.client_secret.to_string()));
|
||||
}
|
||||
if let Some(verifier) = pkce_verifier {
|
||||
form.push(("code_verifier", verifier.to_string()));
|
||||
}
|
||||
request
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Accept", "application/json")
|
||||
.form(&form)
|
||||
.send()
|
||||
let form_body = {
|
||||
let mut form = form_urlencoded::Serializer::new(String::new());
|
||||
form.append_pair("grant_type", "authorization_code");
|
||||
form.append_pair("client_id", template.client_id);
|
||||
form.append_pair("redirect_uri", template.redirect_uri);
|
||||
form.append_pair("code", code);
|
||||
if !template.client_secret.trim().is_empty() {
|
||||
form.append_pair("client_secret", template.client_secret);
|
||||
}
|
||||
if let Some(verifier) = pkce_verifier {
|
||||
form.append_pair("code_verifier", verifier);
|
||||
}
|
||||
form.finish().into_bytes()
|
||||
};
|
||||
let headers = reqwest::header::HeaderMap::from_iter([
|
||||
(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
reqwest::header::HeaderValue::from_static("application/x-www-form-urlencoded"),
|
||||
),
|
||||
(
|
||||
reqwest::header::ACCEPT,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
),
|
||||
]);
|
||||
state
|
||||
.execute_admin_provider_oauth_http_request(
|
||||
"provider-oauth:exchange-code",
|
||||
reqwest::Method::POST,
|
||||
&token_url,
|
||||
&headers,
|
||||
Some("application/x-www-form-urlencoded"),
|
||||
None,
|
||||
Some(form_body),
|
||||
proxy_node_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
.map_err(|_| {
|
||||
build_internal_control_error_response(http::StatusCode::BAD_REQUEST, "token exchange 失败")
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
if !response.status.is_success() {
|
||||
return Err(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"token exchange 失败",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = response.json::<serde_json::Value>().await.map_err(|_| {
|
||||
let payload = response.json_body.ok_or_else(|| {
|
||||
build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"token exchange 返回缺少 access_token",
|
||||
@@ -99,9 +134,9 @@ pub(crate) async fn exchange_admin_provider_oauth_refresh_token(
|
||||
state: &AdminAppState<'_>,
|
||||
template: AdminProviderOAuthTemplate,
|
||||
refresh_token: &str,
|
||||
proxy_node_id: Option<&str>,
|
||||
) -> Result<serde_json::Value, Response<Body>> {
|
||||
let token_url = state.provider_oauth_token_url(template.provider_type, template.token_url);
|
||||
let request = state.http_client().post(token_url);
|
||||
let scope = template.scopes.join(" ");
|
||||
let response = if template.provider_type == "claude_code" {
|
||||
let mut body = serde_json::Map::from_iter([
|
||||
@@ -121,29 +156,63 @@ pub(crate) async fn exchange_admin_provider_oauth_refresh_token(
|
||||
if !scope.trim().is_empty() {
|
||||
body.insert("scope".to_string(), serde_json::Value::String(scope));
|
||||
}
|
||||
request
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.json(&serde_json::Value::Object(body))
|
||||
.send()
|
||||
let headers = reqwest::header::HeaderMap::from_iter([
|
||||
(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
),
|
||||
(
|
||||
reqwest::header::ACCEPT,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
),
|
||||
]);
|
||||
state
|
||||
.execute_admin_provider_oauth_http_request(
|
||||
"provider-oauth:refresh-token",
|
||||
reqwest::Method::POST,
|
||||
&token_url,
|
||||
&headers,
|
||||
Some("application/json"),
|
||||
Some(serde_json::Value::Object(body)),
|
||||
None,
|
||||
proxy_node_id,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
let mut form = vec![
|
||||
("grant_type", "refresh_token".to_string()),
|
||||
("client_id", template.client_id.to_string()),
|
||||
("refresh_token", refresh_token.to_string()),
|
||||
];
|
||||
if !scope.trim().is_empty() {
|
||||
form.push(("scope", scope));
|
||||
}
|
||||
if !template.client_secret.trim().is_empty() {
|
||||
form.push(("client_secret", template.client_secret.to_string()));
|
||||
}
|
||||
request
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Accept", "application/json")
|
||||
.form(&form)
|
||||
.send()
|
||||
let form_body = {
|
||||
let mut form = form_urlencoded::Serializer::new(String::new());
|
||||
form.append_pair("grant_type", "refresh_token");
|
||||
form.append_pair("client_id", template.client_id);
|
||||
form.append_pair("refresh_token", refresh_token);
|
||||
if !scope.trim().is_empty() {
|
||||
form.append_pair("scope", &scope);
|
||||
}
|
||||
if !template.client_secret.trim().is_empty() {
|
||||
form.append_pair("client_secret", template.client_secret);
|
||||
}
|
||||
form.finish().into_bytes()
|
||||
};
|
||||
let headers = reqwest::header::HeaderMap::from_iter([
|
||||
(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
reqwest::header::HeaderValue::from_static("application/x-www-form-urlencoded"),
|
||||
),
|
||||
(
|
||||
reqwest::header::ACCEPT,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
),
|
||||
]);
|
||||
state
|
||||
.execute_admin_provider_oauth_http_request(
|
||||
"provider-oauth:refresh-token",
|
||||
reqwest::Method::POST,
|
||||
&token_url,
|
||||
&headers,
|
||||
Some("application/x-www-form-urlencoded"),
|
||||
None,
|
||||
Some(form_body),
|
||||
proxy_node_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
.map_err(|_| {
|
||||
@@ -153,13 +222,8 @@ pub(crate) async fn exchange_admin_provider_oauth_refresh_token(
|
||||
)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
let body = response.text().await.map_err(|_| {
|
||||
build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"Refresh Token 验证失败: token exchange 失败",
|
||||
)
|
||||
})?;
|
||||
let status = response.status;
|
||||
let body = response.body_text;
|
||||
if !status.is_success() {
|
||||
let reason =
|
||||
normalize_provider_oauth_refresh_error_message(Some(status.as_u16()), Some(&body));
|
||||
@@ -169,12 +233,15 @@ pub(crate) async fn exchange_admin_provider_oauth_refresh_token(
|
||||
));
|
||||
}
|
||||
|
||||
let payload = serde_json::from_str::<serde_json::Value>(&body).map_err(|_| {
|
||||
build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"token refresh 返回缺少 access_token",
|
||||
)
|
||||
})?;
|
||||
let payload = response
|
||||
.json_body
|
||||
.or_else(|| serde_json::from_str::<serde_json::Value>(&body).ok())
|
||||
.ok_or_else(|| {
|
||||
build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"token refresh 返回缺少 access_token",
|
||||
)
|
||||
})?;
|
||||
if json_non_empty_string(payload.get("access_token")).is_none() {
|
||||
return Err(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
|
||||
+17
-35
@@ -1,5 +1,7 @@
|
||||
use super::super::super::support::AdminProviderOpsCheckinOutcome;
|
||||
use super::super::super::verify::admin_provider_ops_execute_proxy_json_request;
|
||||
use super::super::super::verify::{
|
||||
admin_provider_ops_execute_json_request, AdminProviderOpsExecuteJsonError,
|
||||
};
|
||||
use super::super::support::{admin_provider_ops_json_object_map, admin_provider_ops_request_url};
|
||||
use super::shared::{
|
||||
admin_provider_ops_checkin_already_done, admin_provider_ops_checkin_auth_failure,
|
||||
@@ -27,40 +29,20 @@ pub(in super::super) async fn admin_provider_ops_probe_new_api_checkin(
|
||||
&admin_provider_ops_json_object_map(json!({ "endpoint": endpoint })),
|
||||
endpoint,
|
||||
);
|
||||
let (status, response_json) = if let Some(proxy_snapshot) = proxy_snapshot {
|
||||
match admin_provider_ops_execute_proxy_json_request(
|
||||
state,
|
||||
"provider-ops-action:probe_checkin",
|
||||
reqwest::Method::POST,
|
||||
&url,
|
||||
headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => return None,
|
||||
}
|
||||
} else {
|
||||
let response = match state
|
||||
.http_client()
|
||||
.request(reqwest::Method::POST, url)
|
||||
.headers(headers.clone())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let status = response.status();
|
||||
let response_json = match response.bytes().await {
|
||||
Ok(bytes) => {
|
||||
serde_json::from_slice::<serde_json::Value>(&bytes).unwrap_or_else(|_| json!({}))
|
||||
}
|
||||
Err(_) => json!({}),
|
||||
};
|
||||
(status, response_json)
|
||||
let (status, response_json) = match admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
"provider-ops-action:probe_checkin",
|
||||
reqwest::Method::POST,
|
||||
&url,
|
||||
headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(AdminProviderOpsExecuteJsonError::InvalidJson(_))
|
||||
| Err(AdminProviderOpsExecuteJsonError::Transport(_)) => return None,
|
||||
};
|
||||
|
||||
if status == http::StatusCode::NOT_FOUND {
|
||||
|
||||
+33
-74
@@ -1,5 +1,7 @@
|
||||
use super::super::super::support::ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE;
|
||||
use super::super::super::verify::admin_provider_ops_execute_proxy_json_request;
|
||||
use super::super::super::verify::{
|
||||
admin_provider_ops_execute_json_request, AdminProviderOpsExecuteJsonError,
|
||||
};
|
||||
use super::super::responses::{
|
||||
admin_provider_ops_action_error, admin_provider_ops_action_not_supported,
|
||||
admin_provider_ops_action_response,
|
||||
@@ -32,80 +34,37 @@ pub(in super::super) async fn admin_provider_ops_run_checkin_action(
|
||||
|
||||
let url = admin_provider_ops_request_url(base_url, action_config, "/api/user/checkin");
|
||||
let method = admin_provider_ops_request_method(action_config, "POST");
|
||||
let (status, response_json) = if let Some(proxy_snapshot) = proxy_snapshot {
|
||||
match admin_provider_ops_execute_proxy_json_request(
|
||||
state,
|
||||
&format!(
|
||||
"provider-ops-action:{}:checkin",
|
||||
architecture.architecture_id
|
||||
),
|
||||
method,
|
||||
&url,
|
||||
headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"checkin",
|
||||
admin_provider_ops_network_error_message(&err),
|
||||
None,
|
||||
);
|
||||
}
|
||||
let (status, response_json) = match admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
&format!(
|
||||
"provider-ops-action:{}:checkin",
|
||||
architecture.architecture_id
|
||||
),
|
||||
method,
|
||||
&url,
|
||||
headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(AdminProviderOpsExecuteJsonError::InvalidJson(_)) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"parse_error",
|
||||
"checkin",
|
||||
"响应不是有效的 JSON",
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
);
|
||||
}
|
||||
Err(AdminProviderOpsExecuteJsonError::Transport(err)) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"checkin",
|
||||
admin_provider_ops_network_error_message(&err),
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let response = match state
|
||||
.http_client()
|
||||
.request(method, url)
|
||||
.headers(headers.clone())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(err) if err.is_timeout() => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"checkin",
|
||||
"请求超时",
|
||||
None,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"checkin",
|
||||
format!("网络错误: {err}"),
|
||||
None,
|
||||
);
|
||||
}
|
||||
};
|
||||
let status = response.status();
|
||||
let response_json = match response.bytes().await {
|
||||
Ok(bytes) => match serde_json::from_slice::<serde_json::Value>(&bytes) {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"parse_error",
|
||||
"checkin",
|
||||
"响应不是有效的 JSON",
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"checkin",
|
||||
format!("网络错误: {err}"),
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
);
|
||||
}
|
||||
};
|
||||
(status, response_json)
|
||||
};
|
||||
let response_time_ms = Some(start.elapsed().as_millis() as u64);
|
||||
|
||||
|
||||
+33
-74
@@ -2,7 +2,9 @@ mod sub2api;
|
||||
mod yescode;
|
||||
|
||||
use super::super::support::AdminProviderOpsCheckinOutcome;
|
||||
use super::super::verify::admin_provider_ops_execute_proxy_json_request;
|
||||
use super::super::verify::{
|
||||
admin_provider_ops_execute_json_request, AdminProviderOpsExecuteJsonError,
|
||||
};
|
||||
use super::checkin::admin_provider_ops_probe_new_api_checkin;
|
||||
use super::responses::{admin_provider_ops_action_error, admin_provider_ops_action_response};
|
||||
use super::support::{admin_provider_ops_request_method, admin_provider_ops_request_url};
|
||||
@@ -73,80 +75,37 @@ pub(super) async fn admin_provider_ops_run_query_balance_action(
|
||||
let start = std::time::Instant::now();
|
||||
let url = admin_provider_ops_request_url(base_url, action_config, "/api/user/balance");
|
||||
let method = admin_provider_ops_request_method(action_config, "GET");
|
||||
let (status, response_json) = if let Some(proxy_snapshot) = proxy_snapshot {
|
||||
match admin_provider_ops_execute_proxy_json_request(
|
||||
state,
|
||||
&format!(
|
||||
"provider-ops-action:{}:query_balance:{provider_id}",
|
||||
architecture.architecture_id
|
||||
),
|
||||
method,
|
||||
&url,
|
||||
headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"query_balance",
|
||||
admin_provider_ops_network_error_message(&err),
|
||||
None,
|
||||
);
|
||||
}
|
||||
let (status, response_json) = match admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
&format!(
|
||||
"provider-ops-action:{}:query_balance:{provider_id}",
|
||||
architecture.architecture_id
|
||||
),
|
||||
method,
|
||||
&url,
|
||||
headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(AdminProviderOpsExecuteJsonError::InvalidJson(_)) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"parse_error",
|
||||
"query_balance",
|
||||
"响应不是有效的 JSON",
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
);
|
||||
}
|
||||
Err(AdminProviderOpsExecuteJsonError::Transport(err)) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"query_balance",
|
||||
admin_provider_ops_network_error_message(&err),
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let response = match state
|
||||
.http_client()
|
||||
.request(method, url)
|
||||
.headers(headers.clone())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(err) if err.is_timeout() => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"query_balance",
|
||||
"请求超时",
|
||||
None,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"query_balance",
|
||||
format!("网络错误: {err}"),
|
||||
None,
|
||||
);
|
||||
}
|
||||
};
|
||||
let status = response.status();
|
||||
let response_json = match response.bytes().await {
|
||||
Ok(bytes) => match serde_json::from_slice::<serde_json::Value>(&bytes) {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"parse_error",
|
||||
"query_balance",
|
||||
"响应不是有效的 JSON",
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"query_balance",
|
||||
format!("网络错误: {err}"),
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
);
|
||||
}
|
||||
};
|
||||
(status, response_json)
|
||||
};
|
||||
let response_time_ms = Some(start.elapsed().as_millis() as u64);
|
||||
|
||||
|
||||
+32
-92
@@ -1,7 +1,7 @@
|
||||
use super::super::super::config::persist_admin_provider_ops_runtime_credentials;
|
||||
use super::super::super::verify::{
|
||||
admin_provider_ops_execute_proxy_json_request, admin_provider_ops_sub2api_exchange_token,
|
||||
admin_provider_ops_sub2api_request_url,
|
||||
admin_provider_ops_execute_json_request, admin_provider_ops_sub2api_exchange_token,
|
||||
admin_provider_ops_sub2api_request_url, AdminProviderOpsExecuteJsonError,
|
||||
};
|
||||
use super::super::responses::{
|
||||
admin_provider_ops_action_error, admin_provider_ops_action_response,
|
||||
@@ -93,97 +93,37 @@ pub(super) async fn admin_provider_ops_sub2api_balance_payload(
|
||||
};
|
||||
let auth_headers =
|
||||
reqwest::header::HeaderMap::from_iter([(reqwest::header::AUTHORIZATION, auth_value)]);
|
||||
let (me_result, subscription_result) = if let Some(proxy_snapshot) = proxy_snapshot {
|
||||
let me_request_id = format!("provider-ops-action:sub2api:me:{provider_id}");
|
||||
let subscription_request_id =
|
||||
format!("provider-ops-action:sub2api:subscriptions:{provider_id}");
|
||||
tokio::join!(
|
||||
admin_provider_ops_execute_proxy_json_request(
|
||||
state,
|
||||
&me_request_id,
|
||||
reqwest::Method::GET,
|
||||
&me_url,
|
||||
&auth_headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
),
|
||||
admin_provider_ops_execute_proxy_json_request(
|
||||
state,
|
||||
&subscription_request_id,
|
||||
reqwest::Method::GET,
|
||||
&subscription_url,
|
||||
&auth_headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
let me_request_id = format!("provider-ops-action:sub2api:me:{provider_id}");
|
||||
let subscription_request_id =
|
||||
format!("provider-ops-action:sub2api:subscriptions:{provider_id}");
|
||||
let (me_result, subscription_result) = tokio::join!(
|
||||
admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
&me_request_id,
|
||||
reqwest::Method::GET,
|
||||
&me_url,
|
||||
&auth_headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
),
|
||||
admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
&subscription_request_id,
|
||||
reqwest::Method::GET,
|
||||
&subscription_url,
|
||||
&auth_headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
} else {
|
||||
let http_client = state.http_client();
|
||||
let (me_response, subscription_response) = tokio::join!(
|
||||
http_client.get(me_url).bearer_auth(&access_token).send(),
|
||||
http_client
|
||||
.get(subscription_url)
|
||||
.bearer_auth(&access_token)
|
||||
.send()
|
||||
);
|
||||
let me_result = match me_response {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
let value = match response.bytes().await {
|
||||
Ok(bytes) => {
|
||||
serde_json::from_slice::<Value>(&bytes).unwrap_or_else(|_| json!({}))
|
||||
}
|
||||
Err(err) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"query_balance",
|
||||
format!("网络错误: {err}"),
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
)
|
||||
}
|
||||
};
|
||||
Ok((status, value))
|
||||
}
|
||||
Err(err) if err.is_timeout() => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"query_balance",
|
||||
"请求超时",
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"query_balance",
|
||||
format!("网络错误: {err}"),
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
);
|
||||
}
|
||||
};
|
||||
let subscription_result = match subscription_response {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
let value = match response.bytes().await {
|
||||
Ok(bytes) => {
|
||||
serde_json::from_slice::<Value>(&bytes).unwrap_or_else(|_| json!({}))
|
||||
}
|
||||
Err(err) => {
|
||||
return admin_provider_ops_action_error(
|
||||
"network_error",
|
||||
"query_balance",
|
||||
format!("网络错误: {err}"),
|
||||
Some(start.elapsed().as_millis() as u64),
|
||||
)
|
||||
}
|
||||
};
|
||||
Ok((status, value))
|
||||
}
|
||||
Err(err) if err.is_timeout() => Err("请求超时".to_string()),
|
||||
Err(err) => Err(format!("网络错误: {err}")),
|
||||
};
|
||||
(me_result, subscription_result)
|
||||
};
|
||||
);
|
||||
let me_result = me_result.map_err(|err| match err {
|
||||
AdminProviderOpsExecuteJsonError::InvalidJson(message)
|
||||
| AdminProviderOpsExecuteJsonError::Transport(message) => message,
|
||||
});
|
||||
let subscription_result = subscription_result.map_err(|err| match err {
|
||||
AdminProviderOpsExecuteJsonError::InvalidJson(message)
|
||||
| AdminProviderOpsExecuteJsonError::Transport(message) => message,
|
||||
});
|
||||
let response_time_ms = Some(start.elapsed().as_millis() as u64);
|
||||
|
||||
let (me_status, me_json) = match me_result {
|
||||
|
||||
+30
-59
@@ -1,4 +1,6 @@
|
||||
use super::super::super::verify::admin_provider_ops_execute_proxy_json_request;
|
||||
use super::super::super::verify::{
|
||||
admin_provider_ops_execute_json_request, AdminProviderOpsExecuteJsonError,
|
||||
};
|
||||
use super::super::responses::{
|
||||
admin_provider_ops_action_error, admin_provider_ops_action_response,
|
||||
};
|
||||
@@ -17,65 +19,34 @@ pub(super) async fn admin_provider_ops_yescode_balance_payload(
|
||||
let start = std::time::Instant::now();
|
||||
let balance_url = format!("{}/api/v1/user/balance", base_url.trim_end_matches('/'));
|
||||
let profile_url = format!("{}/api/v1/auth/profile", base_url.trim_end_matches('/'));
|
||||
let (balance_result, profile_result) = if let Some(proxy_snapshot) = proxy_snapshot {
|
||||
tokio::join!(
|
||||
admin_provider_ops_execute_proxy_json_request(
|
||||
state,
|
||||
"provider-ops-action:yescode:balance",
|
||||
reqwest::Method::GET,
|
||||
&balance_url,
|
||||
headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
),
|
||||
admin_provider_ops_execute_proxy_json_request(
|
||||
state,
|
||||
"provider-ops-action:yescode:profile",
|
||||
reqwest::Method::GET,
|
||||
&profile_url,
|
||||
headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
let (balance_result, profile_result) = tokio::join!(
|
||||
admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
"provider-ops-action:yescode:balance",
|
||||
reqwest::Method::GET,
|
||||
&balance_url,
|
||||
headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
),
|
||||
admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
"provider-ops-action:yescode:profile",
|
||||
reqwest::Method::GET,
|
||||
&profile_url,
|
||||
headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
} else {
|
||||
let balance_future = state
|
||||
.http_client()
|
||||
.request(reqwest::Method::GET, balance_url)
|
||||
.headers(headers.clone())
|
||||
.send();
|
||||
let profile_future = state
|
||||
.http_client()
|
||||
.request(reqwest::Method::GET, profile_url)
|
||||
.headers(headers.clone())
|
||||
.send();
|
||||
let (balance_result, profile_result) = tokio::join!(balance_future, profile_future);
|
||||
let balance_result = match balance_result {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
let value = match response.bytes().await {
|
||||
Ok(bytes) => serde_json::from_slice::<serde_json::Value>(&bytes)
|
||||
.unwrap_or_else(|_| json!({})),
|
||||
Err(_) => json!({}),
|
||||
};
|
||||
Ok((status, value))
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
let profile_result = match profile_result {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
let value = match response.bytes().await {
|
||||
Ok(bytes) => serde_json::from_slice::<serde_json::Value>(&bytes)
|
||||
.unwrap_or_else(|_| json!({})),
|
||||
Err(_) => json!({}),
|
||||
};
|
||||
Ok((status, value))
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
(balance_result, profile_result)
|
||||
};
|
||||
);
|
||||
let balance_result = balance_result.map_err(|err| match err {
|
||||
AdminProviderOpsExecuteJsonError::InvalidJson(message)
|
||||
| AdminProviderOpsExecuteJsonError::Transport(message) => message,
|
||||
});
|
||||
let profile_result = profile_result.map_err(|err| match err {
|
||||
AdminProviderOpsExecuteJsonError::InvalidJson(message)
|
||||
| AdminProviderOpsExecuteJsonError::Transport(message) => message,
|
||||
});
|
||||
let response_time_ms = Some(start.elapsed().as_millis() as u64);
|
||||
|
||||
let mut combined = serde_json::Map::new();
|
||||
|
||||
@@ -12,7 +12,10 @@ use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogPr
|
||||
pub(super) use proxy::{
|
||||
admin_provider_ops_anyrouter_acw_cookie, admin_provider_ops_resolve_proxy_snapshot,
|
||||
};
|
||||
pub(super) use request::admin_provider_ops_execute_proxy_json_request;
|
||||
pub(super) use request::{
|
||||
admin_provider_ops_execute_json_request, admin_provider_ops_execute_proxy_json_request,
|
||||
AdminProviderOpsExecuteJsonError,
|
||||
};
|
||||
pub(super) use sub2api::{
|
||||
admin_provider_ops_sub2api_exchange_token, admin_provider_ops_sub2api_request_url,
|
||||
};
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
use super::request::{
|
||||
admin_provider_ops_execute_get_text, admin_provider_ops_execute_get_text_no_redirect,
|
||||
};
|
||||
use super::request::admin_provider_ops_execute_get_text_no_redirect;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use aether_admin::provider::ops::admin_provider_ops_anyrouter_compute_acw_sc_v2;
|
||||
use aether_contracts::ProxySnapshot;
|
||||
use aether_data::repository::proxy_nodes::StoredProxyNode;
|
||||
use aether_provider_transport::TransportTunnelAffinityLookup;
|
||||
use regex::Regex;
|
||||
use serde_json::{json, Map, Value};
|
||||
use url::Url;
|
||||
|
||||
const TUNNEL_BASE_URL_EXTRA_KEY: &str = "tunnel_base_url";
|
||||
const TUNNEL_OWNER_INSTANCE_ID_EXTRA_KEY: &str = "tunnel_owner_instance_id";
|
||||
const TUNNEL_OWNER_OBSERVED_AT_EXTRA_KEY: &str = "tunnel_owner_observed_at_unix_secs";
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
pub(in super::super) struct AdminProviderOpsAnyrouterChallenge {
|
||||
pub(in super::super) acw_cookie: String,
|
||||
@@ -30,25 +21,15 @@ pub(in super::super) async fn admin_provider_ops_anyrouter_acw_cookie(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
),
|
||||
)]);
|
||||
let response = if admin_provider_ops_proxy_uses_tunnel(proxy_snapshot.as_ref()) {
|
||||
admin_provider_ops_execute_get_text(
|
||||
state,
|
||||
"provider-ops-acw:anyrouter",
|
||||
base_url.trim_end_matches('/'),
|
||||
&headers,
|
||||
proxy_snapshot.as_ref(),
|
||||
)
|
||||
.await
|
||||
.ok()?
|
||||
} else {
|
||||
admin_provider_ops_execute_get_text_no_redirect(
|
||||
base_url.trim_end_matches('/'),
|
||||
&headers,
|
||||
proxy_snapshot.as_ref(),
|
||||
)
|
||||
.await
|
||||
.ok()?
|
||||
};
|
||||
let response = admin_provider_ops_execute_get_text_no_redirect(
|
||||
state,
|
||||
"provider-ops-acw:anyrouter",
|
||||
base_url.trim_end_matches('/'),
|
||||
&headers,
|
||||
proxy_snapshot.as_ref(),
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
let compiled = Regex::new(r"var\s+arg1\s*=\s*'([0-9a-fA-F]{40})'").ok()?;
|
||||
let captures = compiled.captures(&response.body)?;
|
||||
let arg1 = captures.get(1)?.as_str();
|
||||
@@ -63,222 +44,7 @@ pub(in super::super) async fn admin_provider_ops_resolve_proxy_snapshot(
|
||||
state: &AdminAppState<'_>,
|
||||
connector_config: Option<&Map<String, Value>>,
|
||||
) -> Option<ProxySnapshot> {
|
||||
let explicit_node_id = connector_config
|
||||
.and_then(|config| admin_provider_ops_string_field(config, "proxy_node_id"));
|
||||
if let Some(snapshot) =
|
||||
admin_provider_ops_resolve_proxy_node_snapshot(state, explicit_node_id.as_deref()).await
|
||||
{
|
||||
return Some(snapshot);
|
||||
}
|
||||
|
||||
if explicit_node_id.is_none() {
|
||||
let system_node_id = state
|
||||
.read_system_config_json_value("system_proxy_node_id")
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| value.as_str().map(str::trim).map(ToOwned::to_owned))
|
||||
.filter(|value| !value.is_empty());
|
||||
if let Some(snapshot) =
|
||||
admin_provider_ops_resolve_proxy_node_snapshot(state, system_node_id.as_deref()).await
|
||||
{
|
||||
return Some(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
connector_config
|
||||
.and_then(|config| config.get("proxy"))
|
||||
.and_then(admin_provider_ops_legacy_proxy_snapshot)
|
||||
}
|
||||
|
||||
async fn admin_provider_ops_resolve_proxy_node_snapshot(
|
||||
state: &AdminAppState<'_>,
|
||||
node_id: Option<&str>,
|
||||
) -> Option<ProxySnapshot> {
|
||||
let node_id = node_id.map(str::trim).filter(|value| !value.is_empty())?;
|
||||
let node = state.find_proxy_node(node_id).await.ok().flatten()?;
|
||||
if node.status.trim() != "online" {
|
||||
return None;
|
||||
}
|
||||
if node.tunnel_mode && node.tunnel_connected {
|
||||
let mut extra = Map::new();
|
||||
if let Ok(Some(owner)) = state.app().lookup_tunnel_attachment_owner(node_id).await {
|
||||
extra.insert(
|
||||
TUNNEL_BASE_URL_EXTRA_KEY.to_string(),
|
||||
Value::String(owner.relay_base_url),
|
||||
);
|
||||
extra.insert(
|
||||
TUNNEL_OWNER_INSTANCE_ID_EXTRA_KEY.to_string(),
|
||||
Value::String(owner.gateway_instance_id),
|
||||
);
|
||||
extra.insert(
|
||||
TUNNEL_OWNER_OBSERVED_AT_EXTRA_KEY.to_string(),
|
||||
json!(owner.observed_at_unix_secs),
|
||||
);
|
||||
}
|
||||
return Some(ProxySnapshot {
|
||||
enabled: Some(true),
|
||||
mode: Some("tunnel".to_string()),
|
||||
node_id: Some(node_id.to_string()),
|
||||
label: Some(node.name),
|
||||
url: None,
|
||||
extra: if extra.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Value::Object(extra))
|
||||
},
|
||||
});
|
||||
}
|
||||
if !node.is_manual {
|
||||
return None;
|
||||
}
|
||||
let proxy_url = node
|
||||
.proxy_url
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
Some(ProxySnapshot {
|
||||
enabled: Some(true),
|
||||
mode: admin_provider_ops_proxy_mode(Some(proxy_url)),
|
||||
node_id: Some(node.id.clone()),
|
||||
label: Some(node.name.clone()),
|
||||
url: admin_provider_ops_proxy_url_with_node_auth(&node),
|
||||
extra: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_provider_ops_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_ops_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_ops_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_ops_inject_proxy_auth(proxy_url, username, password)
|
||||
.or_else(|| Some(proxy_url.to_string())),
|
||||
extra: None,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_provider_ops_proxy_url_with_node_auth(node: &StoredProxyNode) -> Option<String> {
|
||||
let proxy_url = node
|
||||
.proxy_url
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
let username = node
|
||||
.proxy_username
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
let password = node
|
||||
.proxy_password
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
admin_provider_ops_inject_proxy_auth(proxy_url, username, password)
|
||||
.or_else(|| Some(proxy_url.to_string()))
|
||||
}
|
||||
|
||||
fn admin_provider_ops_inject_proxy_auth(
|
||||
proxy_url: &str,
|
||||
username: Option<&str>,
|
||||
password: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let username = username.filter(|value| !value.is_empty())?;
|
||||
let mut parsed = Url::parse(proxy_url).ok()?;
|
||||
parsed.set_username(username).ok()?;
|
||||
parsed.set_password(password).ok()?;
|
||||
Some(parsed.to_string())
|
||||
}
|
||||
|
||||
fn admin_provider_ops_proxy_mode(proxy_url: Option<&str>) -> Option<String> {
|
||||
proxy_url
|
||||
.and_then(|value| {
|
||||
Url::parse(value)
|
||||
.ok()
|
||||
.map(|parsed| parsed.scheme().to_string())
|
||||
})
|
||||
.or_else(|| {
|
||||
proxy_url.and_then(|value| {
|
||||
value
|
||||
.split_once("://")
|
||||
.map(|(scheme, _)| scheme.trim().to_ascii_lowercase())
|
||||
.filter(|scheme| !scheme.is_empty())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_provider_ops_proxy_uses_tunnel(proxy_snapshot: Option<&ProxySnapshot>) -> bool {
|
||||
proxy_snapshot.is_some_and(|proxy| {
|
||||
proxy.mode.as_deref().map(str::trim) == Some("tunnel")
|
||||
|| (proxy
|
||||
.url
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or_default()
|
||||
.is_empty()
|
||||
&& proxy
|
||||
.node_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty()))
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_provider_ops_string_field(config: &Map<String, Value>, key: &str) -> Option<String> {
|
||||
config
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
state
|
||||
.resolve_admin_connector_proxy_snapshot(connector_config)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use aether_contracts::{
|
||||
ExecutionPlan, ExecutionResult, ExecutionTimeouts, ProxySnapshot, RequestBody,
|
||||
EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, EXECUTION_REQUEST_HTTP1_ONLY_HEADER,
|
||||
};
|
||||
use aether_http::{apply_http_client_config, HttpClientConfig};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use flate2::read::{DeflateDecoder, GzDecoder};
|
||||
use reqwest::redirect::Policy;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::Read;
|
||||
@@ -17,6 +16,11 @@ pub(super) struct AdminProviderOpsTextResponse {
|
||||
pub(super) body: String,
|
||||
}
|
||||
|
||||
pub(in super::super) enum AdminProviderOpsExecuteJsonError {
|
||||
InvalidJson(String),
|
||||
Transport(String),
|
||||
}
|
||||
|
||||
pub(super) async fn admin_provider_ops_execute_get_json(
|
||||
state: &AdminAppState<'_>,
|
||||
request_id: &str,
|
||||
@@ -24,37 +28,7 @@ pub(super) async fn admin_provider_ops_execute_get_json(
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
proxy_snapshot: Option<&ProxySnapshot>,
|
||||
) -> Result<(http::StatusCode, Value), String> {
|
||||
if proxy_snapshot.is_none() {
|
||||
let response = match state
|
||||
.http_client()
|
||||
.get(url)
|
||||
.headers(headers.clone())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(err) if err.is_timeout() => return Err("timeout".to_string()),
|
||||
Err(err) => return Err(err.to_string()),
|
||||
};
|
||||
let status = response.status();
|
||||
let content_encoding = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_ENCODING)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(ToOwned::to_owned);
|
||||
let bytes = response.bytes().await.map_err(|err| err.to_string())?;
|
||||
let decoded_bytes =
|
||||
admin_provider_ops_decode_response_bytes(bytes.as_ref(), content_encoding.as_deref())
|
||||
.unwrap_or_else(|| bytes.to_vec());
|
||||
let response_json = match serde_json::from_slice::<Value>(&decoded_bytes) {
|
||||
Ok(value) => value,
|
||||
Err(err) if status != http::StatusCode::OK => json!({}),
|
||||
Err(err) => return Err(format!("upstream response is not valid JSON: {err}")),
|
||||
};
|
||||
return Ok((status, response_json));
|
||||
}
|
||||
|
||||
let result = admin_provider_ops_execute_request(
|
||||
match admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
request_id,
|
||||
reqwest::Method::GET,
|
||||
@@ -63,11 +37,35 @@ pub(super) async fn admin_provider_ops_execute_get_json(
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await?;
|
||||
Ok((
|
||||
admin_provider_ops_execution_status_code(&result),
|
||||
admin_provider_ops_execution_json_body(&result),
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(result) => Ok(result),
|
||||
Err(AdminProviderOpsExecuteJsonError::InvalidJson(message))
|
||||
| Err(AdminProviderOpsExecuteJsonError::Transport(message)) => Err(message),
|
||||
}
|
||||
}
|
||||
|
||||
pub(in super::super) async fn admin_provider_ops_execute_json_request(
|
||||
state: &AdminAppState<'_>,
|
||||
request_id: &str,
|
||||
method: reqwest::Method,
|
||||
url: &str,
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
json_body: Option<Value>,
|
||||
proxy_snapshot: Option<&ProxySnapshot>,
|
||||
) -> Result<(http::StatusCode, Value), AdminProviderOpsExecuteJsonError> {
|
||||
let result = admin_provider_ops_execute_request(
|
||||
state,
|
||||
request_id,
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
json_body,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
.map_err(AdminProviderOpsExecuteJsonError::Transport)?;
|
||||
admin_provider_ops_execution_json_response(&result)
|
||||
}
|
||||
|
||||
pub(in super::super) async fn admin_provider_ops_execute_proxy_json_request(
|
||||
@@ -79,7 +77,7 @@ pub(in super::super) async fn admin_provider_ops_execute_proxy_json_request(
|
||||
json_body: Option<Value>,
|
||||
proxy_snapshot: &ProxySnapshot,
|
||||
) -> Result<(http::StatusCode, Value), String> {
|
||||
let result = admin_provider_ops_execute_request(
|
||||
match admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
request_id,
|
||||
method,
|
||||
@@ -88,11 +86,12 @@ pub(in super::super) async fn admin_provider_ops_execute_proxy_json_request(
|
||||
json_body,
|
||||
Some(proxy_snapshot),
|
||||
)
|
||||
.await?;
|
||||
Ok((
|
||||
admin_provider_ops_execution_status_code(&result),
|
||||
admin_provider_ops_execution_json_body(&result),
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(result) => Ok(result),
|
||||
Err(AdminProviderOpsExecuteJsonError::InvalidJson(message))
|
||||
| Err(AdminProviderOpsExecuteJsonError::Transport(message)) => Err(message),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn admin_provider_ops_execute_get_text(
|
||||
@@ -129,50 +128,20 @@ pub(super) async fn admin_provider_ops_execute_get_text(
|
||||
}
|
||||
|
||||
pub(super) async fn admin_provider_ops_execute_get_text_no_redirect(
|
||||
state: &AdminAppState<'_>,
|
||||
request_id: &str,
|
||||
url: &str,
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
proxy_snapshot: Option<&ProxySnapshot>,
|
||||
) -> Result<AdminProviderOpsTextResponse, String> {
|
||||
let mut builder = apply_http_client_config(
|
||||
reqwest::Client::builder().redirect(Policy::none()),
|
||||
&HttpClientConfig {
|
||||
connect_timeout_ms: Some(10_000),
|
||||
request_timeout_ms: Some(ADMIN_PROVIDER_OPS_VERIFY_TIMEOUT_MS),
|
||||
use_rustls_tls: true,
|
||||
http2_adaptive_window: true,
|
||||
..HttpClientConfig::default()
|
||||
},
|
||||
);
|
||||
if let Some(proxy_url) = proxy_snapshot
|
||||
.and_then(|proxy| proxy.url.as_deref())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
let proxy = reqwest::Proxy::all(proxy_url).map_err(|err| format!("连接失败: {err}"))?;
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
let client = builder.build().map_err(|err| format!("验证失败: {err}"))?;
|
||||
let response = match client.get(url).headers(headers.clone()).send().await {
|
||||
Ok(response) => response,
|
||||
Err(err) if err.is_timeout() => return Err("连接超时".to_string()),
|
||||
Err(err) if err.is_connect() => return Err(format!("连接失败: {err}")),
|
||||
Err(err) => return Err(format!("验证失败: {err}")),
|
||||
};
|
||||
let content_encoding = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_ENCODING)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(ToOwned::to_owned);
|
||||
let body = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|err| format!("验证失败: {err}"))
|
||||
.map(|bytes| {
|
||||
admin_provider_ops_decode_response_bytes(bytes.as_ref(), content_encoding.as_deref())
|
||||
.unwrap_or_else(|| bytes.to_vec())
|
||||
})
|
||||
.map(|bytes| String::from_utf8_lossy(&bytes).to_string())?;
|
||||
Ok(AdminProviderOpsTextResponse { body })
|
||||
admin_provider_ops_execute_get_text(
|
||||
state,
|
||||
request_id,
|
||||
url,
|
||||
&admin_provider_ops_headers_with_transport_controls(headers, Some(false), false),
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn admin_provider_ops_execute_request(
|
||||
@@ -240,23 +209,55 @@ fn admin_provider_ops_execution_headers(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(in super::super) fn admin_provider_ops_headers_with_transport_controls(
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
follow_redirects: Option<bool>,
|
||||
http1_only: bool,
|
||||
) -> reqwest::header::HeaderMap {
|
||||
let mut headers = headers.clone();
|
||||
if let Some(follow_redirects) = follow_redirects {
|
||||
let value = if follow_redirects { "true" } else { "false" };
|
||||
headers.insert(
|
||||
reqwest::header::HeaderName::from_static(EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER),
|
||||
reqwest::header::HeaderValue::from_static(value),
|
||||
);
|
||||
}
|
||||
if http1_only {
|
||||
headers.insert(
|
||||
reqwest::header::HeaderName::from_static(EXECUTION_REQUEST_HTTP1_ONLY_HEADER),
|
||||
reqwest::header::HeaderValue::from_static("true"),
|
||||
);
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
fn admin_provider_ops_execution_status_code(result: &ExecutionResult) -> http::StatusCode {
|
||||
http::StatusCode::from_u16(result.status_code).unwrap_or(http::StatusCode::BAD_GATEWAY)
|
||||
}
|
||||
|
||||
fn admin_provider_ops_execution_json_body(result: &ExecutionResult) -> Value {
|
||||
result
|
||||
fn admin_provider_ops_execution_json_response(
|
||||
result: &ExecutionResult,
|
||||
) -> Result<(http::StatusCode, Value), AdminProviderOpsExecuteJsonError> {
|
||||
let status = admin_provider_ops_execution_status_code(result);
|
||||
if let Some(json_body) = result.body.as_ref().and_then(|body| body.json_body.clone()) {
|
||||
return Ok((status, json_body));
|
||||
}
|
||||
|
||||
let Some(bytes) = result
|
||||
.body
|
||||
.as_ref()
|
||||
.and_then(|body| body.json_body.clone())
|
||||
.or_else(|| {
|
||||
result
|
||||
.body
|
||||
.as_ref()
|
||||
.and_then(|body| admin_provider_ops_execution_body_bytes(&result.headers, body))
|
||||
.and_then(|bytes| serde_json::from_slice::<Value>(&bytes).ok())
|
||||
})
|
||||
.unwrap_or_else(|| json!({}))
|
||||
.and_then(|body| admin_provider_ops_execution_body_bytes(&result.headers, body))
|
||||
else {
|
||||
return Ok((status, json!({})));
|
||||
};
|
||||
|
||||
match serde_json::from_slice::<Value>(&bytes) {
|
||||
Ok(value) => Ok((status, value)),
|
||||
Err(_) if status != http::StatusCode::OK => Ok((status, json!({}))),
|
||||
Err(err) => Err(AdminProviderOpsExecuteJsonError::InvalidJson(format!(
|
||||
"upstream response is not valid JSON: {err}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_provider_ops_execution_body_bytes(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::request::{
|
||||
admin_provider_ops_execute_proxy_json_request,
|
||||
admin_provider_ops_verify_execution_error_message,
|
||||
admin_provider_ops_execute_json_request, admin_provider_ops_headers_with_transport_controls,
|
||||
admin_provider_ops_verify_execution_error_message, AdminProviderOpsExecuteJsonError,
|
||||
};
|
||||
use crate::handlers::admin::provider::ops::providers::config::persist_admin_provider_ops_runtime_credentials;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
@@ -10,7 +10,6 @@ use aether_admin::provider::ops::{
|
||||
};
|
||||
use aether_contracts::ProxySnapshot;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider;
|
||||
use aether_http::{apply_http_client_config, HttpClientConfig};
|
||||
use serde_json::{json, Map, Value};
|
||||
use tracing::warn;
|
||||
|
||||
@@ -64,51 +63,26 @@ pub(super) async fn admin_provider_ops_local_sub2api_verify_response(
|
||||
reqwest::header::HeaderValue::from_static("*/*"),
|
||||
),
|
||||
]);
|
||||
let (status, response_json) = if let Some(proxy_snapshot) = proxy_snapshot {
|
||||
match admin_provider_ops_execute_proxy_json_request(
|
||||
state,
|
||||
"provider-ops-verify:sub2api",
|
||||
reqwest::Method::GET,
|
||||
&verify_url,
|
||||
&auth_headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
return admin_provider_ops_verify_failure(
|
||||
admin_provider_ops_verify_execution_error_message(&error),
|
||||
);
|
||||
}
|
||||
let auth_headers =
|
||||
admin_provider_ops_headers_with_transport_controls(&auth_headers, None, true);
|
||||
let (status, response_json) = match admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
"provider-ops-verify:sub2api",
|
||||
reqwest::Method::GET,
|
||||
&verify_url,
|
||||
&auth_headers,
|
||||
None,
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(AdminProviderOpsExecuteJsonError::InvalidJson(message))
|
||||
| Err(AdminProviderOpsExecuteJsonError::Transport(message)) => {
|
||||
return admin_provider_ops_verify_failure(
|
||||
admin_provider_ops_verify_execution_error_message(&message),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let http_client = match admin_provider_ops_sub2api_http_client() {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
return admin_provider_ops_verify_failure(format!("验证失败: {err}"));
|
||||
}
|
||||
};
|
||||
let response = match http_client
|
||||
.get(&verify_url)
|
||||
.headers(auth_headers)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(err) if err.is_timeout() => return admin_provider_ops_verify_failure("连接超时"),
|
||||
Err(err) if err.is_connect() => {
|
||||
return admin_provider_ops_verify_failure(format!("连接失败: {err}"));
|
||||
}
|
||||
Err(err) => return admin_provider_ops_verify_failure(format!("验证失败: {err}")),
|
||||
};
|
||||
let status = response.status();
|
||||
let response_json = match response.bytes().await {
|
||||
Ok(bytes) => serde_json::from_slice::<Value>(&bytes).unwrap_or_else(|_| json!({})),
|
||||
Err(_) => json!({}),
|
||||
};
|
||||
(status, response_json)
|
||||
};
|
||||
|
||||
parse_verify_payload(
|
||||
@@ -119,20 +93,6 @@ pub(super) async fn admin_provider_ops_local_sub2api_verify_response(
|
||||
)
|
||||
}
|
||||
|
||||
fn admin_provider_ops_sub2api_http_client() -> Result<reqwest::Client, reqwest::Error> {
|
||||
let builder = apply_http_client_config(
|
||||
reqwest::Client::builder().http1_only(),
|
||||
&HttpClientConfig {
|
||||
connect_timeout_ms: Some(10_000),
|
||||
request_timeout_ms: Some(30_000),
|
||||
use_rustls_tls: true,
|
||||
user_agent: Some(ADMIN_PROVIDER_OPS_USER_AGENT.to_string()),
|
||||
..HttpClientConfig::default()
|
||||
},
|
||||
);
|
||||
builder.build()
|
||||
}
|
||||
|
||||
// 对齐 Python httpx.AsyncClient(base_url=...) 的行为:
|
||||
// 以 "/" 开头的端点始终相对站点根路径解析,而不是简单字符串拼接。
|
||||
pub(in super::super) fn admin_provider_ops_sub2api_request_url(
|
||||
@@ -263,39 +223,24 @@ async fn admin_provider_ops_sub2api_token_request(
|
||||
reqwest::header::HeaderValue::from_static("*/*"),
|
||||
),
|
||||
]);
|
||||
let (status, response_json) = if let Some(proxy_snapshot) = proxy_snapshot {
|
||||
admin_provider_ops_execute_proxy_json_request(
|
||||
state,
|
||||
&format!("provider-ops-sub2api:{path}"),
|
||||
reqwest::Method::POST,
|
||||
&url,
|
||||
&default_headers,
|
||||
Some(body),
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| admin_provider_ops_verify_execution_error_message(&error))?
|
||||
} else {
|
||||
let client =
|
||||
admin_provider_ops_sub2api_http_client().map_err(|err| format!("验证失败: {err}"))?;
|
||||
let response = match client
|
||||
.post(url)
|
||||
.headers(default_headers)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(err) if err.is_timeout() => return Err("连接超时".to_string()),
|
||||
Err(err) if err.is_connect() => return Err(format!("连接失败: {err}")),
|
||||
Err(err) => return Err(format!("验证失败: {err}")),
|
||||
};
|
||||
let status = response.status();
|
||||
let response_json = match response.bytes().await {
|
||||
Ok(bytes) => serde_json::from_slice::<Value>(&bytes).unwrap_or_else(|_| json!({})),
|
||||
Err(_) => json!({}),
|
||||
};
|
||||
(status, response_json)
|
||||
let default_headers =
|
||||
admin_provider_ops_headers_with_transport_controls(&default_headers, None, true);
|
||||
let (status, response_json) = match admin_provider_ops_execute_json_request(
|
||||
state,
|
||||
&format!("provider-ops-sub2api:{path}"),
|
||||
reqwest::Method::POST,
|
||||
&url,
|
||||
&default_headers,
|
||||
Some(body),
|
||||
proxy_snapshot,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(AdminProviderOpsExecuteJsonError::InvalidJson(message))
|
||||
| Err(AdminProviderOpsExecuteJsonError::Transport(message)) => {
|
||||
return Err(admin_provider_ops_verify_execution_error_message(&message));
|
||||
}
|
||||
};
|
||||
let payload = response_json.as_object().cloned().unwrap_or_default();
|
||||
if status != http::StatusCode::OK
|
||||
|
||||
Reference in New Issue
Block a user