Skip to main content

meow_api/
routes.rs

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