Skip to main content

meow_api/
routes.rs

1use axum::{
2    extract::ws::{Message, WebSocketUpgrade},
3    extract::{Path, Query, Request, State},
4    http::{header, StatusCode},
5    middleware::{self, Next},
6    response::{IntoResponse, Json, Response},
7    routing::{delete, get, post, put},
8    Router,
9};
10use dashmap::DashMap;
11use meow_common::{Proxy, TunnelMode};
12use meow_config::{
13    proxy_provider::ProxyProvider,
14    raw::{RawConfig, RawProxyGroup, RawSubscription},
15    rule_provider::RuleProvider,
16    NamedListener,
17};
18use meow_tunnel::Tunnel;
19use parking_lot::RwLock;
20use serde::{Deserialize, Serialize};
21use std::collections::{BTreeMap, HashMap};
22use std::sync::Arc;
23use std::time::Duration;
24use tokio::sync::broadcast;
25use tower_http::cors::CorsLayer;
26use tracing::{debug, info, warn};
27
28use crate::log_stream::{parse_log_level, LogMessage};
29use crate::ui;
30
31pub struct AppState {
32    pub tunnel: Tunnel,
33    /// Optional Bearer token enforced by `require_auth`. `None` or empty disables auth.
34    pub secret: Option<String>,
35    pub config_path: String,
36    pub raw_config: Arc<RwLock<RawConfig>>,
37    /// Fan-out channel for log events. Each WS client subscribes a Receiver.
38    pub log_tx: broadcast::Sender<LogMessage>,
39    /// Live proxy-provider registry — refreshed by background task and PUT endpoint.
40    pub proxy_providers: Arc<DashMap<String, Arc<ProxyProvider>>>,
41    pub rule_providers: Arc<RwLock<HashMap<String, Arc<RuleProvider>>>>,
42    /// Snapshot of active named listeners (read-only, startup-time only in M1).
43    pub listeners: Vec<NamedListener>,
44    /// Validated directory for a third-party web UI. When `Some`, it is served
45    /// at `/ui`; when `None`, the built-in panel is served (issue #223).
46    pub external_ui: Option<std::path::PathBuf>,
47}
48
49impl AppState {
50    fn auth_required(&self) -> bool {
51        self.secret.as_deref().is_some_and(|s| !s.is_empty())
52    }
53}
54
55/// Bearer token middleware. Matches upstream mihomo contract:
56/// `Authorization: Bearer <secret>`. When the configured secret is empty or
57/// unset, the middleware is a no-op. Otherwise, requests without a matching
58/// header return `401 Unauthorized`.
59async fn require_auth(State(state): State<Arc<AppState>>, req: Request, next: Next) -> Response {
60    if !state.auth_required() {
61        return next.run(req).await;
62    }
63
64    let Some(expected) = state.secret.as_deref() else {
65        return next.run(req).await;
66    };
67
68    let provided = req
69        .headers()
70        .get(header::AUTHORIZATION)
71        .and_then(|v| v.to_str().ok())
72        .and_then(|v| {
73            v.strip_prefix("Bearer ")
74                .or_else(|| v.strip_prefix("bearer "))
75        });
76
77    // Constant-time comparison so a byte-by-byte attacker cannot distinguish
78    // "first N bytes matched" from "failed immediately". Length still leaks;
79    // that is acceptable for a config-scoped shared secret.
80    let ok = match provided {
81        Some(token) if token.len() == expected.len() => {
82            use subtle::ConstantTimeEq;
83            token.as_bytes().ct_eq(expected.as_bytes()).into()
84        }
85        _ => false,
86    };
87    if ok {
88        next.run(req).await
89    } else {
90        (StatusCode::UNAUTHORIZED, "unauthorized").into_response()
91    }
92}
93
94/// Auth middleware for WebSocket upgrade routes. Accepts `Authorization: Bearer <secret>`
95/// header OR `?token=<secret>` query param (browser WebSocket clients cannot set headers).
96/// `?token=` is accepted ONLY on this middleware — REST routes keep header-only auth.
97async fn require_auth_ws(
98    State(state): State<Arc<AppState>>,
99    Query(query): Query<HashMap<String, String>>,
100    req: Request,
101    next: Next,
102) -> Response {
103    if !state.auth_required() {
104        return next.run(req).await;
105    }
106    let expected = state.secret.as_deref().unwrap_or("");
107
108    let bearer = req
109        .headers()
110        .get(header::AUTHORIZATION)
111        .and_then(|v| v.to_str().ok())
112        .and_then(|v| {
113            v.strip_prefix("Bearer ")
114                .or_else(|| v.strip_prefix("bearer "))
115        });
116
117    let token_param = query.get("token").map(std::string::String::as_str);
118    let provided = bearer.or(token_param);
119
120    let ok = match provided {
121        Some(t) if t.len() == expected.len() => {
122            use subtle::ConstantTimeEq;
123            t.as_bytes().ct_eq(expected.as_bytes()).into()
124        }
125        _ => false,
126    };
127    if ok {
128        next.run(req).await
129    } else {
130        (StatusCode::UNAUTHORIZED, "unauthorized").into_response()
131    }
132}
133
134pub fn create_router(state: Arc<AppState>) -> Router {
135    // WS routes — accept header or ?token= query param for browser dashboard compat.
136    let ws_routes = Router::new()
137        .route("/logs", get(get_logs))
138        .route("/memory", get(get_memory))
139        .route_layer(middleware::from_fn_with_state(
140            Arc::clone(&state),
141            require_auth_ws,
142        ));
143
144    // REST API routes gated behind the Bearer middleware (header-only).
145    let api = Router::new()
146        .route("/", get(hello))
147        .route("/version", get(version))
148        .route("/proxies", get(get_proxies))
149        .route("/proxies/{name}", get(get_proxy).put(update_proxy))
150        .route("/proxies/{name}/delay", get(get_proxy_delay))
151        .route("/group/{name}/delay", get(get_group_delay))
152        .route(
153            "/rules",
154            get(get_rules).post(replace_rules).put(update_rule_at_index),
155        )
156        .route("/rules/{index}", delete(delete_rule))
157        .route("/rules/reorder", post(reorder_rules))
158        .route("/connections", get(get_connections))
159        .route("/connections/{id}", delete(close_connection))
160        .route("/connections", delete(close_all_connections))
161        .route(
162            "/configs",
163            get(get_configs).patch(update_configs).put(put_configs),
164        )
165        .route("/metrics", get(get_metrics))
166        .route("/traffic", get(get_traffic))
167        .route("/dns/query", get(dns_query_get).post(dns_query))
168        .route("/cache/dns/flush", post(flush_dns_cache))
169        .route("/cache/fakeip/flush", post(flush_fakeip_cache))
170        // Config save
171        .route("/api/config/save", post(save_config))
172        // Subscriptions
173        .route(
174            "/api/subscriptions",
175            get(get_subscriptions).post(add_subscription),
176        )
177        .route("/api/subscriptions/{name}", delete(delete_subscription))
178        .route(
179            "/api/subscriptions/{name}/refresh",
180            post(refresh_subscription),
181        )
182        // Proxy groups
183        .route(
184            "/api/proxy-groups",
185            get(get_proxy_groups).post(create_proxy_group),
186        )
187        .route(
188            "/api/proxy-groups/{name}",
189            put(update_proxy_group).delete(delete_proxy_group),
190        )
191        .route(
192            "/api/proxy-groups/{name}/select",
193            put(select_proxy_in_group),
194        )
195        // Proxy providers
196        .route("/providers/proxies", get(get_providers))
197        .route(
198            "/providers/proxies/{name}",
199            get(get_provider).put(refresh_provider),
200        )
201        .route(
202            "/providers/proxies/{name}/healthcheck",
203            get(provider_healthcheck),
204        )
205        // Rule providers
206        .route("/providers/rules", get(get_rule_providers))
207        .route(
208            "/providers/rules/{name}",
209            get(get_rule_provider).put(refresh_rule_provider),
210        )
211        // Listeners (read-only list)
212        .route("/listeners", get(get_listeners))
213        .route_layer(middleware::from_fn_with_state(
214            Arc::clone(&state),
215            require_auth,
216        ));
217
218    // Web UI is intentionally unauthenticated so dashboards can load and then
219    // present a token prompt; this matches upstream mihomo behaviour.
220    //
221    // When `external-ui` is configured (issue #223) the static directory is
222    // served at `/ui` via tower-http's `ServeDir`; otherwise the built-in
223    // single-page panel is served.
224    let router = api.merge(ws_routes);
225    let router = if let Some(dir) = state.external_ui.clone() {
226        // `ServeDir` resolves `index.html` for the directory root and serves
227        // any nested asset; `nest_service("/ui", …)` strips the `/ui` prefix so
228        // both `/ui` and `/ui/<asset>` resolve. Dashboards (metacubexd, yacd)
229        // use hash routing, so no server-side SPA fallback is required.
230        router.nest_service("/ui", tower_http::services::ServeDir::new(dir))
231    } else {
232        router
233            .route("/ui", get(ui::serve_ui))
234            .route("/ui/{*rest}", get(ui::serve_ui))
235    };
236
237    router.layer(CorsLayer::permissive()).with_state(state)
238}
239
240// ── Basic endpoints ──────────────────────────────────────────────────
241
242async fn hello() -> &'static str {
243    "meow-rs"
244}
245
246#[derive(Serialize)]
247struct VersionResponse {
248    version: String,
249    meta: bool,
250}
251
252async fn version() -> Json<VersionResponse> {
253    Json(VersionResponse {
254        version: env!("CARGO_PKG_VERSION").to_string(),
255        meta: true,
256    })
257}
258
259#[derive(Serialize)]
260struct ProxyInfo {
261    name: String,
262    #[serde(rename = "type")]
263    proxy_type: String,
264    alive: bool,
265    history: Vec<meow_common::DelayHistory>,
266    udp: bool,
267    /// Group-only: ordered list of member proxy names.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    all: Option<Vec<String>>,
270    /// Group-only: name of the currently active member.
271    #[serde(skip_serializing_if = "Option::is_none")]
272    now: Option<String>,
273}
274
275impl ProxyInfo {
276    fn from_proxy(proxy: &Arc<dyn meow_common::Proxy>) -> Self {
277        let members = proxy.members();
278        let current = proxy.current();
279        debug!(
280            name = proxy.name(),
281            proxy_type = %proxy.adapter_type(),
282            member_count = members.as_ref().map(std::vec::Vec::len),
283            current = ?current,
284            "building ProxyInfo",
285        );
286        Self {
287            name: proxy.name().to_string(),
288            proxy_type: proxy.adapter_type().to_string(),
289            alive: proxy.alive(),
290            history: proxy.delay_history(),
291            udp: proxy.support_udp(),
292            all: members,
293            now: current,
294        }
295    }
296}
297
298#[derive(Serialize)]
299struct ProxiesResponse {
300    proxies: std::collections::HashMap<String, ProxyInfo>,
301}
302
303async fn get_proxies(State(state): State<Arc<AppState>>) -> Json<ProxiesResponse> {
304    let route = state.tunnel.route_snapshot();
305    let mut result = std::collections::HashMap::new();
306    for (name, proxy) in &route.proxies {
307        result.insert(name.to_string(), ProxyInfo::from_proxy(proxy));
308    }
309    Json(ProxiesResponse { proxies: result })
310}
311
312async fn get_proxy(
313    State(state): State<Arc<AppState>>,
314    Path(name): Path<String>,
315) -> Result<Json<ProxyInfo>, StatusCode> {
316    let route = state.tunnel.route_snapshot();
317    let proxy = route
318        .proxies
319        .get(name.as_str())
320        .ok_or(StatusCode::NOT_FOUND)?;
321    Ok(Json(ProxyInfo::from_proxy(proxy)))
322}
323
324#[derive(Deserialize)]
325struct UpdateProxyRequest {
326    name: String,
327}
328
329async fn update_proxy(
330    State(state): State<Arc<AppState>>,
331    Path(group_name): Path<String>,
332    Json(body): Json<UpdateProxyRequest>,
333) -> StatusCode {
334    let route = state.tunnel.route_snapshot();
335    let Some(proxy) = route.proxies.get(group_name.as_str()).cloned() else {
336        return StatusCode::NOT_FOUND;
337    };
338    match select_proxy_member_async(proxy, body.name.clone()).await {
339        Ok(Some(true)) => {
340            info!("Selector '{}' switched to '{}'", group_name, body.name);
341            StatusCode::NO_CONTENT
342        }
343        Ok(Some(false)) => StatusCode::BAD_REQUEST,
344        Ok(None) => StatusCode::NOT_FOUND,
345        Err(e) => {
346            warn!("Selector '{}' update task failed: {}", group_name, e);
347            StatusCode::INTERNAL_SERVER_ERROR
348        }
349    }
350}
351
352#[derive(Serialize)]
353struct RuleInfo<'a> {
354    #[serde(rename = "type")]
355    rule_type: &'static str,
356    payload: &'a str,
357    proxy: &'a str,
358}
359
360#[derive(Serialize)]
361struct RulesResponse<'a> {
362    rules: Vec<RuleInfo<'a>>,
363}
364
365async fn get_rules(State(state): State<Arc<AppState>>) -> Response {
366    // Serialise straight off the route snapshot — the old rules_info()
367    // accessor built 3 Strings per rule per call (audit #182).
368    let route = state.tunnel.route_snapshot();
369    let result: Vec<RuleInfo> = route
370        .rules
371        .iter()
372        .map(|r| RuleInfo {
373            rule_type: r.rule_type().as_str(),
374            payload: r.payload(),
375            proxy: r.adapter(),
376        })
377        .collect();
378    Json(RulesResponse { rules: result }).into_response()
379}
380
381#[derive(Serialize)]
382struct ConnectionsResponse<'a> {
383    upload_total: i64,
384    download_total: i64,
385    /// Serialised straight from the live table — no per-connection
386    /// `serde_json::Value` tree, no cloned snapshot Vec (audit M8). The
387    /// JSON shape (id/upload/download/start/chains/rule/rulePayload) comes
388    /// from `ConnectionInfo`'s `Serialize` derive.
389    connections: meow_tunnel::statistics::ActiveConnectionsView<'a>,
390}
391
392async fn get_connections(State(state): State<Arc<AppState>>) -> Response {
393    let stats = state.tunnel.statistics();
394    let (up, down) = stats.snapshot();
395    Json(ConnectionsResponse {
396        upload_total: up,
397        download_total: down,
398        connections: stats.active_connections_view(),
399    })
400    .into_response()
401}
402
403async fn close_connection(
404    State(state): State<Arc<AppState>>,
405    Path(id): Path<String>,
406) -> StatusCode {
407    match uuid::Uuid::parse_str(&id) {
408        Ok(uuid) => {
409            state.tunnel.statistics().close_connection(uuid);
410            StatusCode::NO_CONTENT
411        }
412        Err(_) => StatusCode::BAD_REQUEST,
413    }
414}
415
416#[derive(Serialize)]
417struct ConfigResponse {
418    mode: String,
419    #[serde(rename = "log-level")]
420    log_level: String,
421    #[serde(rename = "mixed-port", skip_serializing_if = "Option::is_none")]
422    mixed_port: Option<u16>,
423    #[serde(rename = "socks-port", skip_serializing_if = "Option::is_none")]
424    socks_port: Option<u16>,
425    #[serde(rename = "port", skip_serializing_if = "Option::is_none")]
426    http_port: Option<u16>,
427    #[serde(
428        rename = "external-controller",
429        skip_serializing_if = "Option::is_none"
430    )]
431    external_controller: Option<String>,
432}
433
434async fn get_configs(State(state): State<Arc<AppState>>) -> Json<ConfigResponse> {
435    let raw = state.raw_config.read();
436    Json(ConfigResponse {
437        mode: state.tunnel.mode().to_string(),
438        log_level: "info".to_string(),
439        mixed_port: raw.mixed_port,
440        socks_port: raw.socks_port,
441        http_port: raw.port,
442        external_controller: raw.external_controller.clone(),
443    })
444}
445
446#[derive(Deserialize)]
447struct UpdateConfigRequest {
448    mode: Option<String>,
449    #[serde(rename = "log-level")]
450    log_level: Option<String>,
451}
452
453async fn update_configs(
454    State(state): State<Arc<AppState>>,
455    Json(body): Json<UpdateConfigRequest>,
456) -> StatusCode {
457    if let Some(mode_str) = body.mode {
458        match mode_str.parse::<TunnelMode>() {
459            Ok(mode) => {
460                state.tunnel.set_mode(mode);
461                info!("Mode changed to {}", mode);
462            }
463            Err(_) => return StatusCode::BAD_REQUEST,
464        }
465    }
466    let _ = body.log_level;
467    StatusCode::NO_CONTENT
468}
469
470#[derive(Serialize)]
471struct TrafficResponse {
472    up: i64,
473    down: i64,
474}
475
476async fn get_traffic(State(state): State<Arc<AppState>>) -> Json<TrafficResponse> {
477    let (up, down) = state.tunnel.statistics().snapshot();
478    Json(TrafficResponse { up, down })
479}
480
481#[derive(Deserialize)]
482struct DnsQueryRequest {
483    name: String,
484    #[serde(rename = "type")]
485    qtype: Option<String>,
486}
487
488async fn dns_query(
489    State(state): State<Arc<AppState>>,
490    Json(body): Json<DnsQueryRequest>,
491) -> Json<serde_json::Value> {
492    let resolver = state.tunnel.resolver();
493    let result = resolver.resolve_ip(&body.name).await;
494    let _ = body.qtype;
495    Json(serde_json::json!({ "name": body.name, "answer": result.map(|ip| ip.to_string()) }))
496}
497
498// upstream: hub/route/dns.go — GET alias added alongside existing POST.
499// Class B per ADR-0002: POST kept for back-compat; GET matches upstream's current form.
500async fn dns_query_get(
501    State(state): State<Arc<AppState>>,
502    Query(params): Query<DnsQueryRequest>,
503) -> Json<serde_json::Value> {
504    let resolver = state.tunnel.resolver();
505    let result = resolver.resolve_ip(&params.name).await;
506    Json(serde_json::json!({ "name": params.name, "answer": result.map(|ip| ip.to_string()) }))
507}
508
509async fn flush_dns_cache(State(state): State<Arc<AppState>>) -> StatusCode {
510    state.tunnel.resolver().clear_cache();
511    StatusCode::NO_CONTENT
512}
513
514/// `POST /cache/fakeip/flush` — clear every fake-IP allocation. Mirrors
515/// upstream `hub/route/cache.go::flushFakeIPPool`. Returns 204 on success,
516/// 400 with a JSON `{message: ...}` body if persistence flushing fails.
517async fn flush_fakeip_cache(
518    State(state): State<Arc<AppState>>,
519) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
520    match state.tunnel.resolver().flush_fake_ip() {
521        Ok(()) => Ok(StatusCode::NO_CONTENT),
522        Err(e) => Err((
523            StatusCode::BAD_REQUEST,
524            Json(serde_json::json!({ "message": e.to_string() })),
525        )),
526    }
527}
528
529async fn close_all_connections(State(state): State<Arc<AppState>>) -> StatusCode {
530    state.tunnel.statistics().close_all_connections();
531    StatusCode::NO_CONTENT
532}
533
534// ── Config save ──────────────────────────────────────────────────────
535
536async fn save_config(
537    State(state): State<Arc<AppState>>,
538) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
539    let raw = state.raw_config.read().clone();
540    meow_config::save_raw_config_async(&state.config_path, &raw)
541        .await
542        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
543    Ok(Json(serde_json::json!({"message": "config saved"})))
544}
545
546// ── Helper: rebuild proxies/rules from raw and apply to tunnel ───────
547
548/// Pre-resolve DNS-sourced ECH then rebuild proxies/rules from `raw` and
549/// apply to the live tunnel. Takes the config *by value* so callers
550/// clone-and-drop their `parking_lot` guard before awaiting — those guards
551/// are not Send and would otherwise break the axum Handler bound.
552async fn apply_raw_to_tunnel(
553    mut raw: RawConfig,
554    tunnel: &Tunnel,
555) -> Result<(), (StatusCode, String)> {
556    if let Some(ps) = raw.proxies.as_mut() {
557        meow_config::ech_dns::preresolve_ech(ps).await;
558    }
559    let (proxies, rules) = rebuild_from_raw_with_resolver_async(raw, Arc::clone(tunnel.resolver()))
560        .await
561        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
562    tunnel.update_proxies(proxies);
563    tunnel.update_rules(rules);
564    Ok(())
565}
566
567async fn rebuild_from_raw_with_resolver_async(
568    raw: RawConfig,
569    resolver: Arc<meow_dns::Resolver>,
570) -> Result<meow_config::RebuildResult, String> {
571    tokio::task::spawn_blocking(move || {
572        meow_config::rebuild_from_raw_with_resolver(&raw, Some(resolver))
573    })
574    .await
575    .map_err(|e| format!("config rebuild task failed: {e}"))?
576    .map_err(|e| e.to_string())
577}
578
579async fn select_proxy_member_async(
580    proxy: Arc<dyn Proxy>,
581    member: String,
582) -> Result<Option<bool>, tokio::task::JoinError> {
583    tokio::task::spawn_blocking(move || {
584        use meow_proxy::SelectorGroup;
585        proxy
586            .as_any()
587            .and_then(|a| a.downcast_ref::<SelectorGroup>())
588            .map(|selector| selector.select(&member))
589    })
590    .await
591}
592
593// ── Subscriptions ────────────────────────────────────────────────────
594// Subscriptions replace local proxies/groups/rules with the remote data as-is.
595
596#[derive(Serialize)]
597struct SubscriptionInfo {
598    name: String,
599    url: String,
600    interval: Option<u64>,
601    last_updated: Option<i64>,
602    proxy_count: usize,
603    group_count: usize,
604    rule_count: usize,
605}
606
607async fn get_subscriptions(State(state): State<Arc<AppState>>) -> Json<Vec<SubscriptionInfo>> {
608    let raw = state.raw_config.read();
609    let subs = raw.subscriptions.as_deref().unwrap_or(&[]);
610    let result: Vec<SubscriptionInfo> = subs
611        .iter()
612        .map(|s| SubscriptionInfo {
613            name: s.name.clone(),
614            url: s.url.clone(),
615            interval: s.interval,
616            last_updated: s.last_updated,
617            proxy_count: raw.proxies.as_ref().map_or(0, std::vec::Vec::len),
618            group_count: raw.proxy_groups.as_ref().map_or(0, std::vec::Vec::len),
619            rule_count: raw.rules.as_ref().map_or(0, std::vec::Vec::len),
620        })
621        .collect();
622    Json(result)
623}
624
625#[derive(Deserialize)]
626struct AddSubscriptionRequest {
627    name: String,
628    url: String,
629    interval: Option<u64>,
630}
631
632async fn add_subscription(
633    State(state): State<Arc<AppState>>,
634    Json(body): Json<AddSubscriptionRequest>,
635) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
636    let fetched = meow_config::subscription::fetch_subscription(&body.url)
637        .await
638        .map_err(|e| (StatusCode::BAD_REQUEST, format!("fetch failed: {e}")))?;
639
640    let now = std::time::SystemTime::now()
641        .duration_since(std::time::UNIX_EPOCH)
642        .unwrap_or_default()
643        .as_secs() as i64;
644
645    let pc = fetched.proxies.len();
646    let gc = fetched.proxy_groups.len();
647    let rc = fetched.rules.len();
648
649    let snapshot = {
650        let mut raw = state.raw_config.write();
651
652        if let Some(ref subs) = raw.subscriptions {
653            if subs.iter().any(|s| s.name == body.name) {
654                return Err((
655                    StatusCode::CONFLICT,
656                    "subscription name already exists".into(),
657                ));
658            }
659        }
660
661        let sub = RawSubscription {
662            name: body.name.clone(),
663            url: body.url.clone(),
664            interval: body.interval,
665            last_updated: Some(now),
666        };
667        raw.subscriptions.get_or_insert_with(Vec::new).push(sub);
668
669        // Replace proxies, groups, and rules with remote data as-is
670        raw.proxies = Some(fetched.proxies);
671        raw.proxy_groups = Some(fetched.proxy_groups);
672        raw.rules = Some(fetched.rules);
673
674        raw.clone()
675    };
676    apply_raw_to_tunnel(snapshot, &state.tunnel).await?;
677
678    // Auto-save so subscription data is cached on disk
679    let raw = state.raw_config.read().clone();
680    let _ = meow_config::save_raw_config_async(&state.config_path, &raw).await;
681
682    Ok(Json(serde_json::json!({
683        "message": "subscription added",
684        "proxy_count": pc, "group_count": gc, "rule_count": rc
685    })))
686}
687
688async fn delete_subscription(
689    State(state): State<Arc<AppState>>,
690    Path(name): Path<String>,
691) -> Result<StatusCode, (StatusCode, String)> {
692    let snapshot = {
693        let mut raw = state.raw_config.write();
694
695        if let Some(ref mut subs) = raw.subscriptions {
696            let before = subs.len();
697            subs.retain(|s| s.name != name);
698            if subs.len() == before {
699                return Err((StatusCode::NOT_FOUND, "subscription not found".into()));
700            }
701        } else {
702            return Err((StatusCode::NOT_FOUND, "no subscriptions".into()));
703        }
704
705        // Clear everything from the remote subscription
706        raw.proxies = Some(Vec::new());
707        raw.proxy_groups = Some(Vec::new());
708        raw.rules = Some(Vec::new());
709
710        raw.clone()
711    };
712    apply_raw_to_tunnel(snapshot, &state.tunnel).await?;
713    let raw = state.raw_config.read().clone();
714    let _ = meow_config::save_raw_config_async(&state.config_path, &raw).await;
715    Ok(StatusCode::NO_CONTENT)
716}
717
718async fn refresh_subscription(
719    State(state): State<Arc<AppState>>,
720    Path(name): Path<String>,
721) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
722    let url = {
723        let raw = state.raw_config.read();
724        raw.subscriptions
725            .as_ref()
726            .and_then(|subs| subs.iter().find(|s| s.name == name))
727            .map(|s| s.url.clone())
728            .ok_or_else(|| (StatusCode::NOT_FOUND, "subscription not found".into()))?
729    };
730
731    let fetched = meow_config::subscription::fetch_subscription(&url)
732        .await
733        .map_err(|e| (StatusCode::BAD_REQUEST, format!("fetch failed: {e}")))?;
734
735    let now = std::time::SystemTime::now()
736        .duration_since(std::time::UNIX_EPOCH)
737        .unwrap_or_default()
738        .as_secs() as i64;
739
740    let pc = fetched.proxies.len();
741    let gc = fetched.proxy_groups.len();
742    let rc = fetched.rules.len();
743
744    let snapshot = {
745        let mut raw = state.raw_config.write();
746
747        if let Some(ref mut subs) = raw.subscriptions {
748            if let Some(sub) = subs.iter_mut().find(|s| s.name == name) {
749                sub.last_updated = Some(now);
750            }
751        }
752
753        raw.proxies = Some(fetched.proxies);
754        raw.proxy_groups = Some(fetched.proxy_groups);
755        raw.rules = Some(fetched.rules);
756
757        raw.clone()
758    };
759    apply_raw_to_tunnel(snapshot, &state.tunnel).await?;
760
761    // Auto-save so subscription data is cached on disk
762    let raw = state.raw_config.read().clone();
763    let _ = meow_config::save_raw_config_async(&state.config_path, &raw).await;
764
765    Ok(Json(serde_json::json!({
766        "message": "subscription refreshed",
767        "proxy_count": pc, "group_count": gc, "rule_count": rc
768    })))
769}
770
771// ── Proxy Groups ─────────────────────────────────────────────────────
772
773#[derive(Serialize)]
774struct ProxyGroupInfo {
775    name: String,
776    #[serde(rename = "type")]
777    group_type: String,
778    proxies: Vec<String>,
779    now: Option<String>,
780    url: Option<String>,
781    interval: Option<u64>,
782    tolerance: Option<u16>,
783}
784
785async fn get_proxy_groups(State(state): State<Arc<AppState>>) -> Json<Vec<ProxyGroupInfo>> {
786    let raw = state.raw_config.read();
787    let groups = raw.proxy_groups.as_deref().unwrap_or(&[]);
788    let route = state.tunnel.route_snapshot();
789    let tunnel_proxies = &route.proxies;
790
791    let result: Vec<ProxyGroupInfo> = groups
792        .iter()
793        .map(|g| {
794            let runtime = tunnel_proxies.get(g.name.as_str());
795            let now = runtime.and_then(|p| p.current());
796            let proxies = runtime
797                .and_then(|p| p.members())
798                .unwrap_or_else(|| g.proxies.clone().unwrap_or_default());
799            ProxyGroupInfo {
800                name: g.name.clone(),
801                group_type: g.group_type.clone(),
802                proxies,
803                now,
804                url: g.url.clone(),
805                interval: g.interval,
806                tolerance: g.tolerance,
807            }
808        })
809        .collect();
810    Json(result)
811}
812
813#[derive(Deserialize)]
814struct CreateProxyGroupRequest {
815    name: String,
816    #[serde(rename = "type")]
817    group_type: String,
818    proxies: Vec<String>,
819    url: Option<String>,
820    interval: Option<u64>,
821    tolerance: Option<u16>,
822}
823
824async fn create_proxy_group(
825    State(state): State<Arc<AppState>>,
826    Json(body): Json<CreateProxyGroupRequest>,
827) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
828    let group_name = body.name.clone();
829    let snapshot = {
830        let mut raw = state.raw_config.write();
831        if let Some(ref groups) = raw.proxy_groups {
832            if groups.iter().any(|g| g.name == body.name) {
833                return Err((StatusCode::CONFLICT, "group name already exists".into()));
834            }
835        }
836        let group = RawProxyGroup {
837            name: body.name,
838            group_type: body.group_type,
839            proxies: Some(body.proxies),
840            url: body.url,
841            interval: body.interval,
842            tolerance: body.tolerance,
843            ..Default::default()
844        };
845        raw.proxy_groups.get_or_insert_with(Vec::new).push(group);
846        raw.clone()
847    };
848    apply_raw_to_tunnel(snapshot, &state.tunnel).await?;
849    Ok(Json(
850        serde_json::json!({"message": "group created", "name": group_name}),
851    ))
852}
853
854async fn update_proxy_group(
855    State(state): State<Arc<AppState>>,
856    Path(name): Path<String>,
857    Json(body): Json<CreateProxyGroupRequest>,
858) -> Result<StatusCode, (StatusCode, String)> {
859    let snapshot = {
860        let mut raw = state.raw_config.write();
861        let group = raw
862            .proxy_groups
863            .as_mut()
864            .and_then(|groups| groups.iter_mut().find(|g| g.name == name))
865            .ok_or_else(|| (StatusCode::NOT_FOUND, "group not found".into()))?;
866        group.group_type = body.group_type;
867        group.proxies = Some(body.proxies);
868        group.url = body.url;
869        group.interval = body.interval;
870        group.tolerance = body.tolerance;
871        raw.clone()
872    };
873    apply_raw_to_tunnel(snapshot, &state.tunnel).await?;
874    Ok(StatusCode::NO_CONTENT)
875}
876
877async fn delete_proxy_group(
878    State(state): State<Arc<AppState>>,
879    Path(name): Path<String>,
880) -> Result<StatusCode, (StatusCode, String)> {
881    let snapshot = {
882        let mut raw = state.raw_config.write();
883        if let Some(ref mut groups) = raw.proxy_groups {
884            let before = groups.len();
885            groups.retain(|g| g.name != name);
886            if groups.len() == before {
887                return Err((StatusCode::NOT_FOUND, "group not found".into()));
888            }
889        } else {
890            return Err((StatusCode::NOT_FOUND, "no groups".into()));
891        }
892        if let Some(ref mut rules) = raw.rules {
893            rules.retain(|r| {
894                let parts: Vec<&str> = r.split(',').collect();
895                parts.last().is_none_or(|target| target.trim() != name)
896            });
897        }
898        raw.clone()
899    };
900    apply_raw_to_tunnel(snapshot, &state.tunnel).await?;
901    Ok(StatusCode::NO_CONTENT)
902}
903
904#[derive(Deserialize)]
905struct SelectProxyRequest {
906    name: String,
907}
908
909async fn select_proxy_in_group(
910    State(state): State<Arc<AppState>>,
911    Path(group_name): Path<String>,
912    Json(body): Json<SelectProxyRequest>,
913) -> StatusCode {
914    let route = state.tunnel.route_snapshot();
915    let Some(proxy) = route.proxies.get(group_name.as_str()).cloned() else {
916        return StatusCode::NOT_FOUND;
917    };
918    match select_proxy_member_async(proxy, body.name.clone()).await {
919        Ok(Some(true)) => {
920            info!("Selector '{}' switched to '{}'", group_name, body.name);
921            StatusCode::NO_CONTENT
922        }
923        Ok(Some(false)) => StatusCode::BAD_REQUEST,
924        Ok(None) => StatusCode::NOT_FOUND,
925        Err(e) => {
926            warn!("Selector '{}' update task failed: {}", group_name, e);
927            StatusCode::INTERNAL_SERVER_ERROR
928        }
929    }
930}
931
932// ── Rules CRUD ───────────────────────────────────────────────────────
933
934#[derive(Deserialize)]
935struct ReplaceRulesRequest {
936    rules: Vec<String>,
937}
938
939async fn replace_rules(
940    State(state): State<Arc<AppState>>,
941    Json(body): Json<ReplaceRulesRequest>,
942) -> Result<StatusCode, (StatusCode, String)> {
943    let snapshot = {
944        let mut raw = state.raw_config.write();
945        raw.rules = Some(body.rules);
946        raw.clone()
947    };
948    apply_raw_to_tunnel(snapshot, &state.tunnel).await?;
949    Ok(StatusCode::NO_CONTENT)
950}
951
952#[derive(Deserialize)]
953struct UpdateRuleRequest {
954    index: usize,
955    rule: String,
956}
957
958async fn update_rule_at_index(
959    State(state): State<Arc<AppState>>,
960    Json(body): Json<UpdateRuleRequest>,
961) -> Result<StatusCode, (StatusCode, String)> {
962    let snapshot = {
963        let mut raw = state.raw_config.write();
964        let rules = raw.rules.get_or_insert_with(Vec::new);
965        if body.index >= rules.len() {
966            return Err((StatusCode::BAD_REQUEST, "index out of range".into()));
967        }
968        rules[body.index] = body.rule;
969        raw.clone()
970    };
971    apply_raw_to_tunnel(snapshot, &state.tunnel).await?;
972    Ok(StatusCode::NO_CONTENT)
973}
974
975async fn delete_rule(
976    State(state): State<Arc<AppState>>,
977    Path(index): Path<usize>,
978) -> Result<StatusCode, (StatusCode, String)> {
979    let snapshot = {
980        let mut raw = state.raw_config.write();
981        let rules = raw.rules.get_or_insert_with(Vec::new);
982        if index >= rules.len() {
983            return Err((StatusCode::BAD_REQUEST, "index out of range".into()));
984        }
985        rules.remove(index);
986        raw.clone()
987    };
988    apply_raw_to_tunnel(snapshot, &state.tunnel).await?;
989    Ok(StatusCode::NO_CONTENT)
990}
991
992#[derive(Deserialize)]
993struct ReorderRulesRequest {
994    from: usize,
995    to: usize,
996}
997
998async fn reorder_rules(
999    State(state): State<Arc<AppState>>,
1000    Json(body): Json<ReorderRulesRequest>,
1001) -> Result<StatusCode, (StatusCode, String)> {
1002    let snapshot = {
1003        let mut raw = state.raw_config.write();
1004        let rules = raw.rules.get_or_insert_with(Vec::new);
1005        if body.from >= rules.len() || body.to >= rules.len() {
1006            return Err((StatusCode::BAD_REQUEST, "index out of range".into()));
1007        }
1008        let rule = rules.remove(body.from);
1009        rules.insert(body.to, rule);
1010        raw.clone()
1011    };
1012    apply_raw_to_tunnel(snapshot, &state.tunnel).await?;
1013    Ok(StatusCode::NO_CONTENT)
1014}
1015
1016// ── Delay probe endpoints ────────────────────────────────────────────
1017//
1018// Matches upstream mihomo `hub/route/proxies.go::getProxyDelay` and
1019// `hub/route/groups.go::getGroupDelay`. Error bodies are byte-exact copies
1020// of upstream's `ErrBadRequest` / `ErrNotFound` / `ErrRequestTimeout` /
1021// `newError("An error occurred in the delay test")`.
1022
1023#[derive(Deserialize)]
1024struct DelayParams {
1025    url: Option<String>,
1026    timeout: Option<String>,
1027    expected: Option<String>,
1028}
1029
1030#[derive(Serialize)]
1031struct DelayResp {
1032    delay: u16,
1033}
1034
1035/// `{"message": "..."}` body matching upstream's error render.
1036fn msg_err(status: StatusCode, message: &'static str) -> Response {
1037    (status, Json(serde_json::json!({ "message": message }))).into_response()
1038}
1039
1040/// Validate `url` and `timeout`. Returns `timeout` as `Duration` on success,
1041/// or the `400 Body invalid` response on any validation failure — matching
1042/// upstream's single "ErrBadRequest" shape for all parse errors.
1043fn parse_delay_params(params: &DelayParams) -> Result<Duration, Box<Response>> {
1044    // upstream: hub/route/proxies.go::getProxyDelay — url is not strictly
1045    // validated upstream, but an empty host would panic our prober.
1046    let url = params.url.as_deref().unwrap_or("").trim();
1047    if url.is_empty() {
1048        return Err(Box::new(msg_err(StatusCode::BAD_REQUEST, "Body invalid")));
1049    }
1050
1051    // upstream parses `timeout` as int16 and treats parse failure as
1052    // ErrBadRequest. We reject 0 as well (a zero-budget probe is never useful).
1053    let timeout_str = params
1054        .timeout
1055        .as_deref()
1056        .ok_or_else(|| Box::new(msg_err(StatusCode::BAD_REQUEST, "Body invalid")))?;
1057    let timeout_ms: u16 = timeout_str
1058        .trim()
1059        .parse()
1060        .map_err(|_| Box::new(msg_err(StatusCode::BAD_REQUEST, "Body invalid")))?;
1061    if timeout_ms == 0 {
1062        return Err(Box::new(msg_err(StatusCode::BAD_REQUEST, "Body invalid")));
1063    }
1064    Ok(Duration::from_millis(timeout_ms as u64))
1065}
1066
1067/// Probe a single adapter and record the result into its health handle.
1068/// On success records the measured delay; on any failure records `0` so
1069/// the proxy's `last_delay` tracks the most recent outcome.
1070async fn probe_and_record(
1071    proxy: &Arc<dyn meow_common::Proxy>,
1072    url: &str,
1073    expected: Option<&str>,
1074    timeout: Duration,
1075) -> Result<u16, meow_proxy::health::UrlTestError> {
1076    meow_proxy::health::probe_and_record(proxy, url, expected, timeout).await
1077}
1078
1079async fn get_proxy_delay(
1080    State(state): State<Arc<AppState>>,
1081    Path(name): Path<String>,
1082    Query(params): Query<DelayParams>,
1083) -> Response {
1084    let timeout = match parse_delay_params(&params) {
1085        Ok(t) => t,
1086        Err(resp) => return *resp,
1087    };
1088    let url = params.url.as_deref().unwrap_or("").to_string();
1089    let expected = params.expected.clone();
1090
1091    let route = state.tunnel.route_snapshot();
1092    // upstream: hub/route/proxies.go::getProxyDelay — findProxyByName middleware
1093    let Some(proxy) = route.proxies.get(name.as_str()).cloned() else {
1094        return msg_err(StatusCode::NOT_FOUND, "resource not found");
1095    };
1096    drop(route);
1097
1098    match probe_and_record(&proxy, &url, expected.as_deref(), timeout).await {
1099        Ok(delay) => Json(DelayResp { delay }).into_response(),
1100        // upstream: `render.Status(r, http.StatusGatewayTimeout)` → 504.
1101        Err(meow_proxy::health::UrlTestError::Timeout) => {
1102            msg_err(StatusCode::GATEWAY_TIMEOUT, "Timeout")
1103        }
1104        // upstream: `newError("An error occurred in the delay test")` → 503.
1105        Err(meow_proxy::health::UrlTestError::Transport(_)) => msg_err(
1106            StatusCode::SERVICE_UNAVAILABLE,
1107            "An error occurred in the delay test",
1108        ),
1109    }
1110}
1111
1112async fn get_group_delay(
1113    State(state): State<Arc<AppState>>,
1114    Path(name): Path<String>,
1115    Query(params): Query<DelayParams>,
1116) -> Response {
1117    let timeout = match parse_delay_params(&params) {
1118        Ok(t) => t,
1119        Err(resp) => return *resp,
1120    };
1121    let url = params.url.as_deref().unwrap_or("").to_string();
1122    let expected = params.expected.clone();
1123
1124    let route = state.tunnel.route_snapshot();
1125    let Some(group) = route.proxies.get(name.as_str()).cloned() else {
1126        return msg_err(StatusCode::NOT_FOUND, "resource not found");
1127    };
1128    // upstream: findProxyByName rejects non-groups with 404 for this route.
1129    let Some(member_names) = group.members() else {
1130        return msg_err(StatusCode::NOT_FOUND, "resource not found");
1131    };
1132
1133    // Resolve each member name to an `Arc<dyn Proxy>` *before* dropping the
1134    // proxies map so the spawned tasks hold their own Arc clones.
1135    let members: Vec<(String, Arc<dyn meow_common::Proxy>)> = member_names
1136        .into_iter()
1137        .filter_map(|n| route.proxies.get(n.as_str()).cloned().map(|p| (n, p)))
1138        .collect();
1139    drop(route);
1140
1141    // upstream: group probe wraps the whole batch in one context.WithTimeout,
1142    // not per-member. A slow member does not get its own budget.
1143    let collected = tokio::time::timeout(
1144        timeout,
1145        meow_proxy::health::probe_many_bounded_detailed(
1146            members,
1147            &url,
1148            expected.as_deref(),
1149            timeout,
1150            meow_proxy::health::GROUP_DELAY_CONCURRENCY,
1151        ),
1152    )
1153    .await;
1154
1155    let Ok(pairs) = collected else {
1156        // upstream: 504 "Timeout". Even if some members completed before the
1157        // deadline, upstream still returns the timeout error — we match.
1158        return msg_err(StatusCode::GATEWAY_TIMEOUT, "Timeout");
1159    };
1160
1161    let mut result: BTreeMap<String, u16> = BTreeMap::new();
1162    for pair in pairs {
1163        if matches!(pair.error, Some(meow_proxy::health::UrlTestError::Timeout)) {
1164            return msg_err(StatusCode::GATEWAY_TIMEOUT, "Timeout");
1165        }
1166        result.insert(pair.name, pair.delay);
1167    }
1168    Json(result).into_response()
1169}
1170
1171// ── Config reload (M1.G-10) ──────────────────────────────────────────
1172// upstream: hub/server.go::patchConfig
1173// Class B per ADR-0002: payload must be base64 (upstream inconsistent); YAML parse errors
1174// always return 400 even with force=true; NOT upstream silent broken-config apply.
1175
1176#[derive(Deserialize)]
1177struct PutConfigsBody {
1178    path: Option<String>,
1179    payload: Option<String>,
1180}
1181
1182async fn put_configs(
1183    State(state): State<Arc<AppState>>,
1184    Query(params): Query<HashMap<String, String>>,
1185    Json(body): Json<PutConfigsBody>,
1186) -> Response {
1187    let force = params.get("force").is_some_and(|v| v == "true");
1188
1189    let yaml =
1190        match (body.path, body.payload) {
1191            (Some(p), _) => match tokio::fs::read_to_string(&p).await {
1192                Ok(s) => s,
1193                Err(e) => {
1194                    return (
1195                        StatusCode::BAD_REQUEST,
1196                        Json(serde_json::json!({"message": e.to_string()})),
1197                    )
1198                        .into_response()
1199                }
1200            },
1201            (_, Some(b64)) => {
1202                use base64::engine::general_purpose::STANDARD;
1203                use base64::Engine as _;
1204                let Ok(bytes) = STANDARD.decode(&b64) else {
1205                    return (
1206                        StatusCode::BAD_REQUEST,
1207                        Json(serde_json::json!({"message": "payload is not valid base64"})),
1208                    )
1209                        .into_response();
1210                };
1211                match String::from_utf8(bytes) {
1212                    Ok(s) => s,
1213                    Err(_) => {
1214                        return (
1215                            StatusCode::BAD_REQUEST,
1216                            Json(serde_json::json!({"message": "payload is not valid UTF-8"})),
1217                        )
1218                            .into_response()
1219                    }
1220                }
1221            }
1222            _ => return (
1223                StatusCode::BAD_REQUEST,
1224                Json(
1225                    serde_json::json!({"message": "request body must contain 'path' or 'payload'"}),
1226                ),
1227            )
1228                .into_response(),
1229        };
1230
1231    // YAML syntax check — always 400 even with force=true (per spec)
1232    let mut raw_config: RawConfig = match serde_yaml::from_str(&yaml) {
1233        Ok(c) => c,
1234        Err(e) => {
1235            return (
1236                StatusCode::BAD_REQUEST,
1237                Json(serde_json::json!({"message": format!("config parse error: {e}")})),
1238            )
1239                .into_response()
1240        }
1241    };
1242
1243    // Pre-resolve any DNS-sourced ECH configs into inline base64.
1244    if let Some(ps) = raw_config.proxies.as_mut() {
1245        meow_config::ech_dns::preresolve_ech(ps).await;
1246    }
1247
1248    // Semantic rebuild (proxy/rule parsing)
1249    let resolver = Arc::clone(state.tunnel.resolver());
1250    let (proxies, rules) = match rebuild_from_raw_with_resolver_async(raw_config.clone(), resolver)
1251        .await
1252    {
1253        Ok(r) => r,
1254        Err(e) => {
1255            if force {
1256                tracing::error!("config reload forced despite validation error: {e}");
1257                (Default::default(), Vec::new())
1258            } else {
1259                return (
1260                    StatusCode::BAD_REQUEST,
1261                    Json(serde_json::json!({"message": format!("config validation error: {e}")})),
1262                )
1263                    .into_response();
1264            }
1265        }
1266    };
1267
1268    // Cold reload: close all connections with structured log (Class A divergence from upstream)
1269    let stats = state.tunnel.statistics();
1270    let dropped = stats.active_connection_count();
1271    stats.close_all_connections();
1272    if dropped > 0 {
1273        tracing::warn!(
1274            connections_dropped = dropped,
1275            "connections force-closed after reload drain timeout"
1276        );
1277    }
1278
1279    state.tunnel.update_proxies(proxies);
1280    state.tunnel.update_rules(rules);
1281    if let Some(mode_str) = &raw_config.mode {
1282        if let Ok(mode) = mode_str.parse::<TunnelMode>() {
1283            state.tunnel.set_mode(mode);
1284        }
1285    }
1286    *state.raw_config.write() = raw_config;
1287
1288    StatusCode::NO_CONTENT.into_response()
1289}
1290
1291// ── Prometheus metrics (M1.H-2) ──────────────────────────────────────
1292// upstream: N/A — meow-rs enhancement; Go mihomo has no native /metrics endpoint.
1293
1294async fn get_metrics(State(state): State<Arc<AppState>>) -> Response {
1295    use prometheus_client::encoding::text::encode;
1296    use prometheus_client::metrics::counter::Counter;
1297    use prometheus_client::metrics::family::Family;
1298    use prometheus_client::metrics::gauge::Gauge;
1299    use prometheus_client::registry::Registry;
1300    use std::sync::atomic::{AtomicI64, AtomicU64};
1301
1302    let mut registry = Registry::default();
1303    let stats = state.tunnel.statistics();
1304    let (upload_total, download_total) = stats.snapshot();
1305
1306    // meow_traffic_bytes — counter{direction}
1307    let traffic = Family::<Vec<(String, String)>, Counter<u64, AtomicU64>>::default();
1308    traffic
1309        .get_or_create(&vec![("direction".to_string(), "upload".to_string())])
1310        .inc_by(upload_total.max(0) as u64);
1311    traffic
1312        .get_or_create(&vec![("direction".to_string(), "download".to_string())])
1313        .inc_by(download_total.max(0) as u64);
1314    registry.register(
1315        "meow_traffic_bytes",
1316        "Cumulative bytes transferred since process start",
1317        traffic,
1318    );
1319
1320    // meow_connections_active — gauge
1321    let connections_active = Gauge::<i64, AtomicI64>::default();
1322    connections_active.set(stats.active_connection_count() as i64);
1323    registry.register(
1324        "meow_connections_active",
1325        "Number of currently open connections",
1326        connections_active,
1327    );
1328
1329    // meow_proxy_alive and meow_proxy_delay_ms — gauge{proxy_name,adapter_type}
1330    let proxy_alive = Family::<Vec<(String, String)>, Gauge<i64, AtomicI64>>::default();
1331    let proxy_delay = Family::<Vec<(String, String)>, Gauge<i64, AtomicI64>>::default();
1332    let route = state.tunnel.route_snapshot();
1333    for (name, proxy) in &route.proxies {
1334        let labels = vec![
1335            ("proxy_name".to_string(), name.to_string()),
1336            ("adapter_type".to_string(), proxy.adapter_type().to_string()),
1337        ];
1338        proxy_alive
1339            .get_or_create(&labels)
1340            .set(if proxy.alive() { 1 } else { 0 });
1341        // Omit delay series entirely when no health check has run (empty history).
1342        // NOT -1, NOT 0 — absence is the correct Prometheus signal for "unknown".
1343        if !proxy.delay_history().is_empty() {
1344            proxy_delay
1345                .get_or_create(&labels)
1346                .set(proxy.last_delay() as i64);
1347        }
1348    }
1349    registry.register(
1350        "meow_proxy_alive",
1351        "Proxy alive status (1=alive, 0=dead)",
1352        proxy_alive,
1353    );
1354    registry.register(
1355        "meow_proxy_delay_ms",
1356        "Last measured proxy round-trip delay in milliseconds",
1357        proxy_delay,
1358    );
1359
1360    // meow_rules_matched — counter{rule_type,action}
1361    let rules_matched = Family::<Vec<(String, String)>, Counter<u64, AtomicU64>>::default();
1362    for ((rule_type, action), count) in stats.rule_match.snapshot() {
1363        rules_matched
1364            .get_or_create(&vec![
1365                ("rule_type".to_string(), rule_type.to_string()),
1366                ("action".to_string(), action.to_string()),
1367            ])
1368            .inc_by(count);
1369    }
1370    registry.register(
1371        "meow_rules_matched",
1372        "Cumulative rule matches by type and action",
1373        rules_matched,
1374    );
1375
1376    // meow_memory_rss_bytes — gauge
1377    let memory_rss = Gauge::<i64, AtomicI64>::default();
1378    memory_rss.set(read_rss_bytes().await as i64);
1379    registry.register(
1380        "meow_memory_rss_bytes",
1381        "Current process RSS in bytes",
1382        memory_rss,
1383    );
1384
1385    // meow_info — gauge{version,mode} always = 1
1386    let info = Family::<Vec<(String, String)>, Gauge<i64, AtomicI64>>::default();
1387    info.get_or_create(&vec![
1388        ("version".to_string(), env!("CARGO_PKG_VERSION").to_string()),
1389        ("mode".to_string(), state.tunnel.mode().to_string()),
1390    ])
1391    .set(1);
1392    registry.register("meow_info", "meow-rs runtime info", info);
1393
1394    let mut body = String::new();
1395    encode(&mut body, &registry).expect("prometheus text encoding is infallible");
1396    (
1397        StatusCode::OK,
1398        [(
1399            header::CONTENT_TYPE,
1400            "text/plain; version=0.0.4; charset=utf-8",
1401        )],
1402        body,
1403    )
1404        .into_response()
1405}
1406
1407// ── WebSocket: log stream ────────────────────────────────────────────
1408
1409#[derive(Deserialize)]
1410struct LogsParams {
1411    level: Option<String>,
1412}
1413
1414// upstream: hub/route/logs.go::getLogs
1415async fn get_logs(
1416    State(state): State<Arc<AppState>>,
1417    Query(params): Query<LogsParams>,
1418    ws: WebSocketUpgrade,
1419) -> Response {
1420    let level = parse_log_level(params.level.as_deref().unwrap_or("info"));
1421    let mut rx = state.log_tx.subscribe();
1422    ws.on_upgrade(move |mut socket| async move {
1423        loop {
1424            match rx.recv().await {
1425                Ok(msg) if msg.level >= level => {
1426                    let json = serde_json::to_string(&msg).unwrap_or_default();
1427                    if socket.send(Message::Text(json.into())).await.is_err() {
1428                        break;
1429                    }
1430                }
1431                Ok(_) => {}
1432                Err(broadcast::error::RecvError::Lagged(n)) => {
1433                    let lag_msg = format!("{{\"type\":\"lagged\",\"missed\":{n}}}");
1434                    if socket.send(Message::Text(lag_msg.into())).await.is_err() {
1435                        break;
1436                    }
1437                }
1438                Err(broadcast::error::RecvError::Closed) => break,
1439            }
1440        }
1441    })
1442}
1443
1444// ── WebSocket: memory stream ─────────────────────────────────────────
1445
1446// upstream: hub/route/memory.go
1447//
1448// One process-wide sampler task reads RSS + limit and serialises the JSON
1449// frame once per tick; every connected socket forwards the shared string
1450// (audit M8 — previously each socket sampled and serialised independently,
1451// per-socket per-tick). The sampler starts with the first subscriber and
1452// exits once the last socket disconnects, so an idle API server pays nothing.
1453// Model: the log websocket's single-serialisation broadcast fan-out.
1454static MEMORY_FEED: std::sync::Mutex<Option<broadcast::Sender<Arc<str>>>> =
1455    std::sync::Mutex::new(None);
1456
1457fn subscribe_memory_feed() -> broadcast::Receiver<Arc<str>> {
1458    let mut guard = MEMORY_FEED.lock().expect("memory feed lock poisoned");
1459    if let Some(tx) = guard.as_ref() {
1460        // Sampler still alive (it clears the slot under this lock on exit).
1461        return tx.subscribe();
1462    }
1463    let (tx, rx) = broadcast::channel(2);
1464    *guard = Some(tx.clone());
1465    tokio::spawn(async move {
1466        let mut interval = tokio::time::interval(Duration::from_secs(1));
1467        loop {
1468            interval.tick().await;
1469            if tx.receiver_count() == 0 {
1470                // Re-check under the lock so a subscriber arriving right now
1471                // either sees the live sender or a cleared slot — never a
1472                // sender whose sampler has already exited.
1473                let mut guard = MEMORY_FEED.lock().expect("memory feed lock poisoned");
1474                if tx.receiver_count() == 0 {
1475                    *guard = None;
1476                    break;
1477                }
1478            }
1479            let inuse = read_rss_bytes().await;
1480            let oslimit = read_os_memory_limit().await;
1481            let msg: Arc<str> = Arc::from(format!("{{\"inuse\":{inuse},\"oslimit\":{oslimit}}}"));
1482            let _ = tx.send(msg);
1483        }
1484    });
1485    rx
1486}
1487
1488async fn get_memory(State(_state): State<Arc<AppState>>, ws: WebSocketUpgrade) -> Response {
1489    ws.on_upgrade(|mut socket| async move {
1490        let mut feed = subscribe_memory_feed();
1491        loop {
1492            let msg = match feed.recv().await {
1493                Ok(msg) => msg,
1494                // Slow consumer skipped a tick — just continue with the next.
1495                Err(broadcast::error::RecvError::Lagged(_)) => continue,
1496                Err(broadcast::error::RecvError::Closed) => break,
1497            };
1498            if socket
1499                .send(Message::Text(msg.as_ref().into()))
1500                .await
1501                .is_err()
1502            {
1503                break;
1504            }
1505        }
1506    })
1507}
1508
1509async fn read_rss_bytes() -> u64 {
1510    tokio::task::spawn_blocking(|| {
1511        use sysinfo::{Pid, ProcessesToUpdate, System};
1512        let pid = Pid::from_u32(std::process::id());
1513        let mut sys = System::new();
1514        sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), false);
1515        sys.process(pid).map_or(0, sysinfo::Process::memory)
1516    })
1517    .await
1518    .unwrap_or(0)
1519}
1520
1521async fn read_os_memory_limit() -> u64 {
1522    #[cfg(target_os = "linux")]
1523    {
1524        read_os_memory_limit_linux().await
1525    }
1526    #[cfg(not(target_os = "linux"))]
1527    {
1528        0
1529    }
1530}
1531
1532#[cfg(target_os = "linux")]
1533async fn read_os_memory_limit_linux() -> u64 {
1534    // Try cgroup v2 memory limit first, fall back to rlimit.
1535    if let Ok(s) = tokio::fs::read_to_string("/sys/fs/cgroup/memory.max").await {
1536        if let Ok(n) = s.trim().parse::<u64>() {
1537            return n;
1538        }
1539    }
1540    // rlimit RLIMIT_AS (virtual address space) as a proxy; RLIMIT_RSS is deprecated.
1541    unsafe {
1542        let mut rl = libc::rlimit {
1543            rlim_cur: 0,
1544            rlim_max: 0,
1545        };
1546        if libc::getrlimit(libc::RLIMIT_AS, &mut rl) == 0 && rl.rlim_cur != libc::RLIM_INFINITY {
1547            return rl.rlim_cur;
1548        }
1549    }
1550    0
1551}
1552
1553// ── Proxy providers ───────────────────────────────────────────────────
1554
1555#[derive(Serialize)]
1556#[serde(rename_all = "camelCase")]
1557struct ProviderInfo {
1558    name: String,
1559    #[serde(rename = "type")]
1560    provider_type: String,
1561    vehicle_type: String,
1562    proxies: Vec<ProxyInfo>,
1563}
1564
1565fn provider_to_info(name: &str, provider: &ProxyProvider) -> ProviderInfo {
1566    let proxies = provider
1567        .proxies()
1568        .iter()
1569        .map(ProxyInfo::from_proxy)
1570        .collect();
1571    ProviderInfo {
1572        name: name.to_string(),
1573        provider_type: "Proxy".to_string(),
1574        vehicle_type: provider.vehicle_type.to_string(),
1575        proxies,
1576    }
1577}
1578
1579async fn get_providers(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
1580    let mut map = serde_json::Map::new();
1581    for entry in state.proxy_providers.iter() {
1582        let info = provider_to_info(entry.key(), entry.value());
1583        map.insert(
1584            entry.key().clone(),
1585            serde_json::to_value(info).unwrap_or_default(),
1586        );
1587    }
1588    Json(serde_json::json!({ "providers": map }))
1589}
1590
1591async fn get_provider(State(state): State<Arc<AppState>>, Path(name): Path<String>) -> Response {
1592    match state.proxy_providers.get(&name) {
1593        Some(entry) => Json(provider_to_info(&name, entry.value())).into_response(),
1594        None => msg_err(StatusCode::NOT_FOUND, "resource not found"),
1595    }
1596}
1597
1598async fn refresh_provider(
1599    State(state): State<Arc<AppState>>,
1600    Path(name): Path<String>,
1601) -> Response {
1602    let provider = match state.proxy_providers.get(&name) {
1603        Some(entry) => Arc::clone(entry.value()),
1604        None => return msg_err(StatusCode::NOT_FOUND, "resource not found"),
1605    };
1606    provider.refresh().await;
1607    StatusCode::NO_CONTENT.into_response()
1608}
1609
1610/// Trigger a health check for all proxies in the named provider.
1611/// Accepts the same `url` and `timeout` query params as `GET /proxies/:name/delay`.
1612async fn provider_healthcheck(
1613    State(state): State<Arc<AppState>>,
1614    Path(name): Path<String>,
1615    Query(params): Query<DelayParams>,
1616) -> Response {
1617    let timeout = match parse_delay_params(&params) {
1618        Ok(t) => t,
1619        Err(resp) => return *resp,
1620    };
1621    let url = params.url.as_deref().unwrap_or("").to_string();
1622    let expected = params.expected.clone();
1623
1624    let provider = match state.proxy_providers.get(&name) {
1625        Some(entry) => Arc::clone(entry.value()),
1626        None => return msg_err(StatusCode::NOT_FOUND, "resource not found"),
1627    };
1628
1629    let members = provider
1630        .proxies()
1631        .into_iter()
1632        .map(|proxy| (proxy.name().to_string(), proxy))
1633        .collect();
1634
1635    let mut results = serde_json::Map::new();
1636    for (pname, delay) in meow_proxy::health::probe_many_bounded(
1637        members,
1638        &url,
1639        expected.as_deref(),
1640        timeout,
1641        meow_proxy::health::PROVIDER_HEALTHCHECK_CONCURRENCY,
1642    )
1643    .await
1644    {
1645        results.insert(pname, serde_json::Value::Number(delay.into()));
1646    }
1647
1648    Json(serde_json::Value::Object(results)).into_response()
1649}
1650
1651// ── Rule Providers ────────────────────────────────────────────────────
1652
1653#[derive(Serialize)]
1654struct RuleProviderInfo {
1655    name: String,
1656    #[serde(rename = "type")]
1657    provider_type: String,
1658    behavior: String,
1659    #[serde(rename = "ruleCount")]
1660    rule_count: usize,
1661    #[serde(rename = "updatedAt")]
1662    updated_at: u64,
1663    #[serde(rename = "vehicleType")]
1664    vehicle_type: String,
1665}
1666
1667impl RuleProviderInfo {
1668    fn from_provider(p: &Arc<RuleProvider>) -> Self {
1669        Self {
1670            name: p.name.clone(),
1671            provider_type: p.provider_type.to_string(),
1672            behavior: p.behavior.to_string(),
1673            rule_count: p.rule_count(),
1674            updated_at: p.updated_at_secs(),
1675            vehicle_type: p.vehicle.clone(),
1676        }
1677    }
1678}
1679
1680#[derive(Serialize)]
1681struct RuleProvidersResponse {
1682    providers: HashMap<String, RuleProviderInfo>,
1683}
1684
1685async fn get_rule_providers(State(state): State<Arc<AppState>>) -> Json<RuleProvidersResponse> {
1686    let providers = state.rule_providers.read();
1687    let map: HashMap<String, RuleProviderInfo> = providers
1688        .iter()
1689        .map(|(name, p): (&String, &Arc<RuleProvider>)| {
1690            (name.clone(), RuleProviderInfo::from_provider(p))
1691        })
1692        .collect();
1693    Json(RuleProvidersResponse { providers: map })
1694}
1695
1696async fn get_rule_provider(
1697    State(state): State<Arc<AppState>>,
1698    Path(name): Path<String>,
1699) -> Result<Json<RuleProviderInfo>, StatusCode> {
1700    let providers = state.rule_providers.read();
1701    let p = providers.get(&name).ok_or(StatusCode::NOT_FOUND)?;
1702    Ok(Json(RuleProviderInfo::from_provider(p)))
1703}
1704
1705async fn refresh_rule_provider(
1706    State(state): State<Arc<AppState>>,
1707    Path(name): Path<String>,
1708) -> StatusCode {
1709    let provider = {
1710        let providers = state.rule_providers.read();
1711        providers.get(&name).cloned()
1712    };
1713    let Some(p) = provider else {
1714        return StatusCode::NOT_FOUND;
1715    };
1716    let ctx = meow_rules::ParserContext::empty();
1717    match p.refresh(&ctx).await {
1718        Ok(()) => StatusCode::NO_CONTENT,
1719        Err(e) => {
1720            tracing::warn!(provider = %name, "rule-provider refresh failed: {:#}", e);
1721            StatusCode::SERVICE_UNAVAILABLE
1722        }
1723    }
1724}
1725
1726// ── Listeners ─────────────────────────────────────────────────────────
1727
1728async fn get_listeners(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
1729    let items: Vec<serde_json::Value> = state
1730        .listeners
1731        .iter()
1732        .map(|l| {
1733            serde_json::json!({
1734                "name": l.name,
1735                "type": l.listener_type.to_string(),
1736                "port": l.port,
1737                "listen": l.listen,
1738            })
1739        })
1740        .collect();
1741    Json(serde_json::json!(items))
1742}