Skip to main content

meow_api/
routes.rs

1use axum::{
2    body::Body,
3    extract::ws::{Message, WebSocketUpgrade},
4    extract::{FromRequestParts, Path, Query, Request, State},
5    http::{header, request::Parts, StatusCode},
6    middleware::{self, Next},
7    response::{IntoResponse, Json, Response},
8    routing::{delete, get, post, put},
9    Router,
10};
11use dashmap::DashMap;
12use meow_common::TunnelMode;
13use meow_config::{
14    proxy_provider::ProxyProvider,
15    raw::{RawConfig, RawProxyGroup, RawSubscription},
16    rule_provider::RuleProvider,
17    NamedListener,
18};
19use meow_tunnel::Tunnel;
20use parking_lot::RwLock;
21use serde::{Deserialize, Serialize};
22use std::collections::{BTreeMap, HashMap};
23use std::sync::Arc;
24use std::time::Duration;
25use tokio::sync::{broadcast, Mutex};
26use tower_http::cors::CorsLayer;
27use tracing::{debug, info, warn};
28
29#[cfg(feature = "listener-tun")]
30use meow_listener::TunListener;
31
32use crate::log_stream::{parse_log_level, LogMessage};
33use crate::ui;
34
35struct MaybeWebSocket(Option<WebSocketUpgrade>);
36
37impl<S> FromRequestParts<S> for MaybeWebSocket
38where
39    S: Send + Sync,
40{
41    type Rejection = std::convert::Infallible;
42
43    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
44        let is_websocket = parts
45            .headers
46            .get(header::UPGRADE)
47            .and_then(|v| v.to_str().ok())
48            .is_some_and(|v| v.eq_ignore_ascii_case("websocket"));
49        if !is_websocket {
50            return Ok(Self(None));
51        }
52        Ok(Self(
53            WebSocketUpgrade::from_request_parts(parts, state)
54                .await
55                .ok(),
56        ))
57    }
58}
59
60pub struct AppState {
61    pub tunnel: Tunnel,
62    /// Optional Bearer token enforced by `require_auth`. `None` or empty disables auth.
63    pub secret: Option<String>,
64    pub config_path: String,
65    pub raw_config: Arc<RwLock<RawConfig>>,
66    /// Fan-out channel for log events. Each WS client subscribes a Receiver.
67    pub log_tx: broadcast::Sender<LogMessage>,
68    /// Live proxy-provider registry — refreshed by background task and PUT endpoint.
69    pub proxy_providers: Arc<DashMap<String, Arc<ProxyProvider>>>,
70    pub rule_providers: Arc<RwLock<HashMap<String, Arc<RuleProvider>>>>,
71    /// Snapshot of active named listeners (read-only, startup-time only in M1).
72    pub listeners: Vec<NamedListener>,
73    /// Validated directory for a third-party web UI. When `Some`, it is served
74    /// at `/ui`; when `None`, the built-in panel is served (issue #223).
75    pub external_ui: Option<std::path::PathBuf>,
76    /// Serialises `put_configs` and `commit_raw_candidate` so that the
77    /// "read-old-config → write-new-config → TUN reconcile" sequence is
78    /// executed atomically with respect to other config mutations.  Without
79    /// this a concurrent PUT could stop_tun before a sibling's
80    /// set_tun_handle has completed, leaving a running TUN device behind an
81    /// `enable=false` config.
82    pub config_mutation_lock: tokio::sync::Mutex<()>,
83}
84
85/// The API server owns one raw/runtime configuration, so all mutation
86/// endpoints share one commit lane. Reads remain independent.
87static CONFIG_MUTATION: Mutex<()> = Mutex::const_new(());
88
89impl AppState {
90    fn auth_required(&self) -> bool {
91        self.secret.as_deref().is_some_and(|s| !s.is_empty())
92    }
93}
94
95/// Auth middleware for all API routes. Accepts `Authorization: Bearer <secret>`
96/// header. For WebSocket upgrade requests, also accepts `?token=<secret>` query
97/// param (browser WebSocket clients cannot set custom headers).
98async fn require_auth_ws(
99    State(state): State<Arc<AppState>>,
100    Query(query): Query<HashMap<String, String>>,
101    req: Request,
102    next: Next,
103) -> Response {
104    if !state.auth_required() {
105        return next.run(req).await;
106    }
107    let expected = state.secret.as_deref().unwrap_or("");
108
109    let bearer = req
110        .headers()
111        .get(header::AUTHORIZATION)
112        .and_then(|v| v.to_str().ok())
113        .and_then(|v| v.strip_prefix("Bearer "));
114
115    let is_websocket = req
116        .headers()
117        .get(header::UPGRADE)
118        .and_then(|v| v.to_str().ok())
119        .is_some_and(|v| v.eq_ignore_ascii_case("websocket"));
120    let token_param = if is_websocket {
121        query.get("token").map(std::string::String::as_str)
122    } else {
123        None
124    };
125    let provided = bearer.or(token_param);
126
127    let ok = match provided {
128        Some(t) if t.len() == expected.len() => {
129            use subtle::ConstantTimeEq;
130            t.as_bytes().ct_eq(expected.as_bytes()).into()
131        }
132        _ => false,
133    };
134    if ok {
135        next.run(req).await
136    } else {
137        (
138            StatusCode::UNAUTHORIZED,
139            Json(serde_json::json!({"message": "Unauthorized"})),
140        )
141            .into_response()
142    }
143}
144
145pub fn create_router(state: Arc<AppState>) -> Router {
146    // WS routes — accept header or ?token= query param for browser dashboard compat.
147    // REST API routes gated behind the Bearer middleware (header-only).
148    let api = Router::new()
149        .route("/", get(hello))
150        .route("/version", get(version))
151        .route("/proxies", get(get_proxies))
152        .route(
153            "/proxies/{name}",
154            get(get_proxy).put(update_proxy).delete(unfix_proxy),
155        )
156        .route("/proxies/{name}/delay", get(get_proxy_delay))
157        .route("/group", get(get_groups))
158        .route("/group/{name}", get(get_group))
159        .route("/group/{name}/delay", get(get_group_delay))
160        .route(
161            "/rules",
162            get(get_rules).post(replace_rules).put(update_rule_at_index),
163        )
164        .route("/rules/{index}", delete(delete_rule))
165        .route("/rules/reorder", post(reorder_rules))
166        .route("/connections", get(get_connections))
167        .route("/connections/{id}", delete(close_connection))
168        .route("/connections", delete(close_all_connections))
169        .route(
170            "/configs",
171            get(get_configs).patch(update_configs).put(put_configs),
172        )
173        .route("/metrics", get(get_metrics))
174        .route("/traffic", get(get_traffic))
175        .route("/logs", get(get_logs))
176        .route("/memory", get(get_memory))
177        .route("/dns/results", get(get_dns_results))
178        .route("/dns/query", get(dns_query_get).post(dns_query))
179        .route("/cache/dns/flush", post(flush_dns_cache))
180        .route("/cache/fakeip/flush", post(flush_fakeip_cache))
181        // Config save
182        .route("/api/config/save", post(save_config))
183        // Subscriptions
184        .route(
185            "/api/subscriptions",
186            get(get_subscriptions).post(add_subscription),
187        )
188        .route("/api/subscriptions/{name}", delete(delete_subscription))
189        .route(
190            "/api/subscriptions/{name}/refresh",
191            post(refresh_subscription),
192        )
193        // Proxy groups
194        .route(
195            "/api/proxy-groups",
196            get(get_proxy_groups).post(create_proxy_group),
197        )
198        .route(
199            "/api/proxy-groups/{name}",
200            put(update_proxy_group).delete(delete_proxy_group),
201        )
202        .route(
203            "/api/proxy-groups/{name}/select",
204            put(select_proxy_in_group),
205        )
206        // Proxy providers
207        .route("/providers/proxies", get(get_providers))
208        .route(
209            "/providers/proxies/{name}",
210            get(get_provider).put(refresh_provider),
211        )
212        .route(
213            "/providers/proxies/{name}/healthcheck",
214            get(provider_healthcheck),
215        )
216        .route(
217            "/providers/proxies/{provider_name}/{proxy_name}",
218            get(get_provider_proxy),
219        )
220        .route(
221            "/providers/proxies/{provider_name}/{proxy_name}/healthcheck",
222            get(provider_proxy_healthcheck),
223        )
224        // Rule providers
225        .route("/providers/rules", get(get_rule_providers))
226        .route(
227            "/providers/rules/{name}",
228            get(get_rule_provider).put(refresh_rule_provider),
229        )
230        // Listeners (read-only list)
231        .route("/listeners", get(get_listeners))
232        .route_layer(middleware::from_fn_with_state(
233            Arc::clone(&state),
234            require_auth_ws,
235        ));
236
237    // Web UI is intentionally unauthenticated so dashboards can load and then
238    // present a token prompt; this matches upstream mihomo behaviour.
239    //
240    // When `external-ui` is configured (issue #223) the static directory is
241    // served at `/ui` via tower-http's `ServeDir`; otherwise the built-in
242    // single-page panel is served.
243    let router = api;
244    let router = if let Some(dir) = state.external_ui.clone() {
245        // `ServeDir` resolves `index.html` for the directory root and serves
246        // any nested asset; `nest_service("/ui", …)` strips the `/ui` prefix so
247        // both `/ui` and `/ui/<asset>` resolve. Dashboards (metacubexd, yacd)
248        // use hash routing, so no server-side SPA fallback is required.
249        router.nest_service("/ui", tower_http::services::ServeDir::new(dir))
250    } else {
251        router
252            .route("/ui", get(ui::serve_ui))
253            .route("/ui/{*rest}", get(ui::serve_ui))
254    };
255
256    router.layer(CorsLayer::permissive()).with_state(state)
257}
258
259// ── Basic endpoints ──────────────────────────────────────────────────
260
261#[derive(Serialize)]
262struct HelloResponse {
263    hello: &'static str,
264}
265
266async fn hello() -> Json<HelloResponse> {
267    Json(HelloResponse { hello: "meow" })
268}
269
270#[derive(Serialize)]
271struct VersionResponse {
272    version: String,
273    meta: bool,
274}
275
276async fn version() -> Json<VersionResponse> {
277    Json(VersionResponse {
278        version: format!("v{}", env!("CARGO_PKG_VERSION")),
279        meta: true,
280    })
281}
282
283#[derive(Serialize)]
284struct ProxyInfo {
285    name: String,
286    #[serde(rename = "type")]
287    proxy_type: String,
288    alive: bool,
289    history: Vec<meow_common::DelayHistory>,
290    udp: bool,
291    /// Group-only: ordered list of member proxy names.
292    #[serde(skip_serializing_if = "Option::is_none")]
293    all: Option<Vec<String>>,
294    /// Group-only: name of the currently active member.
295    #[serde(skip_serializing_if = "Option::is_none")]
296    now: Option<String>,
297    /// Automatic-group user pin. `Some("")` means automatic mode.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    fixed: Option<String>,
300    #[serde(rename = "testUrl", skip_serializing_if = "Option::is_none")]
301    test_url: Option<String>,
302    #[serde(rename = "expectedStatus", skip_serializing_if = "Option::is_none")]
303    expected_status: Option<String>,
304    /// Last measured delay in ms; omitted until a probe has succeeded.
305    #[serde(skip_serializing_if = "Option::is_none")]
306    delay: Option<u16>,
307}
308
309impl ProxyInfo {
310    fn from_proxy(proxy: &Arc<dyn meow_common::Proxy>) -> Self {
311        let members = proxy.members();
312        let current = proxy.current();
313        debug!(
314            name = proxy.name(),
315            proxy_type = %proxy.adapter_type(),
316            member_count = members.as_ref().map(std::vec::Vec::len),
317            current = ?current,
318            "building ProxyInfo",
319        );
320        let delay = Some(proxy.last_delay()).filter(|&d| d > 0);
321        Self {
322            name: proxy.name().to_string(),
323            proxy_type: proxy.adapter_type().to_string(),
324            alive: proxy.alive(),
325            history: proxy.delay_history(),
326            udp: proxy.support_udp(),
327            all: members,
328            now: current,
329            fixed: proxy
330                .selection()
331                .and_then(meow_common::ProxySelection::fixed),
332            test_url: proxy.test_url().map(str::to_string),
333            expected_status: proxy.expected_status().map(str::to_string),
334            delay,
335        }
336    }
337}
338
339#[derive(Serialize)]
340struct ProxiesResponse {
341    proxies: std::collections::HashMap<String, ProxyInfo>,
342}
343
344async fn get_proxies(State(state): State<Arc<AppState>>) -> Json<ProxiesResponse> {
345    let route = state.tunnel.route_snapshot();
346    let mut result = std::collections::HashMap::new();
347    for (name, proxy) in &route.proxies {
348        result.insert(name.to_string(), ProxyInfo::from_proxy(proxy));
349    }
350    Json(ProxiesResponse { proxies: result })
351}
352
353async fn get_proxy(
354    State(state): State<Arc<AppState>>,
355    Path(name): Path<String>,
356) -> Result<Json<ProxyInfo>, StatusCode> {
357    let route = state.tunnel.route_snapshot();
358    let proxy = route
359        .proxies
360        .get(name.as_str())
361        .ok_or(StatusCode::NOT_FOUND)?;
362    Ok(Json(ProxyInfo::from_proxy(proxy)))
363}
364
365#[derive(Deserialize)]
366struct UpdateProxyRequest {
367    name: String,
368}
369
370async fn update_proxy(
371    State(state): State<Arc<AppState>>,
372    Path(group_name): Path<String>,
373    Json(body): Json<UpdateProxyRequest>,
374) -> Response {
375    let route = state.tunnel.route_snapshot();
376    let Some(proxy) = route.proxies.get(group_name.as_str()).cloned() else {
377        return msg_err(StatusCode::NOT_FOUND, "Resource not found");
378    };
379    let Some(selection) = proxy.selection() else {
380        return msg_err(StatusCode::BAD_REQUEST, "Must be a Selector");
381    };
382    match selection.set(&body.name).await {
383        Ok(()) => {
384            info!("Proxy group '{}' switched to '{}'", group_name, body.name);
385            StatusCode::NO_CONTENT.into_response()
386        }
387        Err(e) => (
388            StatusCode::BAD_REQUEST,
389            Json(serde_json::json!({"message": format!("Selector update error: {e}")})),
390        )
391            .into_response(),
392    }
393}
394
395async fn unfix_proxy(
396    State(state): State<Arc<AppState>>,
397    Path(group_name): Path<String>,
398) -> Response {
399    let route = state.tunnel.route_snapshot();
400    let Some(proxy) = route.proxies.get(group_name.as_str()).cloned() else {
401        return msg_err(StatusCode::NOT_FOUND, "Resource not found");
402    };
403    let Some(selection) = proxy.selection() else {
404        return msg_err(StatusCode::BAD_REQUEST, "Body invalid");
405    };
406    if !selection.can_unfix() {
407        return msg_err(StatusCode::BAD_REQUEST, "Body invalid");
408    }
409    selection.force_set(None);
410    StatusCode::NO_CONTENT.into_response()
411}
412
413async fn get_groups(State(state): State<Arc<AppState>>) -> Json<ProxiesResponse> {
414    let route = state.tunnel.route_snapshot();
415    let proxies = route
416        .proxies
417        .iter()
418        .filter(|(_, proxy)| proxy.members().is_some())
419        .map(|(name, proxy)| (name.to_string(), ProxyInfo::from_proxy(proxy)))
420        .collect();
421    Json(ProxiesResponse { proxies })
422}
423
424async fn get_group(State(state): State<Arc<AppState>>, Path(name): Path<String>) -> Response {
425    let route = state.tunnel.route_snapshot();
426    match route.proxies.get(name.as_str()) {
427        Some(proxy) if proxy.members().is_some() => {
428            Json(ProxyInfo::from_proxy(proxy)).into_response()
429        }
430        _ => msg_err(StatusCode::NOT_FOUND, "Resource not found"),
431    }
432}
433
434#[derive(Serialize)]
435struct RuleInfo<'a> {
436    index: usize,
437    #[serde(rename = "type")]
438    rule_type: &'static str,
439    payload: &'a str,
440    proxy: &'a str,
441    size: i64,
442}
443
444#[derive(Serialize)]
445struct RulesResponse<'a> {
446    rules: Vec<RuleInfo<'a>>,
447}
448
449async fn get_rules(State(state): State<Arc<AppState>>) -> Response {
450    // Serialise straight off the route snapshot — the old rules_info()
451    // accessor built 3 Strings per rule per call (audit #182).
452    let route = state.tunnel.route_snapshot();
453    let result: Vec<RuleInfo> = route
454        .rules
455        .iter()
456        .enumerate()
457        .map(|(index, r)| RuleInfo {
458            index,
459            rule_type: r.rule_type().as_str(),
460            payload: r.payload(),
461            proxy: r.adapter(),
462            size: -1,
463        })
464        .collect();
465    Json(RulesResponse { rules: result }).into_response()
466}
467
468#[derive(Serialize)]
469#[serde(rename_all = "camelCase")]
470struct ConnectionsResponse<'a> {
471    upload_total: i64,
472    download_total: i64,
473    memory: u64,
474    /// Serialised straight from the live table — no per-connection
475    /// `serde_json::Value` tree, no cloned snapshot Vec (audit M8). The
476    /// JSON shape (id/upload/download/start/chains/rule/rulePayload) comes
477    /// from `ConnectionInfo`'s `Serialize` derive.
478    connections: meow_tunnel::statistics::ActiveConnectionsView<'a>,
479}
480
481#[derive(Deserialize)]
482struct ConnectionsParams {
483    interval: Option<String>,
484}
485
486/// Floor for the `/connections` WebSocket push interval. Each tick
487/// re-serializes the whole connection table while holding DashMap shard
488/// read-guards, so a sub-100ms interval is a self-DoS lever on the API
489/// worker rather than a useful refresh rate. Out-of-range values are
490/// clamped, not rejected: `0` stays a 400 (upstream contract), anything
491/// else is a well-formed request that just asked for too much.
492const MIN_CONNECTIONS_INTERVAL_MS: u64 = 100;
493
494/// Parse the `interval` query param: `None` on `0` / non-numeric input
495/// (rendered as `400 Body invalid` by the caller), otherwise the value in
496/// milliseconds, defaulted to 1000 and clamped to
497/// [`MIN_CONNECTIONS_INTERVAL_MS`].
498fn parse_connections_interval(raw: Option<&str>) -> Option<u64> {
499    match raw {
500        Some(raw) => match raw.parse::<u64>() {
501            Ok(0) | Err(_) => None,
502            Ok(value) => Some(value.max(MIN_CONNECTIONS_INTERVAL_MS)),
503        },
504        None => Some(1000),
505    }
506}
507
508async fn connections_json(state: &AppState) -> String {
509    let stats = state.tunnel.statistics();
510    let (up, down) = stats.snapshot();
511    let memory = read_rss_bytes().await;
512    #[allow(
513        clippy::unnecessary_cast,
514        reason = "no-op on 64-bit; widens i32 on targets without 64-bit atomics"
515    )]
516    let upload = up as i64;
517    #[allow(
518        clippy::unnecessary_cast,
519        reason = "no-op on 64-bit; widens i32 on targets without 64-bit atomics"
520    )]
521    let download = down as i64;
522    serde_json::to_string(&ConnectionsResponse {
523        upload_total: upload,
524        download_total: download,
525        memory,
526        connections: stats.active_connections_view(),
527    })
528    .unwrap_or_else(|_| {
529        "{\"uploadTotal\":0,\"downloadTotal\":0,\"memory\":0,\"connections\":[]}".into()
530    })
531}
532
533async fn get_connections(
534    State(state): State<Arc<AppState>>,
535    Query(params): Query<ConnectionsParams>,
536    MaybeWebSocket(ws): MaybeWebSocket,
537) -> Response {
538    let Some(interval_ms) = parse_connections_interval(params.interval.as_deref()) else {
539        return msg_err(StatusCode::BAD_REQUEST, "Body invalid");
540    };
541
542    if let Some(ws) = ws {
543        return ws.on_upgrade(move |mut socket| async move {
544            if socket
545                .send(Message::Text(connections_json(&state).await.into()))
546                .await
547                .is_err()
548            {
549                return;
550            }
551            let mut ticker = tokio::time::interval(Duration::from_millis(interval_ms));
552            ticker.tick().await;
553            loop {
554                ticker.tick().await;
555                if socket
556                    .send(Message::Text(connections_json(&state).await.into()))
557                    .await
558                    .is_err()
559                {
560                    break;
561                }
562            }
563        });
564    }
565
566    let body = connections_json(&state).await;
567    ([(header::CONTENT_TYPE, "application/json")], body).into_response()
568}
569
570async fn close_connection(
571    State(state): State<Arc<AppState>>,
572    Path(id): Path<String>,
573) -> StatusCode {
574    match uuid::Uuid::parse_str(&id) {
575        Ok(uuid) => {
576            state.tunnel.statistics().close_connection(uuid);
577            StatusCode::NO_CONTENT
578        }
579        Err(_) => StatusCode::BAD_REQUEST,
580    }
581}
582
583#[derive(Serialize)]
584struct ConfigResponse {
585    mode: String,
586    #[serde(rename = "log-level")]
587    log_level: String,
588    #[serde(rename = "mixed-port", skip_serializing_if = "Option::is_none")]
589    mixed_port: Option<u16>,
590    #[serde(rename = "socks-port", skip_serializing_if = "Option::is_none")]
591    socks_port: Option<u16>,
592    #[serde(rename = "port", skip_serializing_if = "Option::is_none")]
593    http_port: Option<u16>,
594    #[serde(rename = "redir-port")]
595    redir_port: u16,
596    #[serde(rename = "tproxy-port")]
597    tproxy_port: u16,
598    #[serde(
599        rename = "external-controller",
600        skip_serializing_if = "Option::is_none"
601    )]
602    external_controller: Option<String>,
603    #[serde(rename = "allow-lan")]
604    allow_lan: bool,
605    #[serde(rename = "bind-address")]
606    bind_address: String,
607    #[serde(rename = "ipv6")]
608    ipv6: bool,
609    /// Whether the TUN listener is currently running (issue #326).
610    /// Mirrors `tun.enable` from the raw config and reflects actual
611    /// runtime state so nyanpasu can correctly render its toggle.
612    #[serde(rename = "tun-enable")]
613    tun_enable: bool,
614}
615
616async fn get_configs(State(state): State<Arc<AppState>>) -> Json<ConfigResponse> {
617    let raw = state.raw_config.read();
618    Json(ConfigResponse {
619        mode: state.tunnel.mode().to_string(),
620        log_level: raw.log_level.clone().unwrap_or_else(|| "info".to_string()),
621        mixed_port: raw.mixed_port,
622        socks_port: raw.socks_port,
623        http_port: raw.port,
624        redir_port: 0,
625        tproxy_port: raw.tproxy_port.unwrap_or(0),
626        external_controller: raw.external_controller.clone(),
627        allow_lan: raw.allow_lan.unwrap_or(false),
628        bind_address: raw
629            .bind_address
630            .clone()
631            .unwrap_or_else(|| "0.0.0.0".to_string()),
632        ipv6: raw.ipv6.unwrap_or(false),
633        tun_enable: state.tunnel.has_tun(),
634    })
635}
636
637#[derive(Deserialize)]
638struct UpdateConfigRequest {
639    mode: Option<String>,
640    #[serde(rename = "log-level")]
641    log_level: Option<String>,
642}
643
644async fn update_configs(
645    State(state): State<Arc<AppState>>,
646    Json(body): Json<UpdateConfigRequest>,
647) -> Response {
648    // Validate both fields first so we never partially apply on error.
649    let mode = body.mode.map(|s| s.parse::<TunnelMode>());
650    if let Some(Err(_)) = mode {
651        return msg_err(StatusCode::BAD_REQUEST, "Body invalid");
652    }
653    if let Some(ref level) = body.log_level {
654        if !matches!(
655            level.to_ascii_lowercase().as_str(),
656            "debug" | "info" | "warning" | "warn" | "error" | "silent"
657        ) {
658            return msg_err(StatusCode::BAD_REQUEST, "Body invalid");
659        }
660    }
661
662    // Both valid — apply atomically.
663    let mut raw = state.raw_config.write();
664    if let Some(Ok(parsed_mode)) = mode {
665        state.tunnel.set_mode(parsed_mode);
666        raw.mode = Some(parsed_mode.to_string());
667        info!("Mode changed to {}", parsed_mode);
668    }
669    if let Some(level) = body.log_level {
670        if let Err(e) = crate::log_stream::reload_log_level(&level) {
671            return (
672                StatusCode::INTERNAL_SERVER_ERROR,
673                Json(serde_json::json!({"message": e})),
674            )
675                .into_response();
676        }
677        raw.log_level = Some(level);
678    }
679    StatusCode::NO_CONTENT.into_response()
680}
681
682#[derive(Serialize)]
683struct TrafficResponse {
684    up: i64,
685    down: i64,
686    #[serde(rename = "upTotal")]
687    up_total: i64,
688    #[serde(rename = "downTotal")]
689    down_total: i64,
690}
691
692fn traffic_json(state: &AppState) -> String {
693    let (up, down, up_total, down_total) = state.tunnel.statistics().traffic_snapshot();
694    #[allow(
695        clippy::useless_conversion,
696        reason = "identity on 64-bit; widens i32 on targets without 64-bit atomics"
697    )]
698    serde_json::to_string(&TrafficResponse {
699        up: up.into(),
700        down: down.into(),
701        up_total: up_total.into(),
702        down_total: down_total.into(),
703    })
704    .unwrap_or_default()
705}
706
707async fn get_traffic(
708    State(state): State<Arc<AppState>>,
709    MaybeWebSocket(ws): MaybeWebSocket,
710) -> Response {
711    if let Some(ws) = ws {
712        return ws.on_upgrade(move |mut socket| async move {
713            let mut ticker = tokio::time::interval(Duration::from_secs(1));
714            ticker.tick().await;
715            loop {
716                ticker.tick().await;
717                let frame = traffic_json(&state);
718                if socket.send(Message::Text(frame.into())).await.is_err() {
719                    break;
720                }
721            }
722        });
723    }
724
725    let stream = futures::stream::unfold(state, |state| async move {
726        tokio::time::sleep(Duration::from_secs(1)).await;
727        let line = format!("{}\n", traffic_json(&state));
728        Some((Ok::<String, std::convert::Infallible>(line), state))
729    });
730    Response::builder()
731        .header(header::CONTENT_TYPE, "application/json")
732        .body(Body::from_stream(stream))
733        .expect("valid traffic stream response")
734}
735
736#[derive(Deserialize)]
737struct DnsQueryRequest {
738    name: String,
739    #[serde(rename = "type")]
740    qtype: Option<String>,
741}
742
743#[derive(Deserialize)]
744struct DnsResultsQuery {
745    search: Option<String>,
746    limit: Option<usize>,
747}
748
749#[derive(Serialize)]
750struct DnsResultEntry {
751    name: String,
752    ips: Vec<String>,
753    #[serde(skip_serializing_if = "Option::is_none")]
754    from_server: Option<String>,
755    ttl: u64,
756}
757
758async fn get_dns_results(
759    State(state): State<Arc<AppState>>,
760    Query(params): Query<DnsResultsQuery>,
761) -> Json<Vec<DnsResultEntry>> {
762    let limit = params.limit.unwrap_or(256).min(1024);
763    let results = state
764        .tunnel
765        .resolver()
766        .dns_results(params.search.as_deref(), limit)
767        .into_iter()
768        .map(|entry| DnsResultEntry {
769            name: entry.name,
770            ips: entry.ips.into_iter().map(|ip| ip.to_string()).collect(),
771            from_server: entry.source,
772            ttl: entry.ttl.as_secs(),
773        })
774        .collect();
775    Json(results)
776}
777
778async fn dns_query(
779    State(state): State<Arc<AppState>>,
780    Json(body): Json<DnsQueryRequest>,
781) -> Json<serde_json::Value> {
782    let resolver = state.tunnel.resolver();
783    let result = resolver.resolve_ip(&body.name).await;
784    let _ = body.qtype;
785    Json(serde_json::json!({ "name": body.name, "answer": result.map(|ip| ip.to_string()) }))
786}
787
788// upstream: hub/route/dns.go — GET alias added alongside existing POST.
789// Class B per ADR-0002: POST kept for back-compat; GET matches upstream's current form.
790async fn dns_query_get(
791    State(state): State<Arc<AppState>>,
792    Query(params): Query<DnsQueryRequest>,
793) -> Response {
794    let enabled = state
795        .raw_config
796        .read()
797        .dns
798        .as_ref()
799        .is_some_and(|dns| dns.enable.unwrap_or(false));
800    if !enabled {
801        return (
802            StatusCode::INTERNAL_SERVER_ERROR,
803            Json(serde_json::json!({"message": "DNS section is disabled"})),
804        )
805            .into_response();
806    }
807
808    use hickory_proto::rr::RecordType;
809    let qtype_text = params.qtype.as_deref().unwrap_or("A").to_ascii_uppercase();
810    let Ok(record_type) = qtype_text.parse::<RecordType>() else {
811        return (
812            StatusCode::BAD_REQUEST,
813            Json(serde_json::json!({"message": "invalid query type"})),
814        )
815            .into_response();
816    };
817
818    let resolver = state.tunnel.resolver();
819    let fqdn = if params.name.ends_with('.') {
820        params.name.clone()
821    } else {
822        format!("{}.", params.name)
823    };
824    let question = serde_json::json!({
825        "Name": fqdn,
826        "Qtype": u16::from(record_type),
827        "Qclass": 1,
828    });
829
830    let mut response = serde_json::Map::new();
831    response.insert("Status".into(), 0.into());
832    response.insert("Question".into(), serde_json::Value::Array(vec![question]));
833    response.insert("TC".into(), false.into());
834    response.insert("RD".into(), true.into());
835    response.insert("RA".into(), true.into());
836    response.insert("AD".into(), false.into());
837    response.insert("CD".into(), false.into());
838
839    if matches!(record_type, RecordType::A | RecordType::AAAA) {
840        let ips = resolver.resolve_ips(&params.name).await.unwrap_or_default();
841        let answers: Vec<_> = ips
842            .into_iter()
843            .filter(|ip| {
844                matches!(record_type, RecordType::A) && ip.is_ipv4()
845                    || matches!(record_type, RecordType::AAAA) && ip.is_ipv6()
846            })
847            .map(|ip| {
848                serde_json::json!({
849                    "name": fqdn,
850                    "type": u16::from(record_type),
851                    "TTL": 60,
852                    "data": ip.to_string(),
853                })
854            })
855            .collect();
856        if !answers.is_empty() {
857            response.insert("Answer".into(), serde_json::Value::Array(answers));
858        }
859    } else if let Some(message) = resolver.forward_generic(&params.name, record_type).await {
860        let metadata = &message.metadata;
861        response.insert("Status".into(), u16::from(metadata.response_code).into());
862        response.insert("TC".into(), metadata.truncation.into());
863        response.insert("RD".into(), metadata.recursion_desired.into());
864        response.insert("RA".into(), metadata.recursion_available.into());
865        response.insert("AD".into(), metadata.authentic_data.into());
866        response.insert("CD".into(), metadata.checking_disabled.into());
867        insert_dns_records(&mut response, "Answer", &message.answers);
868        insert_dns_records(&mut response, "Authority", &message.authorities);
869        insert_dns_records(&mut response, "Additional", &message.additionals);
870    } else {
871        return (
872            StatusCode::INTERNAL_SERVER_ERROR,
873            Json(serde_json::json!({"message": "DNS query failed"})),
874        )
875            .into_response();
876    }
877
878    Json(serde_json::Value::Object(response)).into_response()
879}
880
881fn insert_dns_records(
882    target: &mut serde_json::Map<String, serde_json::Value>,
883    key: &str,
884    records: &[hickory_proto::rr::Record],
885) {
886    if records.is_empty() {
887        return;
888    }
889    target.insert(
890        key.to_string(),
891        serde_json::Value::Array(
892            records
893                .iter()
894                .map(|record| {
895                    serde_json::json!({
896                        "name": record.name.to_string(),
897                        "type": u16::from(record.record_type()),
898                        "TTL": record.ttl,
899                        "data": record.data.to_string(),
900                    })
901                })
902                .collect(),
903        ),
904    );
905}
906
907async fn flush_dns_cache(State(state): State<Arc<AppState>>) -> StatusCode {
908    state.tunnel.resolver().clear_cache();
909    StatusCode::NO_CONTENT
910}
911
912/// `POST /cache/fakeip/flush` — clear every fake-IP allocation. Mirrors
913/// upstream `hub/route/cache.go::flushFakeIPPool`. Returns 204 on success,
914/// 400 with a JSON `{message: ...}` body if persistence flushing fails.
915async fn flush_fakeip_cache(
916    State(state): State<Arc<AppState>>,
917) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
918    match state.tunnel.resolver().flush_fake_ip() {
919        Ok(()) => Ok(StatusCode::NO_CONTENT),
920        Err(e) => Err((
921            StatusCode::BAD_REQUEST,
922            Json(serde_json::json!({ "message": e.to_string() })),
923        )),
924    }
925}
926
927async fn close_all_connections(State(state): State<Arc<AppState>>) -> StatusCode {
928    state.tunnel.statistics().close_all_connections();
929    StatusCode::NO_CONTENT
930}
931
932// ── Config save ──────────────────────────────────────────────────────
933
934async fn save_config(
935    State(state): State<Arc<AppState>>,
936) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
937    let raw = state.raw_config.read().clone();
938    meow_config::save_raw_config_async(&state.config_path, &raw)
939        .await
940        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
941    Ok(Json(serde_json::json!({"message": "config saved"})))
942}
943
944// ── Helper: rebuild proxies/rules from raw and apply to tunnel ───────
945
946/// Pre-resolve DNS-sourced ECH then rebuild proxies/rules from `raw` and
947/// apply to the live tunnel. Takes the config *by value* so callers
948/// clone-and-drop their `parking_lot` guard before awaiting — those guards
949/// are not Send and would otherwise break the axum Handler bound.
950async fn apply_raw_to_tunnel(
951    mut raw: RawConfig,
952    state: &AppState,
953) -> Result<(), (StatusCode, String)> {
954    let expected_groups: Vec<String> = raw
955        .proxy_groups
956        .as_deref()
957        .unwrap_or_default()
958        .iter()
959        .map(|group| group.name.clone())
960        .collect();
961    if let Some(ps) = raw.proxies.as_mut() {
962        meow_config::ech_dns::preresolve_ech(ps).await;
963    }
964    let providers = state
965        .proxy_providers
966        .iter()
967        .map(|entry| (entry.key().clone(), Arc::clone(entry.value())))
968        .collect();
969    let (proxies, rules) =
970        rebuild_from_raw_with_resolver_async(raw, Arc::clone(state.tunnel.resolver()), providers)
971            .await
972            .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
973    if let Some(missing) = expected_groups
974        .iter()
975        .find(|name| !proxies.contains_key(name.as_str()))
976    {
977        return Err((
978            StatusCode::BAD_REQUEST,
979            format!("proxy group '{missing}' failed validation"),
980        ));
981    }
982    state.tunnel.update_proxies(proxies);
983    state.tunnel.update_rules(rules);
984    Ok(())
985}
986
987async fn commit_raw_candidate(
988    state: &AppState,
989    candidate: RawConfig,
990) -> Result<(), (StatusCode, String)> {
991    apply_raw_to_tunnel(candidate.clone(), state).await?;
992    swap_config_and_reconcile_tun(state, candidate).await;
993    Ok(())
994}
995
996async fn rebuild_from_raw_with_resolver_async(
997    raw: RawConfig,
998    resolver: Arc<meow_dns::Resolver>,
999    providers: HashMap<String, Arc<ProxyProvider>>,
1000) -> Result<meow_config::RebuildResult, String> {
1001    tokio::task::spawn_blocking(move || {
1002        meow_config::rebuild_from_raw_runtime(&raw, Some(resolver), &providers)
1003    })
1004    .await
1005    .map_err(|e| format!("config rebuild task failed: {e}"))?
1006    .map_err(|e| e.to_string())
1007}
1008
1009// ── Subscriptions ────────────────────────────────────────────────────
1010// Subscriptions replace local proxies/groups/rules with the remote data as-is.
1011
1012#[derive(Serialize)]
1013struct SubscriptionInfo {
1014    name: String,
1015    url: String,
1016    interval: Option<u64>,
1017    last_updated: Option<i64>,
1018    proxy_count: usize,
1019    group_count: usize,
1020    rule_count: usize,
1021}
1022
1023async fn get_subscriptions(State(state): State<Arc<AppState>>) -> Json<Vec<SubscriptionInfo>> {
1024    let raw = state.raw_config.read();
1025    let subs = raw.subscriptions.as_deref().unwrap_or(&[]);
1026    let result: Vec<SubscriptionInfo> = subs
1027        .iter()
1028        .map(|s| SubscriptionInfo {
1029            name: s.name.clone(),
1030            url: s.url.clone(),
1031            interval: s.interval,
1032            last_updated: s.last_updated,
1033            proxy_count: raw.proxies.as_ref().map_or(0, std::vec::Vec::len),
1034            group_count: raw.proxy_groups.as_ref().map_or(0, std::vec::Vec::len),
1035            rule_count: raw.rules.as_ref().map_or(0, std::vec::Vec::len),
1036        })
1037        .collect();
1038    Json(result)
1039}
1040
1041#[derive(Deserialize)]
1042struct AddSubscriptionRequest {
1043    name: String,
1044    url: String,
1045    interval: Option<u64>,
1046}
1047
1048async fn add_subscription(
1049    State(state): State<Arc<AppState>>,
1050    Json(body): Json<AddSubscriptionRequest>,
1051) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
1052    let fetched = meow_config::subscription::fetch_subscription(&body.url)
1053        .await
1054        .map_err(|e| (StatusCode::BAD_REQUEST, format!("fetch failed: {e}")))?;
1055
1056    let now = std::time::SystemTime::now()
1057        .duration_since(std::time::UNIX_EPOCH)
1058        .unwrap_or_default()
1059        .as_secs() as i64;
1060
1061    let pc = fetched.proxies.len();
1062    let gc = fetched.proxy_groups.len();
1063    let rc = fetched.rules.len();
1064
1065    let _mutation = CONFIG_MUTATION.lock().await;
1066    let snapshot = {
1067        let mut raw = state.raw_config.read().clone();
1068
1069        if let Some(ref subs) = raw.subscriptions {
1070            if subs.iter().any(|s| s.name == body.name) {
1071                return Err((
1072                    StatusCode::CONFLICT,
1073                    "subscription name already exists".into(),
1074                ));
1075            }
1076        }
1077
1078        let sub = RawSubscription {
1079            name: body.name.clone(),
1080            url: body.url.clone(),
1081            interval: body.interval,
1082            last_updated: Some(now),
1083        };
1084        raw.subscriptions.get_or_insert_with(Vec::new).push(sub);
1085
1086        // Replace proxies, groups, and rules with remote data as-is
1087        raw.proxies = Some(fetched.proxies);
1088        raw.proxy_groups = Some(fetched.proxy_groups);
1089        raw.rules = Some(fetched.rules);
1090
1091        raw
1092    };
1093    commit_raw_candidate(&state, snapshot.clone()).await?;
1094
1095    // Auto-save so subscription data is cached on disk
1096    meow_config::save_raw_config_async(&state.config_path, &snapshot)
1097        .await
1098        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
1099
1100    Ok(Json(serde_json::json!({
1101        "message": "subscription added",
1102        "proxy_count": pc, "group_count": gc, "rule_count": rc
1103    })))
1104}
1105
1106async fn delete_subscription(
1107    State(state): State<Arc<AppState>>,
1108    Path(name): Path<String>,
1109) -> Result<StatusCode, (StatusCode, String)> {
1110    let _mutation = CONFIG_MUTATION.lock().await;
1111    let snapshot = {
1112        let mut raw = state.raw_config.read().clone();
1113
1114        if let Some(ref mut subs) = raw.subscriptions {
1115            let before = subs.len();
1116            subs.retain(|s| s.name != name);
1117            if subs.len() == before {
1118                return Err((StatusCode::NOT_FOUND, "subscription not found".into()));
1119            }
1120        } else {
1121            return Err((StatusCode::NOT_FOUND, "no subscriptions".into()));
1122        }
1123
1124        // Clear everything from the remote subscription
1125        raw.proxies = Some(Vec::new());
1126        raw.proxy_groups = Some(Vec::new());
1127        raw.rules = Some(Vec::new());
1128
1129        raw
1130    };
1131    commit_raw_candidate(&state, snapshot.clone()).await?;
1132    meow_config::save_raw_config_async(&state.config_path, &snapshot)
1133        .await
1134        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
1135    Ok(StatusCode::NO_CONTENT)
1136}
1137
1138async fn refresh_subscription(
1139    State(state): State<Arc<AppState>>,
1140    Path(name): Path<String>,
1141) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
1142    let url = {
1143        let raw = state.raw_config.read();
1144        raw.subscriptions
1145            .as_ref()
1146            .and_then(|subs| subs.iter().find(|s| s.name == name))
1147            .map(|s| s.url.clone())
1148            .ok_or_else(|| (StatusCode::NOT_FOUND, "subscription not found".into()))?
1149    };
1150
1151    let fetched = meow_config::subscription::fetch_subscription(&url)
1152        .await
1153        .map_err(|e| (StatusCode::BAD_REQUEST, format!("fetch failed: {e}")))?;
1154
1155    let now = std::time::SystemTime::now()
1156        .duration_since(std::time::UNIX_EPOCH)
1157        .unwrap_or_default()
1158        .as_secs() as i64;
1159
1160    let pc = fetched.proxies.len();
1161    let gc = fetched.proxy_groups.len();
1162    let rc = fetched.rules.len();
1163
1164    let _mutation = CONFIG_MUTATION.lock().await;
1165    let snapshot = {
1166        let mut raw = state.raw_config.read().clone();
1167
1168        if let Some(ref mut subs) = raw.subscriptions {
1169            if let Some(sub) = subs.iter_mut().find(|s| s.name == name) {
1170                sub.last_updated = Some(now);
1171            }
1172        }
1173
1174        raw.proxies = Some(fetched.proxies);
1175        raw.proxy_groups = Some(fetched.proxy_groups);
1176        raw.rules = Some(fetched.rules);
1177
1178        raw
1179    };
1180    commit_raw_candidate(&state, snapshot.clone()).await?;
1181
1182    // Auto-save so subscription data is cached on disk
1183    meow_config::save_raw_config_async(&state.config_path, &snapshot)
1184        .await
1185        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
1186
1187    Ok(Json(serde_json::json!({
1188        "message": "subscription refreshed",
1189        "proxy_count": pc, "group_count": gc, "rule_count": rc
1190    })))
1191}
1192
1193// ── Proxy Groups ─────────────────────────────────────────────────────
1194
1195#[derive(Serialize)]
1196struct ProxyGroupInfo {
1197    name: String,
1198    #[serde(rename = "type")]
1199    group_type: String,
1200    proxies: Vec<String>,
1201    now: Option<String>,
1202    url: Option<String>,
1203    interval: Option<u64>,
1204    tolerance: Option<u16>,
1205}
1206
1207async fn get_proxy_groups(State(state): State<Arc<AppState>>) -> Json<Vec<ProxyGroupInfo>> {
1208    let raw = state.raw_config.read();
1209    let groups = raw.proxy_groups.as_deref().unwrap_or(&[]);
1210    let route = state.tunnel.route_snapshot();
1211    let tunnel_proxies = &route.proxies;
1212
1213    let result: Vec<ProxyGroupInfo> = groups
1214        .iter()
1215        .map(|g| {
1216            let runtime = tunnel_proxies.get(g.name.as_str());
1217            let now = runtime.and_then(|p| p.current());
1218            let proxies = runtime
1219                .and_then(|p| p.members())
1220                .unwrap_or_else(|| g.proxies.clone().unwrap_or_default());
1221            ProxyGroupInfo {
1222                name: g.name.clone(),
1223                group_type: g.group_type.clone(),
1224                proxies,
1225                now,
1226                url: g.url.clone(),
1227                interval: g.interval,
1228                tolerance: g.tolerance,
1229            }
1230        })
1231        .collect();
1232    Json(result)
1233}
1234
1235#[derive(Deserialize)]
1236struct CreateProxyGroupRequest {
1237    name: String,
1238    #[serde(rename = "type")]
1239    group_type: String,
1240    proxies: Vec<String>,
1241    url: Option<String>,
1242    interval: Option<u64>,
1243    tolerance: Option<u16>,
1244}
1245
1246async fn create_proxy_group(
1247    State(state): State<Arc<AppState>>,
1248    Json(body): Json<CreateProxyGroupRequest>,
1249) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
1250    let group_name = body.name.clone();
1251    let _mutation = CONFIG_MUTATION.lock().await;
1252    let snapshot = {
1253        let mut raw = state.raw_config.read().clone();
1254        if let Some(ref groups) = raw.proxy_groups {
1255            if groups.iter().any(|g| g.name == body.name) {
1256                return Err((StatusCode::CONFLICT, "group name already exists".into()));
1257            }
1258        }
1259        let group = RawProxyGroup {
1260            name: body.name,
1261            group_type: body.group_type,
1262            proxies: Some(body.proxies),
1263            url: body.url,
1264            interval: body.interval,
1265            tolerance: body.tolerance,
1266            ..Default::default()
1267        };
1268        raw.proxy_groups.get_or_insert_with(Vec::new).push(group);
1269        raw
1270    };
1271    commit_raw_candidate(&state, snapshot).await?;
1272    Ok(Json(
1273        serde_json::json!({"message": "group created", "name": group_name}),
1274    ))
1275}
1276
1277async fn update_proxy_group(
1278    State(state): State<Arc<AppState>>,
1279    Path(name): Path<String>,
1280    Json(body): Json<CreateProxyGroupRequest>,
1281) -> Result<StatusCode, (StatusCode, String)> {
1282    let _mutation = CONFIG_MUTATION.lock().await;
1283    let snapshot = {
1284        let mut raw = state.raw_config.read().clone();
1285        let group = raw
1286            .proxy_groups
1287            .as_mut()
1288            .and_then(|groups| groups.iter_mut().find(|g| g.name == name))
1289            .ok_or_else(|| (StatusCode::NOT_FOUND, "group not found".into()))?;
1290        group.group_type = body.group_type;
1291        group.proxies = Some(body.proxies);
1292        group.url = body.url;
1293        group.interval = body.interval;
1294        group.tolerance = body.tolerance;
1295        raw
1296    };
1297    commit_raw_candidate(&state, snapshot).await?;
1298    Ok(StatusCode::NO_CONTENT)
1299}
1300
1301async fn delete_proxy_group(
1302    State(state): State<Arc<AppState>>,
1303    Path(name): Path<String>,
1304) -> Result<StatusCode, (StatusCode, String)> {
1305    let _mutation = CONFIG_MUTATION.lock().await;
1306    let snapshot = {
1307        let mut raw = state.raw_config.read().clone();
1308        if let Some(ref mut groups) = raw.proxy_groups {
1309            let before = groups.len();
1310            groups.retain(|g| g.name != name);
1311            if groups.len() == before {
1312                return Err((StatusCode::NOT_FOUND, "group not found".into()));
1313            }
1314        } else {
1315            return Err((StatusCode::NOT_FOUND, "no groups".into()));
1316        }
1317        if let Some(ref mut rules) = raw.rules {
1318            rules.retain(|r| {
1319                let parts: Vec<&str> = r.split(',').collect();
1320                parts.last().is_none_or(|target| target.trim() != name)
1321            });
1322        }
1323        raw
1324    };
1325    commit_raw_candidate(&state, snapshot).await?;
1326    Ok(StatusCode::NO_CONTENT)
1327}
1328
1329#[derive(Deserialize)]
1330struct SelectProxyRequest {
1331    name: String,
1332}
1333
1334async fn select_proxy_in_group(
1335    State(state): State<Arc<AppState>>,
1336    Path(group_name): Path<String>,
1337    Json(body): Json<SelectProxyRequest>,
1338) -> StatusCode {
1339    let route = state.tunnel.route_snapshot();
1340    let Some(proxy) = route.proxies.get(group_name.as_str()).cloned() else {
1341        return StatusCode::NOT_FOUND;
1342    };
1343    let Some(selection) = proxy.selection() else {
1344        return StatusCode::BAD_REQUEST;
1345    };
1346    match selection.set(&body.name).await {
1347        Ok(()) => {
1348            info!("Proxy group '{}' switched to '{}'", group_name, body.name);
1349            StatusCode::NO_CONTENT
1350        }
1351        Err(_) => StatusCode::BAD_REQUEST,
1352    }
1353}
1354
1355// ── Rules CRUD ───────────────────────────────────────────────────────
1356
1357#[derive(Deserialize)]
1358struct ReplaceRulesRequest {
1359    rules: Vec<String>,
1360}
1361
1362async fn replace_rules(
1363    State(state): State<Arc<AppState>>,
1364    Json(body): Json<ReplaceRulesRequest>,
1365) -> Result<StatusCode, (StatusCode, String)> {
1366    let _mutation = CONFIG_MUTATION.lock().await;
1367    let snapshot = {
1368        let mut raw = state.raw_config.read().clone();
1369        raw.rules = Some(body.rules);
1370        raw
1371    };
1372    commit_raw_candidate(&state, snapshot).await?;
1373    Ok(StatusCode::NO_CONTENT)
1374}
1375
1376#[derive(Deserialize)]
1377struct UpdateRuleRequest {
1378    index: usize,
1379    rule: String,
1380}
1381
1382async fn update_rule_at_index(
1383    State(state): State<Arc<AppState>>,
1384    Json(body): Json<UpdateRuleRequest>,
1385) -> Result<StatusCode, (StatusCode, String)> {
1386    let _mutation = CONFIG_MUTATION.lock().await;
1387    let snapshot = {
1388        let mut raw = state.raw_config.read().clone();
1389        let rules = raw.rules.get_or_insert_with(Vec::new);
1390        if body.index >= rules.len() {
1391            return Err((StatusCode::BAD_REQUEST, "index out of range".into()));
1392        }
1393        rules[body.index] = body.rule;
1394        raw
1395    };
1396    commit_raw_candidate(&state, snapshot).await?;
1397    Ok(StatusCode::NO_CONTENT)
1398}
1399
1400async fn delete_rule(
1401    State(state): State<Arc<AppState>>,
1402    Path(index): Path<usize>,
1403) -> Result<StatusCode, (StatusCode, String)> {
1404    let _mutation = CONFIG_MUTATION.lock().await;
1405    let snapshot = {
1406        let mut raw = state.raw_config.read().clone();
1407        let rules = raw.rules.get_or_insert_with(Vec::new);
1408        if index >= rules.len() {
1409            return Err((StatusCode::BAD_REQUEST, "index out of range".into()));
1410        }
1411        rules.remove(index);
1412        raw
1413    };
1414    commit_raw_candidate(&state, snapshot).await?;
1415    Ok(StatusCode::NO_CONTENT)
1416}
1417
1418#[derive(Deserialize)]
1419struct ReorderRulesRequest {
1420    from: usize,
1421    to: usize,
1422}
1423
1424async fn reorder_rules(
1425    State(state): State<Arc<AppState>>,
1426    Json(body): Json<ReorderRulesRequest>,
1427) -> Result<StatusCode, (StatusCode, String)> {
1428    let _mutation = CONFIG_MUTATION.lock().await;
1429    let snapshot = {
1430        let mut raw = state.raw_config.read().clone();
1431        let rules = raw.rules.get_or_insert_with(Vec::new);
1432        if body.from >= rules.len() || body.to >= rules.len() {
1433            return Err((StatusCode::BAD_REQUEST, "index out of range".into()));
1434        }
1435        let rule = rules.remove(body.from);
1436        rules.insert(body.to, rule);
1437        raw
1438    };
1439    commit_raw_candidate(&state, snapshot).await?;
1440    Ok(StatusCode::NO_CONTENT)
1441}
1442
1443// ── Delay probe endpoints ────────────────────────────────────────────
1444//
1445// Matches upstream mihomo `hub/route/proxies.go::getProxyDelay` and
1446// `hub/route/groups.go::getGroupDelay`. Error bodies are byte-exact copies
1447// of upstream's `ErrBadRequest` / `ErrNotFound` / `ErrRequestTimeout` /
1448// `newError("An error occurred in the delay test")`.
1449
1450#[derive(Deserialize)]
1451struct DelayParams {
1452    url: Option<String>,
1453    timeout: Option<String>,
1454    expected: Option<String>,
1455}
1456
1457#[derive(Serialize)]
1458struct DelayResp {
1459    delay: u16,
1460}
1461
1462/// `{"message": "..."}` body matching upstream's error render.
1463fn msg_err(status: StatusCode, message: &'static str) -> Response {
1464    (status, Json(serde_json::json!({ "message": message }))).into_response()
1465}
1466
1467/// Validate `url` and `timeout`. Returns `timeout` as `Duration` on success,
1468/// or the `400 Body invalid` response on any validation failure — matching
1469/// upstream's single "ErrBadRequest" shape for all parse errors.
1470fn parse_delay_params(params: &DelayParams) -> Result<Duration, Box<Response>> {
1471    // upstream: hub/route/proxies.go::getProxyDelay — url is not strictly
1472    // validated upstream, but an empty host would panic our prober.
1473    let url = params.url.as_deref().unwrap_or("").trim();
1474    if url.is_empty() {
1475        return Err(Box::new(msg_err(StatusCode::BAD_REQUEST, "Body invalid")));
1476    }
1477
1478    // upstream parses `timeout` as int16 and treats parse failure as
1479    // ErrBadRequest. We reject 0 as well (a zero-budget probe is never useful).
1480    let timeout_str = params
1481        .timeout
1482        .as_deref()
1483        .ok_or_else(|| Box::new(msg_err(StatusCode::BAD_REQUEST, "Body invalid")))?;
1484    let timeout_ms: u16 = timeout_str
1485        .trim()
1486        .parse()
1487        .map_err(|_| Box::new(msg_err(StatusCode::BAD_REQUEST, "Body invalid")))?;
1488    if timeout_ms == 0 {
1489        return Err(Box::new(msg_err(StatusCode::BAD_REQUEST, "Body invalid")));
1490    }
1491    Ok(Duration::from_millis(timeout_ms as u64))
1492}
1493
1494/// Probe a single adapter and record the result into its health handle.
1495/// On success records the measured delay; on any failure records `0` so
1496/// the proxy's `last_delay` tracks the most recent outcome.
1497async fn probe_and_record(
1498    proxy: &Arc<dyn meow_common::Proxy>,
1499    url: &str,
1500    expected: Option<&str>,
1501    timeout: Duration,
1502) -> Result<u16, meow_proxy::health::UrlTestError> {
1503    meow_proxy::health::probe_and_record(proxy, url, expected, timeout).await
1504}
1505
1506async fn get_proxy_delay(
1507    State(state): State<Arc<AppState>>,
1508    Path(name): Path<String>,
1509    Query(params): Query<DelayParams>,
1510) -> Response {
1511    let timeout = match parse_delay_params(&params) {
1512        Ok(t) => t,
1513        Err(resp) => return *resp,
1514    };
1515    let url = params.url.as_deref().unwrap_or("").to_string();
1516    let expected = params.expected.clone();
1517
1518    let route = state.tunnel.route_snapshot();
1519    // upstream: hub/route/proxies.go::getProxyDelay — findProxyByName middleware
1520    let Some(proxy) = route.proxies.get(name.as_str()).cloned() else {
1521        return msg_err(StatusCode::NOT_FOUND, "resource not found");
1522    };
1523    drop(route);
1524
1525    match probe_and_record(&proxy, &url, expected.as_deref(), timeout).await {
1526        Ok(delay) => Json(DelayResp { delay }).into_response(),
1527        // upstream: `render.Status(r, http.StatusGatewayTimeout)` → 504.
1528        Err(meow_proxy::health::UrlTestError::Timeout) => {
1529            msg_err(StatusCode::GATEWAY_TIMEOUT, "Timeout")
1530        }
1531        // upstream: `newError("An error occurred in the delay test")` → 503.
1532        Err(meow_proxy::health::UrlTestError::Transport(_)) => msg_err(
1533            StatusCode::SERVICE_UNAVAILABLE,
1534            "An error occurred in the delay test",
1535        ),
1536    }
1537}
1538
1539async fn get_group_delay(
1540    State(state): State<Arc<AppState>>,
1541    Path(name): Path<String>,
1542    Query(params): Query<DelayParams>,
1543) -> Response {
1544    let route = state.tunnel.route_snapshot();
1545    let Some(group) = route.proxies.get(name.as_str()).cloned() else {
1546        return msg_err(StatusCode::NOT_FOUND, "resource not found");
1547    };
1548    // upstream: findProxyByName rejects non-groups with 404 for this route.
1549    let Some(member_names) = group.members() else {
1550        return msg_err(StatusCode::NOT_FOUND, "resource not found");
1551    };
1552
1553    let timeout = match parse_delay_params(&params) {
1554        Ok(t) => t,
1555        Err(resp) => return *resp,
1556    };
1557
1558    // mihomo clears a URLTest/Fallback user pin before every group-wide
1559    // health check. Moved after query validation so a malformed request
1560    // does not silently clear user state.
1561    if let Some(selection) = group.selection().filter(|s| s.can_unfix()) {
1562        selection.force_set(None);
1563    }
1564
1565    let url = params.url.as_deref().unwrap_or("").to_string();
1566    let expected = params.expected.clone();
1567
1568    // Resolve each member name to an `Arc<dyn Proxy>` *before* dropping the
1569    // proxies map so the spawned tasks hold their own Arc clones.
1570    let members: Vec<(String, Arc<dyn meow_common::Proxy>)> = member_names
1571        .into_iter()
1572        .filter_map(|n| route.proxies.get(n.as_str()).cloned().map(|p| (n, p)))
1573        .collect();
1574    drop(route);
1575
1576    // upstream: group probe wraps the whole batch in one context.WithTimeout,
1577    // not per-member. A slow member does not get its own budget.
1578    let collected = tokio::time::timeout(
1579        timeout,
1580        meow_proxy::health::probe_many_bounded_detailed(
1581            members,
1582            &url,
1583            expected.as_deref(),
1584            timeout,
1585            meow_proxy::health::GROUP_DELAY_CONCURRENCY,
1586        ),
1587    )
1588    .await;
1589
1590    let Ok(pairs) = collected else {
1591        // upstream: 504 "Timeout". Even if some members completed before the
1592        // deadline, upstream still returns the timeout error — we match.
1593        return msg_err(StatusCode::GATEWAY_TIMEOUT, "Timeout");
1594    };
1595
1596    let mut result: BTreeMap<String, u16> = BTreeMap::new();
1597    for pair in pairs {
1598        if matches!(pair.error, Some(meow_proxy::health::UrlTestError::Timeout)) {
1599            return msg_err(StatusCode::GATEWAY_TIMEOUT, "Timeout");
1600        }
1601        result.insert(pair.name, pair.delay);
1602    }
1603    Json(result).into_response()
1604}
1605
1606// ── Config reload (M1.G-10) ──────────────────────────────────────────
1607// upstream: hub/server.go::patchConfig
1608// Class B per ADR-0002: payload must be base64 (upstream inconsistent); YAML parse errors
1609// always return 400 even with force=true; NOT upstream silent broken-config apply.
1610
1611/// Spawn a TUN listener from a raw config and wait for device readiness.
1612/// Returns `Ok(Some(handle))` on success, `Ok(None)` when `tun.enable` is
1613/// false or the feature is not compiled in, or `Err(msg)` when startup fails
1614/// (permission denied, device-name conflict, timeout, etc.).
1615#[cfg(feature = "listener-tun")]
1616async fn spawn_tun_from_raw(
1617    tunnel: &Tunnel,
1618    raw: &RawConfig,
1619) -> Result<Option<tokio::task::JoinHandle<()>>, String> {
1620    let tun_cfg = match meow_config::parse_tun_config(raw.tun.as_ref()) {
1621        Ok(c) => c,
1622        Err(e) => {
1623            return Err(format!("tun config parse error: {e}"));
1624        }
1625    };
1626    if !tun_cfg.enable {
1627        return Ok(None);
1628    }
1629
1630    let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1631    let listener = TunListener::new(
1632        tunnel.clone(),
1633        crate::tun_config_to_listener_config(&tun_cfg),
1634        "meow-tun".to_string(),
1635    )
1636    .with_readiness_signal(ready_tx);
1637
1638    let handle = tokio::spawn(async move {
1639        if let Err(e) = listener.run().await {
1640            tracing::error!("TUN listener error: {e}");
1641        }
1642    });
1643
1644    // Await the readiness signal so we don't store a dead JoinHandle when
1645    // device creation fails (e.g. os error 5 / permission denied). The
1646    // timeout guards against a genuinely stuck startup path; immediate
1647    // failures are reported through `TunReady::Failed` without delay.
1648    match tokio::time::timeout(crate::TUN_STARTUP_TIMEOUT, ready_rx).await {
1649        Ok(Ok(meow_listener::TunReady::Ready)) => {}
1650        Ok(Ok(meow_listener::TunReady::Failed(msg))) => {
1651            tracing::error!(
1652                "TUN listener failed to start: {msg} \
1653                 (check permissions / admin / CAP_NET_ADMIN)"
1654            );
1655            handle.abort();
1656            return Err(msg);
1657        }
1658        Ok(Err(_)) => {
1659            // Should not happen with ReadyNotifier, but handle defensively.
1660            tracing::error!("TUN listener readiness signal dropped unexpectedly");
1661            handle.abort();
1662            return Err("TUN listener readiness signal dropped unexpectedly".into());
1663        }
1664        Err(_) => {
1665            let msg = format!(
1666                "TUN listener startup timed out after {} s",
1667                crate::TUN_STARTUP_TIMEOUT.as_secs()
1668            );
1669            tracing::error!("{msg}");
1670            handle.abort();
1671            return Err(msg);
1672        }
1673    }
1674
1675    Ok(Some(handle))
1676}
1677
1678#[cfg(not(feature = "listener-tun"))]
1679async fn spawn_tun_from_raw(
1680    _tunnel: &Tunnel,
1681    raw: &RawConfig,
1682) -> Result<Option<tokio::task::JoinHandle<()>>, String> {
1683    if raw.tun.as_ref().is_some_and(|t| t.enable) {
1684        // Err (not Ok(None)) so the off→on reconcile path rolls
1685        // `tun.enable` back — otherwise the stored config would claim TUN
1686        // is enabled while nothing can ever run.
1687        return Err("this build lacks the 'listener-tun' feature".into());
1688    }
1689    Ok(None)
1690}
1691
1692/// Commit `candidate` as the new raw config and reconcile the TUN listener
1693/// against the `tun.enable` transition. The whole sequence is serialised by
1694/// `config_mutation_lock` so two concurrent mutations cannot interleave
1695/// their TUN start/stop operations — without this a disable→stop could run
1696/// before a sibling enable→start has stored its handle, leaving a running
1697/// device behind an `enable=false` config.
1698///
1699/// On an off→on transition, if the TUN listener fails to start the stored
1700/// config is rolled back (`tun.enable` set to `false`) to prevent state
1701/// inconsistency (the HTTP API would report TUN as enabled but nothing is
1702/// actually running).  The HTTP response is still 204 — the error is
1703/// logged but not surfaced to the caller.
1704async fn swap_config_and_reconcile_tun(state: &AppState, candidate: RawConfig) {
1705    let _guard = state.config_mutation_lock.lock().await;
1706
1707    let new_enable = candidate.tun.as_ref().is_some_and(|t| t.enable);
1708    // Snapshot the candidate (only on an off→on transition, before it is
1709    // moved into the lock) so the parking_lot write guard — which is
1710    // !Send — is dropped before the first .await below.
1711    let (old_enable, snapshot) = {
1712        let mut guard = state.raw_config.write();
1713        let old = guard.tun.as_ref().is_some_and(|t| t.enable);
1714        let snapshot = (new_enable && !old).then(|| candidate.clone());
1715        *guard = candidate;
1716        (old, snapshot)
1717    };
1718
1719    if old_enable == new_enable {
1720        return;
1721    }
1722    if let Some(snapshot) = snapshot {
1723        // off → on
1724        match spawn_tun_from_raw(&state.tunnel, &snapshot).await {
1725            Ok(Some(handle)) => {
1726                state.tunnel.set_tun_handle(handle).await;
1727                info!("TUN listener started via config reload");
1728            }
1729            Ok(None) => {
1730                // enable=true but spawn returned no handle — should not
1731                // happen for this transition, but treat as success.
1732            }
1733            Err(e) => {
1734                // TUN failed to start — roll back tun.enable to false so
1735                // nyanpasu / the dashboard don't show TUN as active when
1736                // nothing is actually running.
1737                warn!("TUN listener failed to start: {e} (config rolled back)");
1738                if let Some(ref mut tun) = state.raw_config.write().tun {
1739                    tun.enable = false;
1740                }
1741            }
1742        }
1743    } else {
1744        // on → off
1745        state.tunnel.stop_tun().await;
1746        info!("TUN listener stopped via config reload");
1747    }
1748}
1749
1750#[derive(Deserialize)]
1751struct PutConfigsBody {
1752    path: Option<String>,
1753    payload: Option<String>,
1754}
1755
1756async fn put_configs(
1757    State(state): State<Arc<AppState>>,
1758    Query(params): Query<HashMap<String, String>>,
1759    Json(body): Json<PutConfigsBody>,
1760) -> Response {
1761    let force = params.get("force").is_some_and(|v| v == "true");
1762
1763    let yaml =
1764        match (body.path, body.payload) {
1765            (Some(p), _) => match tokio::fs::read_to_string(&p).await {
1766                Ok(s) => s,
1767                Err(e) => {
1768                    return (
1769                        StatusCode::BAD_REQUEST,
1770                        Json(serde_json::json!({"message": e.to_string()})),
1771                    )
1772                        .into_response()
1773                }
1774            },
1775            (_, Some(b64)) => {
1776                use base64::engine::general_purpose::STANDARD;
1777                use base64::Engine as _;
1778                let Ok(bytes) = STANDARD.decode(&b64) else {
1779                    return (
1780                        StatusCode::BAD_REQUEST,
1781                        Json(serde_json::json!({"message": "payload is not valid base64"})),
1782                    )
1783                        .into_response();
1784                };
1785                match String::from_utf8(bytes) {
1786                    Ok(s) => s,
1787                    Err(_) => {
1788                        return (
1789                            StatusCode::BAD_REQUEST,
1790                            Json(serde_json::json!({"message": "payload is not valid UTF-8"})),
1791                        )
1792                            .into_response()
1793                    }
1794                }
1795            }
1796            _ => return (
1797                StatusCode::BAD_REQUEST,
1798                Json(
1799                    serde_json::json!({"message": "request body must contain 'path' or 'payload'"}),
1800                ),
1801            )
1802                .into_response(),
1803        };
1804
1805    // YAML syntax check — always 400 even with force=true (per spec)
1806    let mut raw_config: RawConfig = match serde_yaml::from_str(&yaml) {
1807        Ok(c) => c,
1808        Err(e) => {
1809            return (
1810                StatusCode::BAD_REQUEST,
1811                Json(serde_json::json!({"message": format!("config parse error: {e}")})),
1812            )
1813                .into_response()
1814        }
1815    };
1816
1817    // Pre-resolve any DNS-sourced ECH configs into inline base64.
1818    if let Some(ps) = raw_config.proxies.as_mut() {
1819        meow_config::ech_dns::preresolve_ech(ps).await;
1820    }
1821
1822    let _mutation = CONFIG_MUTATION.lock().await;
1823
1824    // Semantic rebuild (proxy/rule parsing)
1825    let resolver = Arc::clone(state.tunnel.resolver());
1826    let providers = state
1827        .proxy_providers
1828        .iter()
1829        .map(|entry| (entry.key().clone(), Arc::clone(entry.value())))
1830        .collect();
1831    let (proxies, rules) =
1832        match rebuild_from_raw_with_resolver_async(raw_config.clone(), resolver, providers).await {
1833            Ok(r) => r,
1834            Err(e) => {
1835                if force {
1836                    tracing::error!("config reload forced despite validation error: {e}");
1837                    (Default::default(), Vec::new())
1838                } else {
1839                    return (
1840                        StatusCode::BAD_REQUEST,
1841                        Json(
1842                            serde_json::json!({"message": format!("config validation error: {e}")}),
1843                        ),
1844                    )
1845                        .into_response();
1846                }
1847            }
1848        };
1849
1850    // Cold reload: close all connections with structured log (Class A divergence from upstream)
1851    let stats = state.tunnel.statistics();
1852    let dropped = stats.active_connection_count();
1853    stats.close_all_connections();
1854    if dropped > 0 {
1855        tracing::warn!(
1856            connections_dropped = dropped,
1857            "connections force-closed after reload drain timeout"
1858        );
1859    }
1860
1861    state.tunnel.update_proxies(proxies);
1862    state.tunnel.update_rules(rules);
1863    if let Some(mode_str) = &raw_config.mode {
1864        if let Ok(mode) = mode_str.parse::<TunnelMode>() {
1865            state.tunnel.set_mode(mode);
1866        }
1867    }
1868
1869    swap_config_and_reconcile_tun(&state, raw_config).await;
1870
1871    StatusCode::NO_CONTENT.into_response()
1872}
1873
1874// ── Prometheus metrics (M1.H-2) ──────────────────────────────────────
1875// upstream: N/A — meow-rs enhancement; Go mihomo has no native /metrics endpoint.
1876
1877async fn get_metrics(State(_state): State<Arc<AppState>>) -> Response {
1878    // prometheus-client 0.22 requires AtomicU64/AtomicI64. On targets without
1879    // 64-bit atomics (e.g. MIPS32) these types don't exist in std, so we
1880    // return 501. cfg(target_has_atomic) is the correct gate — i686 Windows
1881    // is 32-bit-pointer but DOES have AtomicU64 via CMPXCHG8B.
1882    #[cfg(not(target_has_atomic = "64"))]
1883    {
1884        return (
1885            StatusCode::NOT_IMPLEMENTED,
1886            "metrics require 64-bit atomic support",
1887        )
1888            .into_response();
1889    }
1890
1891    #[cfg(target_has_atomic = "64")]
1892    {
1893        use prometheus_client::encoding::text::encode;
1894        use prometheus_client::metrics::counter::Counter;
1895        use prometheus_client::metrics::family::Family;
1896        use prometheus_client::metrics::gauge::Gauge;
1897        use prometheus_client::registry::Registry;
1898        use std::sync::atomic::{AtomicI64, AtomicU64};
1899
1900        let mut registry = Registry::default();
1901        let stats = _state.tunnel.statistics();
1902        let (upload_total, download_total) = stats.snapshot();
1903
1904        // meow_traffic_bytes — counter{direction}
1905        let traffic = Family::<Vec<(String, String)>, Counter<u64, AtomicU64>>::default();
1906        traffic
1907            .get_or_create(&vec![("direction".to_string(), "upload".to_string())])
1908            .inc_by(upload_total.max(0) as u64);
1909        traffic
1910            .get_or_create(&vec![("direction".to_string(), "download".to_string())])
1911            .inc_by(download_total.max(0) as u64);
1912        registry.register(
1913            "meow_traffic_bytes",
1914            "Cumulative bytes transferred since process start",
1915            traffic,
1916        );
1917
1918        // meow_connections_active — gauge
1919        let connections_active = Gauge::<i64, AtomicI64>::default();
1920        connections_active.set(stats.active_connection_count() as i64);
1921        registry.register(
1922            "meow_connections_active",
1923            "Number of currently open connections",
1924            connections_active,
1925        );
1926
1927        // meow_proxy_alive and meow_proxy_delay_ms — gauge{proxy_name,adapter_type}
1928        let proxy_alive = Family::<Vec<(String, String)>, Gauge<i64, AtomicI64>>::default();
1929        let proxy_delay = Family::<Vec<(String, String)>, Gauge<i64, AtomicI64>>::default();
1930        let route = _state.tunnel.route_snapshot();
1931        for (name, proxy) in &route.proxies {
1932            let labels = vec![
1933                ("proxy_name".to_string(), name.to_string()),
1934                ("adapter_type".to_string(), proxy.adapter_type().to_string()),
1935            ];
1936            proxy_alive
1937                .get_or_create(&labels)
1938                .set(if proxy.alive() { 1 } else { 0 });
1939            // Omit delay series entirely when no health check has run (empty history).
1940            // NOT -1, NOT 0 — absence is the correct Prometheus signal for "unknown".
1941            if !proxy.delay_history().is_empty() {
1942                proxy_delay
1943                    .get_or_create(&labels)
1944                    .set(proxy.last_delay() as i64);
1945            }
1946        }
1947        registry.register(
1948            "meow_proxy_alive",
1949            "Proxy alive status (1=alive, 0=dead)",
1950            proxy_alive,
1951        );
1952        registry.register(
1953            "meow_proxy_delay_ms",
1954            "Last measured proxy round-trip delay in milliseconds",
1955            proxy_delay,
1956        );
1957
1958        // meow_rules_matched — counter{rule_type,action}
1959        let rules_matched = Family::<Vec<(String, String)>, Counter<u64, AtomicU64>>::default();
1960        for ((rule_type, action), count) in stats.rule_match.snapshot() {
1961            rules_matched
1962                .get_or_create(&vec![
1963                    ("rule_type".to_string(), rule_type.to_string()),
1964                    ("action".to_string(), action.to_string()),
1965                ])
1966                .inc_by(count);
1967        }
1968        registry.register(
1969            "meow_rules_matched",
1970            "Cumulative rule matches by type and action",
1971            rules_matched,
1972        );
1973
1974        // meow_memory_rss_bytes — gauge
1975        let memory_rss = Gauge::<i64, AtomicI64>::default();
1976        memory_rss.set(read_rss_bytes().await as i64);
1977        registry.register(
1978            "meow_memory_rss_bytes",
1979            "Current process RSS in bytes",
1980            memory_rss,
1981        );
1982
1983        // meow_info — gauge{version,mode} always = 1
1984        let info = Family::<Vec<(String, String)>, Gauge<i64, AtomicI64>>::default();
1985        info.get_or_create(&vec![
1986            ("version".to_string(), env!("CARGO_PKG_VERSION").to_string()),
1987            ("mode".to_string(), _state.tunnel.mode().to_string()),
1988        ])
1989        .set(1);
1990        registry.register("meow_info", "meow-rs runtime info", info);
1991
1992        let mut body = String::new();
1993        encode(&mut body, &registry).expect("prometheus text encoding is infallible");
1994        (
1995            StatusCode::OK,
1996            [(
1997                header::CONTENT_TYPE,
1998                "text/plain; version=0.0.4; charset=utf-8",
1999            )],
2000            body,
2001        )
2002            .into_response()
2003    }
2004}
2005
2006// ── WebSocket: log stream ────────────────────────────────────────────
2007
2008#[derive(Deserialize)]
2009struct LogsParams {
2010    level: Option<String>,
2011    format: Option<String>,
2012}
2013
2014fn parse_requested_log_level(
2015    value: Option<&str>,
2016) -> Result<crate::log_stream::LogLevel, Box<Response>> {
2017    let value = value.unwrap_or("info");
2018    match value.to_ascii_lowercase().as_str() {
2019        "debug" | "info" | "warning" | "warn" | "error" | "silent" => Ok(parse_log_level(value)),
2020        _ => Err(Box::new(msg_err(StatusCode::BAD_REQUEST, "Body invalid"))),
2021    }
2022}
2023
2024fn log_json(msg: &LogMessage, structured: bool) -> String {
2025    if !structured {
2026        return serde_json::json!({"type": msg.level.as_str(), "payload": msg.payload}).to_string();
2027    }
2028    let level = if msg.level.as_str() == "warning" {
2029        "warn"
2030    } else {
2031        msg.level.as_str()
2032    };
2033    let t = msg.time.time();
2034    serde_json::json!({
2035        "time": format!("{:02}:{:02}:{:02}", t.hour(), t.minute(), t.second()),
2036        "level": level,
2037        "message": msg.payload,
2038        "fields": [],
2039    })
2040    .to_string()
2041}
2042
2043// upstream: hub/route/logs.go::getLogs
2044async fn get_logs(
2045    State(state): State<Arc<AppState>>,
2046    Query(params): Query<LogsParams>,
2047    MaybeWebSocket(ws): MaybeWebSocket,
2048) -> Response {
2049    let level = match parse_requested_log_level(params.level.as_deref()) {
2050        Ok(level) => level,
2051        Err(response) => return *response,
2052    };
2053    let structured = params.format.as_deref() == Some("structured");
2054    let mut rx = state.log_tx.subscribe();
2055    if let Some(ws) = ws {
2056        return ws.on_upgrade(move |mut socket| async move {
2057            loop {
2058                match rx.recv().await {
2059                    Ok(msg) if msg.level >= level => {
2060                        if socket
2061                            .send(Message::Text(log_json(&msg, structured).into()))
2062                            .await
2063                            .is_err()
2064                        {
2065                            break;
2066                        }
2067                    }
2068                    Ok(_) | Err(broadcast::error::RecvError::Lagged(_)) => {}
2069                    Err(broadcast::error::RecvError::Closed) => break,
2070                }
2071            }
2072        });
2073    }
2074
2075    let stream = futures::stream::unfold(rx, move |mut rx| async move {
2076        loop {
2077            match rx.recv().await {
2078                Ok(msg) if msg.level >= level => {
2079                    return Some((
2080                        Ok::<String, std::convert::Infallible>(format!(
2081                            "{}\n",
2082                            log_json(&msg, structured)
2083                        )),
2084                        rx,
2085                    ));
2086                }
2087                Ok(_) | Err(broadcast::error::RecvError::Lagged(_)) => {}
2088                Err(broadcast::error::RecvError::Closed) => return None,
2089            }
2090        }
2091    });
2092    Response::builder()
2093        .header(header::CONTENT_TYPE, "application/json")
2094        .body(Body::from_stream(stream))
2095        .expect("valid log stream response")
2096}
2097
2098// ── WebSocket: memory stream ─────────────────────────────────────────
2099
2100// upstream: hub/route/memory.go
2101//
2102// One process-wide sampler task reads RSS + limit and serialises the JSON
2103// frame once per tick; every connected socket forwards the shared string
2104// (audit M8 — previously each socket sampled and serialised independently,
2105// per-socket per-tick). The sampler starts with the first subscriber and
2106// exits once the last socket disconnects, so an idle API server pays nothing.
2107// Model: the log websocket's single-serialisation broadcast fan-out.
2108static MEMORY_FEED: std::sync::Mutex<Option<broadcast::Sender<Arc<str>>>> =
2109    std::sync::Mutex::new(None);
2110
2111fn subscribe_memory_feed() -> broadcast::Receiver<Arc<str>> {
2112    let mut guard = MEMORY_FEED.lock().expect("memory feed lock poisoned");
2113    if let Some(tx) = guard.as_ref() {
2114        // Sampler still alive (it clears the slot under this lock on exit).
2115        return tx.subscribe();
2116    }
2117    let (tx, rx) = broadcast::channel(2);
2118    *guard = Some(tx.clone());
2119    tokio::spawn(async move {
2120        let mut interval = tokio::time::interval(Duration::from_secs(1));
2121        loop {
2122            interval.tick().await;
2123            if tx.receiver_count() == 0 {
2124                // Re-check under the lock so a subscriber arriving right now
2125                // either sees the live sender or a cleared slot — never a
2126                // sender whose sampler has already exited.
2127                let mut guard = MEMORY_FEED.lock().expect("memory feed lock poisoned");
2128                if tx.receiver_count() == 0 {
2129                    *guard = None;
2130                    break;
2131                }
2132            }
2133            let inuse = read_rss_bytes().await;
2134            let oslimit = read_os_memory_limit().await;
2135            let msg: Arc<str> = Arc::from(format!("{{\"inuse\":{inuse},\"oslimit\":{oslimit}}}"));
2136            let _ = tx.send(msg);
2137        }
2138    });
2139    rx
2140}
2141
2142async fn get_memory(
2143    State(_state): State<Arc<AppState>>,
2144    MaybeWebSocket(ws): MaybeWebSocket,
2145) -> Response {
2146    let first: Arc<str> = Arc::from("{\"inuse\":0,\"oslimit\":0}");
2147    if let Some(ws) = ws {
2148        return ws.on_upgrade(move |mut socket| async move {
2149            if socket
2150                .send(Message::Text(first.as_ref().into()))
2151                .await
2152                .is_err()
2153            {
2154                return;
2155            }
2156            let mut feed = subscribe_memory_feed();
2157            loop {
2158                let msg = match feed.recv().await {
2159                    Ok(msg) => msg,
2160                    Err(broadcast::error::RecvError::Lagged(_)) => continue,
2161                    Err(broadcast::error::RecvError::Closed) => break,
2162                };
2163                if socket
2164                    .send(Message::Text(msg.as_ref().into()))
2165                    .await
2166                    .is_err()
2167                {
2168                    break;
2169                }
2170            }
2171        });
2172    }
2173
2174    let feed = subscribe_memory_feed();
2175    let stream = futures::stream::unfold((Some(first), feed), |(first, mut feed)| async move {
2176        if let Some(first) = first {
2177            return Some((
2178                Ok::<String, std::convert::Infallible>(format!("{first}\n")),
2179                (None, feed),
2180            ));
2181        }
2182        loop {
2183            match feed.recv().await {
2184                Ok(msg) => {
2185                    return Some((
2186                        Ok::<String, std::convert::Infallible>(format!("{msg}\n")),
2187                        (None, feed),
2188                    ));
2189                }
2190                Err(broadcast::error::RecvError::Lagged(_)) => continue,
2191                Err(broadcast::error::RecvError::Closed) => return None,
2192            }
2193        }
2194    });
2195    Response::builder()
2196        .header(header::CONTENT_TYPE, "application/json")
2197        .body(Body::from_stream(stream))
2198        .expect("valid memory stream response")
2199}
2200
2201async fn read_rss_bytes() -> u64 {
2202    tokio::task::spawn_blocking(|| {
2203        use sysinfo::{Pid, ProcessesToUpdate, System};
2204        let pid = Pid::from_u32(std::process::id());
2205        let mut sys = System::new();
2206        sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), false);
2207        sys.process(pid).map_or(0, sysinfo::Process::memory)
2208    })
2209    .await
2210    .unwrap_or(0)
2211}
2212
2213async fn read_os_memory_limit() -> u64 {
2214    #[cfg(target_os = "linux")]
2215    {
2216        read_os_memory_limit_linux().await
2217    }
2218    #[cfg(not(target_os = "linux"))]
2219    {
2220        0
2221    }
2222}
2223
2224#[cfg(target_os = "linux")]
2225async fn read_os_memory_limit_linux() -> u64 {
2226    // Try cgroup v2 memory limit first, fall back to rlimit.
2227    if let Ok(s) = tokio::fs::read_to_string("/sys/fs/cgroup/memory.max").await {
2228        if let Ok(n) = s.trim().parse::<u64>() {
2229            return n;
2230        }
2231    }
2232    // rlimit RLIMIT_AS (virtual address space) as a proxy; RLIMIT_RSS is deprecated.
2233    unsafe {
2234        let mut rl = libc::rlimit {
2235            rlim_cur: 0,
2236            rlim_max: 0,
2237        };
2238        if libc::getrlimit(libc::RLIMIT_AS, &mut rl) == 0 && rl.rlim_cur != libc::RLIM_INFINITY {
2239            #[cfg(target_pointer_width = "32")]
2240            {
2241                return rl.rlim_cur as u64;
2242            }
2243            #[cfg(not(target_pointer_width = "32"))]
2244            {
2245                return rl.rlim_cur;
2246            }
2247        }
2248    }
2249    0
2250}
2251
2252// ── Proxy providers ───────────────────────────────────────────────────
2253
2254#[derive(Serialize)]
2255#[serde(rename_all = "camelCase")]
2256struct ProviderInfo {
2257    name: String,
2258    #[serde(rename = "type")]
2259    provider_type: String,
2260    vehicle_type: String,
2261    proxies: Vec<ProxyInfo>,
2262    #[serde(rename = "testUrl")]
2263    test_url: String,
2264    #[serde(rename = "expectedStatus")]
2265    expected_status: String,
2266    #[serde(rename = "updatedAt", skip_serializing_if = "Option::is_none")]
2267    updated_at: Option<String>,
2268}
2269
2270fn unix_rfc3339(seconds: u64) -> Option<String> {
2271    use time::format_description::well_known::Rfc3339;
2272    (seconds > 0)
2273        .then(|| time::OffsetDateTime::from_unix_timestamp(seconds as i64).ok())
2274        .flatten()
2275        .and_then(|time| time.format(&Rfc3339).ok())
2276}
2277
2278fn provider_to_info(name: &str, provider: &ProxyProvider) -> ProviderInfo {
2279    let proxies = provider
2280        .proxies()
2281        .iter()
2282        .map(ProxyInfo::from_proxy)
2283        .collect();
2284    ProviderInfo {
2285        name: name.to_string(),
2286        provider_type: "Proxy".to_string(),
2287        vehicle_type: provider.vehicle_type.to_string(),
2288        proxies,
2289        test_url: provider
2290            .health_check
2291            .as_ref()
2292            .map_or_else(String::new, |hc| hc.url.clone()),
2293        expected_status: provider
2294            .health_check
2295            .as_ref()
2296            .map_or_else(String::new, |hc| hc.expected_status.clone()),
2297        updated_at: unix_rfc3339(provider.updated_at_secs()),
2298    }
2299}
2300
2301async fn get_providers(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
2302    let mut map = serde_json::Map::new();
2303    for entry in state.proxy_providers.iter() {
2304        let info = provider_to_info(entry.key(), entry.value());
2305        map.insert(
2306            entry.key().clone(),
2307            serde_json::to_value(info).unwrap_or_default(),
2308        );
2309    }
2310    Json(serde_json::json!({ "providers": map }))
2311}
2312
2313async fn get_provider(State(state): State<Arc<AppState>>, Path(name): Path<String>) -> Response {
2314    match state.proxy_providers.get(&name) {
2315        Some(entry) => Json(provider_to_info(&name, entry.value())).into_response(),
2316        None => msg_err(StatusCode::NOT_FOUND, "resource not found"),
2317    }
2318}
2319
2320async fn refresh_provider(
2321    State(state): State<Arc<AppState>>,
2322    Path(name): Path<String>,
2323) -> Response {
2324    let provider = match state.proxy_providers.get(&name) {
2325        Some(entry) => Arc::clone(entry.value()),
2326        None => return msg_err(StatusCode::NOT_FOUND, "resource not found"),
2327    };
2328    match provider.refresh().await {
2329        Ok(()) => StatusCode::NO_CONTENT.into_response(),
2330        Err(e) => (
2331            StatusCode::SERVICE_UNAVAILABLE,
2332            Json(serde_json::json!({"message": e})),
2333        )
2334            .into_response(),
2335    }
2336}
2337
2338/// Trigger a health check for all proxies in the named provider.
2339/// Accepts the same `url` and `timeout` query params as `GET /proxies/:name/delay`.
2340async fn provider_healthcheck(
2341    State(state): State<Arc<AppState>>,
2342    Path(name): Path<String>,
2343) -> Response {
2344    let provider = match state.proxy_providers.get(&name) {
2345        Some(entry) => Arc::clone(entry.value()),
2346        None => return msg_err(StatusCode::NOT_FOUND, "resource not found"),
2347    };
2348
2349    let Some(health) = provider.health_check.as_ref() else {
2350        return StatusCode::NO_CONTENT.into_response();
2351    };
2352    let timeout = Duration::from_millis(health.timeout.max(1));
2353    let url = health.url.clone();
2354    let expected = (!health.expected_status.is_empty()).then(|| health.expected_status.clone());
2355
2356    let members = provider
2357        .proxies()
2358        .into_iter()
2359        .map(|proxy| (proxy.name().to_string(), proxy))
2360        .collect();
2361
2362    let _ = meow_proxy::health::probe_many_bounded(
2363        members,
2364        &url,
2365        expected.as_deref(),
2366        timeout,
2367        meow_proxy::health::PROVIDER_HEALTHCHECK_CONCURRENCY,
2368    )
2369    .await;
2370
2371    StatusCode::NO_CONTENT.into_response()
2372}
2373
2374async fn get_provider_proxy(
2375    State(state): State<Arc<AppState>>,
2376    Path((provider_name, proxy_name)): Path<(String, String)>,
2377) -> Response {
2378    let Some(provider) = state.proxy_providers.get(&provider_name) else {
2379        return msg_err(StatusCode::NOT_FOUND, "Resource not found");
2380    };
2381    match provider
2382        .proxies()
2383        .into_iter()
2384        .find(|p| p.name() == proxy_name)
2385    {
2386        Some(proxy) => Json(ProxyInfo::from_proxy(&proxy)).into_response(),
2387        None => msg_err(StatusCode::NOT_FOUND, "Resource not found"),
2388    }
2389}
2390
2391async fn provider_proxy_healthcheck(
2392    State(state): State<Arc<AppState>>,
2393    Path((provider_name, proxy_name)): Path<(String, String)>,
2394    Query(params): Query<DelayParams>,
2395) -> Response {
2396    let timeout = match parse_delay_params(&params) {
2397        Ok(timeout) => timeout,
2398        Err(response) => return *response,
2399    };
2400    let Some(provider) = state.proxy_providers.get(&provider_name) else {
2401        return msg_err(StatusCode::NOT_FOUND, "Resource not found");
2402    };
2403    let Some(proxy) = provider
2404        .proxies()
2405        .into_iter()
2406        .find(|p| p.name() == proxy_name)
2407    else {
2408        return msg_err(StatusCode::NOT_FOUND, "Resource not found");
2409    };
2410    match probe_and_record(
2411        &proxy,
2412        params.url.as_deref().unwrap_or(""),
2413        params.expected.as_deref(),
2414        timeout,
2415    )
2416    .await
2417    {
2418        Ok(delay) => Json(DelayResp { delay }).into_response(),
2419        Err(meow_proxy::health::UrlTestError::Timeout) => {
2420            msg_err(StatusCode::GATEWAY_TIMEOUT, "Timeout")
2421        }
2422        Err(meow_proxy::health::UrlTestError::Transport(_)) => msg_err(
2423            StatusCode::SERVICE_UNAVAILABLE,
2424            "An error occurred in the delay test",
2425        ),
2426    }
2427}
2428
2429// ── Rule Providers ────────────────────────────────────────────────────
2430
2431#[derive(Serialize)]
2432struct RuleProviderInfo {
2433    name: String,
2434    #[serde(rename = "type")]
2435    provider_type: String,
2436    behavior: String,
2437    format: String,
2438    #[serde(rename = "ruleCount")]
2439    rule_count: usize,
2440    #[serde(rename = "updatedAt")]
2441    updated_at: String,
2442    #[serde(rename = "vehicleType")]
2443    vehicle_type: String,
2444}
2445
2446impl RuleProviderInfo {
2447    fn from_provider(p: &Arc<RuleProvider>, format: Option<&str>) -> Self {
2448        let vehicle_type = match p.provider_type {
2449            meow_config::rule_provider::ProviderType::Http => "HTTP",
2450            meow_config::rule_provider::ProviderType::File => "File",
2451            meow_config::rule_provider::ProviderType::Inline => "Inline",
2452        };
2453        Self {
2454            name: p.name.clone(),
2455            provider_type: "Rule".to_string(),
2456            behavior: p.behavior.to_string(),
2457            format: format.unwrap_or("yaml").to_string(),
2458            rule_count: p.rule_count(),
2459            updated_at: unix_rfc3339(p.updated_at_secs()).unwrap_or_default(),
2460            vehicle_type: vehicle_type.to_string(),
2461        }
2462    }
2463}
2464
2465#[derive(Serialize)]
2466struct RuleProvidersResponse {
2467    providers: HashMap<String, RuleProviderInfo>,
2468}
2469
2470async fn get_rule_providers(State(state): State<Arc<AppState>>) -> Json<RuleProvidersResponse> {
2471    let providers = state.rule_providers.read();
2472    let raw = state.raw_config.read();
2473    let map: HashMap<String, RuleProviderInfo> = providers
2474        .iter()
2475        .map(|(name, p): (&String, &Arc<RuleProvider>)| {
2476            let format = raw
2477                .rule_providers
2478                .as_ref()
2479                .and_then(|all| all.get(name))
2480                .and_then(|provider| provider.format.as_deref());
2481            (name.clone(), RuleProviderInfo::from_provider(p, format))
2482        })
2483        .collect();
2484    Json(RuleProvidersResponse { providers: map })
2485}
2486
2487async fn get_rule_provider(
2488    State(state): State<Arc<AppState>>,
2489    Path(name): Path<String>,
2490) -> Result<Json<RuleProviderInfo>, StatusCode> {
2491    let providers = state.rule_providers.read();
2492    let p = providers.get(&name).ok_or(StatusCode::NOT_FOUND)?;
2493    let raw = state.raw_config.read();
2494    let format = raw
2495        .rule_providers
2496        .as_ref()
2497        .and_then(|all| all.get(&name))
2498        .and_then(|provider| provider.format.as_deref());
2499    Ok(Json(RuleProviderInfo::from_provider(p, format)))
2500}
2501
2502async fn refresh_rule_provider(
2503    State(state): State<Arc<AppState>>,
2504    Path(name): Path<String>,
2505) -> StatusCode {
2506    let provider = {
2507        let providers = state.rule_providers.read();
2508        providers.get(&name).cloned()
2509    };
2510    let Some(p) = provider else {
2511        return StatusCode::NOT_FOUND;
2512    };
2513    let ctx = meow_rules::ParserContext::empty();
2514    match p.refresh(&ctx).await {
2515        Ok(()) => StatusCode::NO_CONTENT,
2516        Err(e) => {
2517            tracing::warn!(provider = %name, "rule-provider refresh failed: {:#}", e);
2518            StatusCode::SERVICE_UNAVAILABLE
2519        }
2520    }
2521}
2522
2523// ── Listeners ─────────────────────────────────────────────────────────
2524
2525async fn get_listeners(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
2526    let items: Vec<serde_json::Value> = state
2527        .listeners
2528        .iter()
2529        .map(|l| {
2530            serde_json::json!({
2531                "name": l.name,
2532                "type": l.listener_type.to_string(),
2533                "port": l.port,
2534                "listen": l.listen,
2535            })
2536        })
2537        .collect();
2538    Json(serde_json::json!(items))
2539}
2540
2541#[cfg(test)]
2542mod tests {
2543    use super::*;
2544
2545    #[test]
2546    fn connections_interval_rejects_zero_and_garbage() {
2547        // `0` and non-numeric input stay a 400 at the handler — the parser
2548        // signals both with `None`.
2549        assert_eq!(parse_connections_interval(Some("0")), None);
2550        assert_eq!(parse_connections_interval(Some("abc")), None);
2551        assert_eq!(parse_connections_interval(Some("-1")), None);
2552        assert_eq!(parse_connections_interval(Some("")), None);
2553    }
2554
2555    #[test]
2556    fn connections_interval_clamps_to_floor() {
2557        assert_eq!(parse_connections_interval(Some("1")), Some(100));
2558        assert_eq!(parse_connections_interval(Some("99")), Some(100));
2559        assert_eq!(parse_connections_interval(Some("100")), Some(100));
2560    }
2561
2562    #[test]
2563    fn connections_interval_passes_through_above_floor() {
2564        assert_eq!(parse_connections_interval(Some("101")), Some(101));
2565        assert_eq!(parse_connections_interval(Some("5000")), Some(5000));
2566        assert_eq!(parse_connections_interval(None), Some(1000));
2567    }
2568}