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