feat: Monitor (LDOH proxy) routes migrated to Rust

- GET/PUT /api/monitor/config — LDOH cookie management via settings table
- POST /api/monitor/session — cookie auth token
- GET /monitor-proxy/ldoh/{*path} — reverse proxy with content rewriting
- Cookie-based auth, HTML/JS/CSS path rewriting
- 18/18 tests pass
This commit is contained in:
boos
2026-05-17 17:13:28 +08:00
parent adb349eecc
commit 7a3a88f738
2 changed files with 249 additions and 0 deletions
+2
View File
@@ -7,6 +7,7 @@ mod checkin;
mod comfyui;
mod events;
mod health;
mod monitor;
mod organizations;
mod proxy;
mod site_announcements;
@@ -23,6 +24,7 @@ pub fn build_router(state: AppState) -> Router {
.merge(auth::routes())
.merge(events::routes())
.merge(checkin::routes())
.merge(monitor::routes())
.merge(organizations::routes())
.merge(site_announcements::routes())
.merge(stats::routes())
+247
View File
@@ -0,0 +1,247 @@
// Monitor routes — LDOH incident dashboard proxy with cookie auth
use crate::{
error::{AppError, AppResult},
AppState,
};
use axum::{
extract::{Path, State},
http::{header, HeaderMap, StatusCode},
response::Response,
routing::{get, post, put},
Json, Router,
};
use serde_json::{json, Value};
use tracing::info;
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 Ok(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()))?;
Ok(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/")
}