mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Add public proxy install session delivery
This commit is contained in:
@@ -25,6 +25,7 @@ pub(crate) fn mount_public_support_routes(router: Router<AppState>) -> Router<Ap
|
|||||||
.route("/api/capabilities/user-configurable", get(proxy_request))
|
.route("/api/capabilities/user-configurable", get(proxy_request))
|
||||||
.route("/api/capabilities/model/{*model_path}", get(proxy_request))
|
.route("/api/capabilities/model/{*model_path}", get(proxy_request))
|
||||||
.route("/install/{*install_path}", get(proxy_request))
|
.route("/install/{*install_path}", get(proxy_request))
|
||||||
|
.route("/install-proxy/{*install_path}", get(proxy_request))
|
||||||
.route("/i/{*install_path}", get(proxy_request))
|
.route("/i/{*install_path}", get(proxy_request))
|
||||||
.route("/", get(proxy_request))
|
.route("/", get(proxy_request))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -675,6 +675,7 @@ pub(super) fn classify_public_support_route(
|
|||||||
))
|
))
|
||||||
} else if method == http::Method::GET
|
} else if method == http::Method::GET
|
||||||
&& (has_single_segment_after_prefix(normalized_path, "/install/")
|
&& (has_single_segment_after_prefix(normalized_path, "/install/")
|
||||||
|
|| has_single_segment_after_prefix(normalized_path, "/install-proxy/")
|
||||||
|| has_single_segment_after_prefix(normalized_path, "/i/"))
|
|| has_single_segment_after_prefix(normalized_path, "/i/"))
|
||||||
{
|
{
|
||||||
Some(classified(
|
Some(classified(
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ pub(crate) use self::system_modules_helpers::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) use self::support::{
|
pub(crate) use self::support::{
|
||||||
build_api_key_install_session_response, build_unhandled_public_support_response,
|
build_api_key_install_session_response, build_proxy_node_install_session_response,
|
||||||
matches_model_mapping_for_models, maybe_build_local_admin_announcements_response,
|
build_unhandled_public_support_response, matches_model_mapping_for_models,
|
||||||
maybe_build_local_public_support_response, CreateApiKeyInstallSessionRequest,
|
maybe_build_local_admin_announcements_response, maybe_build_local_public_support_response,
|
||||||
|
CreateApiKeyInstallSessionRequest,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ use self::support_auth::{
|
|||||||
};
|
};
|
||||||
use self::support_dashboard::maybe_build_local_dashboard_response;
|
use self::support_dashboard::maybe_build_local_dashboard_response;
|
||||||
pub(crate) use self::support_install::{
|
pub(crate) use self::support_install::{
|
||||||
build_api_key_install_session_response, CreateApiKeyInstallSessionRequest,
|
build_api_key_install_session_response, build_proxy_node_install_session_response,
|
||||||
|
CreateApiKeyInstallSessionRequest,
|
||||||
};
|
};
|
||||||
use self::support_install::{
|
use self::support_install::{
|
||||||
handle_users_me_api_key_install_session_create, maybe_build_local_install_response,
|
handle_users_me_api_key_install_session_create, maybe_build_local_install_response,
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ use super::{
|
|||||||
|
|
||||||
const INSTALL_SESSION_TTL_SECS: u64 = 15 * 60;
|
const INSTALL_SESSION_TTL_SECS: u64 = 15 * 60;
|
||||||
const INSTALL_SESSION_KEY_PREFIX: &str = "install:session:";
|
const INSTALL_SESSION_KEY_PREFIX: &str = "install:session:";
|
||||||
|
const PROXY_INSTALL_SESSION_KEY_PREFIX: &str = "proxy-install:session:";
|
||||||
|
const PROXY_INSTALL_UNIX_SCRIPT_URL: &str = "https://raw.githubusercontent.com/fawney19/Aether/aether-rust-pioneer/apps/aether-proxy/install.sh";
|
||||||
|
const PROXY_INSTALL_POWERSHELL_SCRIPT_URL: &str = "https://raw.githubusercontent.com/fawney19/Aether/aether-rust-pioneer/apps/aether-proxy/install.ps1";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@@ -49,6 +52,14 @@ struct StoredInstallSession {
|
|||||||
expires_at_unix_secs: u64,
|
expires_at_unix_secs: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct StoredProxyInstallSession {
|
||||||
|
aether_url: String,
|
||||||
|
management_token: String,
|
||||||
|
node_name: String,
|
||||||
|
expires_at_unix_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn users_me_api_key_install_sessions_path_matches(request_path: &str) -> bool {
|
pub(super) fn users_me_api_key_install_sessions_path_matches(request_path: &str) -> bool {
|
||||||
users_me_api_key_install_session_id_from_path(request_path).is_some()
|
users_me_api_key_install_session_id_from_path(request_path).is_some()
|
||||||
}
|
}
|
||||||
@@ -78,10 +89,27 @@ fn install_code_from_path(request_path: &str) -> Option<(String, bool)> {
|
|||||||
(!code.is_empty()).then(|| (code.to_string(), is_powershell))
|
(!code.is_empty()).then(|| (code.to_string(), is_powershell))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn proxy_install_code_from_path(request_path: &str) -> Option<(String, bool)> {
|
||||||
|
let raw = request_path
|
||||||
|
.strip_prefix("/install-proxy/")?
|
||||||
|
.trim()
|
||||||
|
.trim_matches('/');
|
||||||
|
if raw.is_empty() || raw.contains('/') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let is_powershell = raw.ends_with(".ps1");
|
||||||
|
let code = raw.strip_suffix(".ps1").unwrap_or(raw).trim();
|
||||||
|
(!code.is_empty()).then(|| (code.to_string(), is_powershell))
|
||||||
|
}
|
||||||
|
|
||||||
fn install_session_runtime_key(code: &str) -> String {
|
fn install_session_runtime_key(code: &str) -> String {
|
||||||
format!("{INSTALL_SESSION_KEY_PREFIX}{code}")
|
format!("{INSTALL_SESSION_KEY_PREFIX}{code}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn proxy_install_session_runtime_key(code: &str) -> String {
|
||||||
|
format!("{PROXY_INSTALL_SESSION_KEY_PREFIX}{code}")
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_install_code() -> String {
|
fn generate_install_code() -> String {
|
||||||
uuid::Uuid::new_v4()
|
uuid::Uuid::new_v4()
|
||||||
.simple()
|
.simple()
|
||||||
@@ -95,7 +123,7 @@ fn unix_secs_now() -> u64 {
|
|||||||
chrono::Utc::now().timestamp().max(0) as u64
|
chrono::Utc::now().timestamp().max(0) as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base_url_from_request(
|
pub(crate) fn base_url_from_request(
|
||||||
headers: &http::HeaderMap,
|
headers: &http::HeaderMap,
|
||||||
request_context: &GatewayPublicRequestContext,
|
request_context: &GatewayPublicRequestContext,
|
||||||
) -> String {
|
) -> String {
|
||||||
@@ -134,6 +162,45 @@ fn powershell_single_quote(value: &str) -> String {
|
|||||||
format!("'{}'", value.replace('\'', "''"))
|
format!("'{}'", value.replace('\'', "''"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_proxy_unix_script(session: &StoredProxyInstallSession) -> String {
|
||||||
|
format!(
|
||||||
|
r###"#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
export AETHER_PROXY_AETHER_URL={aether_url}
|
||||||
|
export AETHER_PROXY_MANAGEMENT_TOKEN={management_token}
|
||||||
|
export AETHER_PROXY_NODE_NAME={node_name}
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL {script_url} | sh
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -qO- {script_url} | sh
|
||||||
|
else
|
||||||
|
printf '%s\n' "[Aether Proxy] 需要 curl 或 wget 下载安装脚本" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"###,
|
||||||
|
aether_url = shell_single_quote(&session.aether_url),
|
||||||
|
management_token = shell_single_quote(&session.management_token),
|
||||||
|
node_name = shell_single_quote(&session.node_name),
|
||||||
|
script_url = shell_single_quote(PROXY_INSTALL_UNIX_SCRIPT_URL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_proxy_powershell_script(session: &StoredProxyInstallSession) -> String {
|
||||||
|
format!(
|
||||||
|
r###"$ErrorActionPreference = 'Stop'
|
||||||
|
$env:AETHER_PROXY_AETHER_URL = {aether_url}
|
||||||
|
$env:AETHER_PROXY_MANAGEMENT_TOKEN = {management_token}
|
||||||
|
$env:AETHER_PROXY_NODE_NAME = {node_name}
|
||||||
|
irm {script_url} | iex
|
||||||
|
"###,
|
||||||
|
aether_url = powershell_single_quote(&session.aether_url),
|
||||||
|
management_token = powershell_single_quote(&session.management_token),
|
||||||
|
node_name = powershell_single_quote(&session.node_name),
|
||||||
|
script_url = powershell_single_quote(PROXY_INSTALL_POWERSHELL_SCRIPT_URL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn cli_label(target_cli: InstallTargetCli) -> &'static str {
|
fn cli_label(target_cli: InstallTargetCli) -> &'static str {
|
||||||
match target_cli {
|
match target_cli {
|
||||||
InstallTargetCli::ClaudeCode => "Claude Code",
|
InstallTargetCli::ClaudeCode => "Claude Code",
|
||||||
@@ -581,6 +648,59 @@ pub(crate) async fn build_api_key_install_session_response(
|
|||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn build_proxy_node_install_session_response(
|
||||||
|
state: &AppState,
|
||||||
|
request_context: &GatewayPublicRequestContext,
|
||||||
|
headers: &http::HeaderMap,
|
||||||
|
node_name: String,
|
||||||
|
management_token: String,
|
||||||
|
) -> Response<Body> {
|
||||||
|
let code = generate_install_code();
|
||||||
|
let expires_at_unix_secs = unix_secs_now().saturating_add(INSTALL_SESSION_TTL_SECS);
|
||||||
|
let session = StoredProxyInstallSession {
|
||||||
|
aether_url: base_url_from_request(headers, request_context),
|
||||||
|
management_token,
|
||||||
|
node_name,
|
||||||
|
expires_at_unix_secs,
|
||||||
|
};
|
||||||
|
let serialized = match serde_json::to_string(&session) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
return build_auth_error_response(
|
||||||
|
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("proxy install session serialize failed: {err:?}"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(err) = state
|
||||||
|
.runtime_kv_setex(
|
||||||
|
&proxy_install_session_runtime_key(&code),
|
||||||
|
&serialized,
|
||||||
|
INSTALL_SESSION_TTL_SECS,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return build_auth_error_response(
|
||||||
|
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("proxy install session create failed: {err:?}"),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_url = session.aether_url.trim_end_matches('/');
|
||||||
|
Json(json!({
|
||||||
|
"install_code": code,
|
||||||
|
"expires_at_unix_secs": expires_at_unix_secs,
|
||||||
|
"expires_in_seconds": INSTALL_SESSION_TTL_SECS,
|
||||||
|
"node_name": session.node_name,
|
||||||
|
"aether_url": session.aether_url,
|
||||||
|
"unix_command": format!("curl -fsSL {base_url}/install-proxy/{code} | sh"),
|
||||||
|
"powershell_command": format!("irm {base_url}/install-proxy/{code}.ps1 | iex"),
|
||||||
|
}))
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn maybe_build_local_install_response(
|
pub(super) async fn maybe_build_local_install_response(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
request_context: &GatewayPublicRequestContext,
|
request_context: &GatewayPublicRequestContext,
|
||||||
@@ -589,6 +709,9 @@ pub(super) async fn maybe_build_local_install_response(
|
|||||||
if decision.route_family.as_deref() != Some("install") {
|
if decision.route_family.as_deref() != Some("install") {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
if request_context.request_path.starts_with("/install-proxy/") {
|
||||||
|
return Some(maybe_build_local_proxy_install_response(state, request_context).await);
|
||||||
|
}
|
||||||
let Some((code, wants_powershell)) = install_code_from_path(&request_context.request_path)
|
let Some((code, wants_powershell)) = install_code_from_path(&request_context.request_path)
|
||||||
else {
|
else {
|
||||||
return Some(build_auth_error_response(
|
return Some(build_auth_error_response(
|
||||||
@@ -664,6 +787,86 @@ pub(super) async fn maybe_build_local_install_response(
|
|||||||
Some(response)
|
Some(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn maybe_build_local_proxy_install_response(
|
||||||
|
state: &AppState,
|
||||||
|
request_context: &GatewayPublicRequestContext,
|
||||||
|
) -> Response<Body> {
|
||||||
|
let Some((code, wants_powershell)) =
|
||||||
|
proxy_install_code_from_path(&request_context.request_path)
|
||||||
|
else {
|
||||||
|
return build_auth_error_response(
|
||||||
|
http::StatusCode::NOT_FOUND,
|
||||||
|
"proxy install code 不存在或已失效",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let raw = match state
|
||||||
|
.runtime_kv_getdel(&proxy_install_session_runtime_key(&code))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(value)) => value,
|
||||||
|
Ok(None) => {
|
||||||
|
return build_auth_error_response(
|
||||||
|
http::StatusCode::NOT_FOUND,
|
||||||
|
"proxy install code 不存在、已过期或已使用",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return build_auth_error_response(
|
||||||
|
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("proxy install session lookup failed: {err:?}"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let session = match serde_json::from_str::<StoredProxyInstallSession>(&raw) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => {
|
||||||
|
return build_auth_error_response(
|
||||||
|
http::StatusCode::BAD_REQUEST,
|
||||||
|
"proxy install code 数据无效",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if session.expires_at_unix_secs <= unix_secs_now() {
|
||||||
|
return build_auth_error_response(
|
||||||
|
http::StatusCode::NOT_FOUND,
|
||||||
|
"proxy install code 已过期",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let body = if wants_powershell {
|
||||||
|
build_proxy_powershell_script(&session)
|
||||||
|
} else {
|
||||||
|
build_proxy_unix_script(&session)
|
||||||
|
};
|
||||||
|
let content_type = if wants_powershell {
|
||||||
|
"text/plain; charset=utf-8"
|
||||||
|
} else {
|
||||||
|
"text/x-shellscript; charset=utf-8"
|
||||||
|
};
|
||||||
|
let mut response = Response::new(Body::from(body));
|
||||||
|
response.headers_mut().insert(
|
||||||
|
http::header::CONTENT_TYPE,
|
||||||
|
http::HeaderValue::from_static(content_type),
|
||||||
|
);
|
||||||
|
response.headers_mut().insert(
|
||||||
|
http::header::CACHE_CONTROL,
|
||||||
|
http::HeaderValue::from_static("no-store"),
|
||||||
|
);
|
||||||
|
response.headers_mut().insert(
|
||||||
|
http::header::PRAGMA,
|
||||||
|
http::HeaderValue::from_static("no-cache"),
|
||||||
|
);
|
||||||
|
response.headers_mut().insert(
|
||||||
|
http::header::HeaderName::from_static("x-content-type-options"),
|
||||||
|
http::HeaderValue::from_static("nosniff"),
|
||||||
|
);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -680,6 +883,50 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_proxy_session() -> StoredProxyInstallSession {
|
||||||
|
StoredProxyInstallSession {
|
||||||
|
aether_url: "https://aether.example".to_string(),
|
||||||
|
management_token: "ae-test-token".to_string(),
|
||||||
|
node_name: "jp-proxy-01".to_string(),
|
||||||
|
expires_at_unix_secs: u64::MAX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn proxy_install_path_accepts_shell_and_powershell_codes() {
|
||||||
|
assert_eq!(
|
||||||
|
proxy_install_code_from_path("/install-proxy/abc123"),
|
||||||
|
Some(("abc123".to_string(), false))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
proxy_install_code_from_path("/install-proxy/abc123.ps1"),
|
||||||
|
Some(("abc123".to_string(), true))
|
||||||
|
);
|
||||||
|
assert_eq!(proxy_install_code_from_path("/install-proxy/a/b"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn proxy_unix_script_exports_session_values_and_reuses_proxy_installer() {
|
||||||
|
let script = build_proxy_unix_script(&test_proxy_session());
|
||||||
|
|
||||||
|
assert!(script.contains("export AETHER_PROXY_AETHER_URL='https://aether.example'"));
|
||||||
|
assert!(script.contains("export AETHER_PROXY_MANAGEMENT_TOKEN='ae-test-token'"));
|
||||||
|
assert!(script.contains("export AETHER_PROXY_NODE_NAME='jp-proxy-01'"));
|
||||||
|
assert!(script.contains("apps/aether-proxy/install.sh"));
|
||||||
|
assert!(!script.contains("[[servers]]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn proxy_powershell_script_exports_session_values_and_reuses_proxy_installer() {
|
||||||
|
let script = build_proxy_powershell_script(&test_proxy_session());
|
||||||
|
|
||||||
|
assert!(script.contains("$env:AETHER_PROXY_AETHER_URL = 'https://aether.example'"));
|
||||||
|
assert!(script.contains("$env:AETHER_PROXY_MANAGEMENT_TOKEN = 'ae-test-token'"));
|
||||||
|
assert!(script.contains("$env:AETHER_PROXY_NODE_NAME = 'jp-proxy-01'"));
|
||||||
|
assert!(script.contains("apps/aether-proxy/install.ps1"));
|
||||||
|
assert!(!script.contains("[[servers]]"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codex_unix_script_preserves_config_and_uses_responses_bearer_token() {
|
fn codex_unix_script_preserves_config_and_uses_responses_bearer_token() {
|
||||||
let script = build_unix_script(&test_session(InstallTargetCli::CodexCli));
|
let script = build_unix_script(&test_session(InstallTargetCli::CodexCli));
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ fn frontend_path_bypasses_static(path: &str) -> bool {
|
|||||||
|| path.starts_with("/_gateway/")
|
|| path.starts_with("/_gateway/")
|
||||||
|| path.starts_with("/.well-known/")
|
|| path.starts_with("/.well-known/")
|
||||||
|| path.starts_with("/install/")
|
|| path.starts_with("/install/")
|
||||||
|
|| path.starts_with("/install-proxy/")
|
||||||
|| path.starts_with("/i/")
|
|| path.starts_with("/i/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user