298 lines
9.6 KiB
Rust
298 lines
9.6 KiB
Rust
// Monitor routes — LDOH incident dashboard proxy with cookie auth
|
|
|
|
use crate::{
|
|
error::{AppError, AppResult},
|
|
AppState,
|
|
};
|
|
use axum::{
|
|
extract::{Path, State},
|
|
http::{header, HeaderMap},
|
|
response::Response,
|
|
routing::{get, post},
|
|
Json, Router,
|
|
};
|
|
use serde_json::{json, Value};
|
|
|
|
const MONITOR_AUTH_COOKIE: &str = "meta_monitor_auth";
|
|
const LDOH_BASE_URL: &str = "https://ldoh.105117.xyz";
|
|
const LDOH_COOKIE_SETTING_KEY: &str = "monitor_ldoh_cookie";
|
|
|
|
pub fn routes() -> Router<AppState> {
|
|
Router::new()
|
|
.route(
|
|
"/api/monitor/config",
|
|
get(get_monitor_config).put(update_monitor_config),
|
|
)
|
|
.route("/api/monitor/session", post(create_session))
|
|
.route("/monitor-proxy/ldoh", get(ldoh_proxy_handler))
|
|
.route("/monitor-proxy/ldoh/", get(ldoh_proxy_handler))
|
|
.route("/monitor-proxy/ldoh/{*path}", get(ldoh_proxy_handler))
|
|
}
|
|
|
|
/// GET /api/monitor/config — returns whether LDOH cookie is configured
|
|
async fn get_monitor_config(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
|
let cookie: Option<(String,)> = sqlx::query_as("SELECT value FROM settings WHERE key = ?")
|
|
.bind(LDOH_COOKIE_SETTING_KEY)
|
|
.fetch_optional(&*state.db)
|
|
.await?;
|
|
|
|
let raw = cookie
|
|
.and_then(|(v,)| {
|
|
serde_json::from_str::<serde_json::Value>(&v)
|
|
.ok()
|
|
.and_then(|parsed| parsed.as_str().map(String::from))
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let configured = !raw.is_empty();
|
|
let masked = if configured {
|
|
mask_cookie_value(&raw)
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
Ok(Json(json!({
|
|
"ldohCookieConfigured": configured,
|
|
"ldohCookieMasked": masked,
|
|
})))
|
|
}
|
|
|
|
/// PUT /api/monitor/config — save LDOH cookie
|
|
async fn update_monitor_config(
|
|
State(state): State<AppState>,
|
|
Json(body): Json<serde_json::Value>,
|
|
) -> AppResult<Json<Value>> {
|
|
let cookie = body
|
|
.get("ldohCookie")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("");
|
|
|
|
if cookie.is_empty() {
|
|
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, '')")
|
|
.bind(LDOH_COOKIE_SETTING_KEY)
|
|
.execute(&*state.db)
|
|
.await?;
|
|
return Ok(Json(json!({
|
|
"success": true,
|
|
"message": "LDOH Cookie 已清空",
|
|
"ldohCookieConfigured": false,
|
|
})));
|
|
}
|
|
|
|
let normalized = normalize_ldoh_cookie(cookie);
|
|
if !normalized.starts_with("ld_auth_session=") || normalized.len() < 24 {
|
|
return Err(AppError::BadRequest("Cookie 格式无效".into()));
|
|
}
|
|
|
|
let escaped = serde_json::to_string(&normalized).unwrap_or_default();
|
|
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
|
.bind(LDOH_COOKIE_SETTING_KEY)
|
|
.bind(&escaped)
|
|
.execute(&*state.db)
|
|
.await?;
|
|
|
|
Ok(Json(json!({
|
|
"success": true,
|
|
"message": "LDOH Cookie 已保存",
|
|
"ldohCookieConfigured": true,
|
|
"ldohCookieMasked": mask_cookie_value(&normalized),
|
|
})))
|
|
}
|
|
|
|
/// POST /api/monitor/session — returns cookie value for client to set
|
|
async fn create_session(State(state): State<AppState>) -> Json<Value> {
|
|
let token = &state.config.auth.auth_token;
|
|
Json(json!({
|
|
"success": true,
|
|
"_cookie": format!("{}={}; Path=/; HttpOnly; SameSite=Lax; Max-Age=7200", MONITOR_AUTH_COOKIE, token),
|
|
}))
|
|
}
|
|
|
|
/// GET /monitor-proxy/ldoh/{*path} — reverse proxy to ldoh.105117.xyz
|
|
async fn ldoh_proxy_handler(
|
|
State(state): State<AppState>,
|
|
headers: HeaderMap,
|
|
Path(path): Path<String>,
|
|
) -> Result<Response, AppError> {
|
|
// Auth check via cookie
|
|
let cookies = parse_cookies(
|
|
headers
|
|
.get(header::COOKIE)
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or(""),
|
|
);
|
|
if cookies.get(MONITOR_AUTH_COOKIE).map(|s| s.as_str()) != Some(&state.config.auth.auth_token) {
|
|
return Err(AppError::Unauthorized(
|
|
"Missing or invalid monitor session".into(),
|
|
));
|
|
}
|
|
|
|
// Get stored LDOH cookie
|
|
let stored: Option<(String,)> = sqlx::query_as("SELECT value FROM settings WHERE key = ?")
|
|
.bind(LDOH_COOKIE_SETTING_KEY)
|
|
.fetch_optional(&*state.db)
|
|
.await?;
|
|
|
|
let ldoh_cookie = stored
|
|
.and_then(|(v,)| {
|
|
serde_json::from_str::<serde_json::Value>(&v)
|
|
.ok()
|
|
.and_then(|parsed| parsed.as_str().map(String::from))
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
if ldoh_cookie.is_empty() {
|
|
return Err(AppError::BadRequest("LDOH cookie not configured".into()));
|
|
}
|
|
|
|
// Build upstream URL
|
|
let clean_path = if path.is_empty() || path == "/" {
|
|
String::new()
|
|
} else {
|
|
format!("/{}", path)
|
|
};
|
|
let target_url = format!("{}{}", LDOH_BASE_URL, clean_path);
|
|
|
|
let client = reqwest::Client::builder()
|
|
.timeout(std::time::Duration::from_secs(30))
|
|
.build()
|
|
.map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to build client: {e}")))?;
|
|
|
|
let mut req = client
|
|
.get(&target_url)
|
|
.header("cookie", &ldoh_cookie)
|
|
.header(
|
|
"accept",
|
|
headers
|
|
.get("accept")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("*/*"),
|
|
)
|
|
.header(
|
|
"accept-language",
|
|
headers
|
|
.get("accept-language")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("zh-CN,zh;q=0.9,en;q=0.8"),
|
|
)
|
|
.header(
|
|
"user-agent",
|
|
headers
|
|
.get("user-agent")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("boosapiMonitorProxy/1.0"),
|
|
);
|
|
|
|
if let Some(referer) = headers.get("referer").and_then(|v| v.to_str().ok()) {
|
|
req = req.header("referer", referer.replace("/monitor-proxy/ldoh", ""));
|
|
}
|
|
|
|
let resp = req
|
|
.send()
|
|
.await
|
|
.map_err(|e| AppError::UpstreamError(format!("LDOH unreachable: {e}")))?;
|
|
|
|
let status = resp.status();
|
|
let content_type = resp
|
|
.headers()
|
|
.get("content-type")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("application/octet-stream")
|
|
.to_string();
|
|
|
|
let mut builder = axum::response::Response::builder().status(status);
|
|
if let Some(ctl) = resp
|
|
.headers()
|
|
.get("cache-control")
|
|
.and_then(|v| v.to_str().ok())
|
|
{
|
|
builder = builder.header("cache-control", ctl);
|
|
}
|
|
|
|
// Rewrite text content
|
|
if content_type.contains("text/html")
|
|
|| content_type.contains("javascript")
|
|
|| content_type.contains("text/css")
|
|
|| content_type.contains("application/json")
|
|
{
|
|
let text = resp
|
|
.text()
|
|
.await
|
|
.map_err(|e| AppError::UpstreamError(e.to_string()))?;
|
|
return builder
|
|
.header(header::CONTENT_TYPE, &content_type)
|
|
.body(axum::body::Body::from(rewrite_proxy_text(&text)))
|
|
.map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")));
|
|
}
|
|
|
|
let bytes = resp
|
|
.bytes()
|
|
.await
|
|
.map_err(|e| AppError::UpstreamError(e.to_string()))?;
|
|
builder
|
|
.header(header::CONTENT_TYPE, &content_type)
|
|
.body(axum::body::Body::from(bytes))
|
|
.map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))
|
|
}
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
|
|
fn parse_cookies(raw: &str) -> std::collections::HashMap<String, String> {
|
|
let mut map = std::collections::HashMap::new();
|
|
for part in raw.split(';') {
|
|
let part = part.trim();
|
|
if let Some(idx) = part.find('=') {
|
|
let key = part[..idx].trim().to_string();
|
|
let val = part[idx + 1..].trim().to_string();
|
|
if !key.is_empty() {
|
|
map.insert(key, val);
|
|
}
|
|
}
|
|
}
|
|
map
|
|
}
|
|
|
|
fn mask_cookie_value(cookie_text: &str) -> String {
|
|
let raw = cookie_text.trim();
|
|
if raw.is_empty() {
|
|
return String::new();
|
|
}
|
|
let value = raw.split('=').nth(1).unwrap_or(raw);
|
|
if value.len() <= 10 {
|
|
format!("{}****", &value[..2.min(value.len())])
|
|
} else {
|
|
format!("{}****{}", &value[..6], &value[value.len() - 4..])
|
|
}
|
|
}
|
|
|
|
fn normalize_ldoh_cookie(raw: &str) -> String {
|
|
let trimmed = raw.trim().to_string();
|
|
if trimmed.contains("ld_auth_session=") {
|
|
if let Some(first) = trimmed.split(';').next() {
|
|
if first.trim().starts_with("ld_auth_session=") {
|
|
return first.trim().to_string();
|
|
}
|
|
}
|
|
}
|
|
format!("ld_auth_session={}", trimmed)
|
|
}
|
|
|
|
fn rewrite_proxy_text(text: &str) -> String {
|
|
text.replace("https://ldoh.105117.xyz/", "/monitor-proxy/ldoh/")
|
|
.replace(
|
|
"https:\\/\\/ldoh.105117.xyz\\/",
|
|
"\\/monitor-proxy\\/ldoh\\/",
|
|
)
|
|
.replace("src=\"/", "src=\"/monitor-proxy/ldoh/")
|
|
.replace("src='/", "src='/monitor-proxy/ldoh/")
|
|
.replace("href=\"/", "href=\"/monitor-proxy/ldoh/")
|
|
.replace("href='/", "href='/monitor-proxy/ldoh/")
|
|
.replace("action=\"/", "action=\"/monitor-proxy/ldoh/")
|
|
.replace("action='/", "action='/monitor-proxy/ldoh/")
|
|
.replace("\"/_next/", "\"/monitor-proxy/ldoh/_next/")
|
|
.replace("'/_next/", "'/monitor-proxy/ldoh/_next/")
|
|
.replace("\"\\\\/api/", "\"\\\\/monitor-proxy\\\\/ldoh\\\\/api/")
|
|
.replace("'/api/", "'/monitor-proxy/ldoh/api/")
|
|
.replace("\"/api/", "\"/monitor-proxy/ldoh/api/")
|
|
}
|