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 pub secret: Option<String>,
61 pub config_path: String,
62 pub raw_config: Arc<RwLock<RawConfig>>,
63 pub log_tx: broadcast::Sender<LogMessage>,
65 pub proxy_providers: Arc<DashMap<String, Arc<ProxyProvider>>>,
67 pub rule_providers: Arc<RwLock<HashMap<String, Arc<RuleProvider>>>>,
68 pub listeners: Vec<NamedListener>,
70 pub external_ui: Option<std::path::PathBuf>,
73}
74
75static 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
85async 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 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 .route("/api/config/save", post(save_config))
173 .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 .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 .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 .route("/providers/rules", get(get_rule_providers))
216 .route(
217 "/providers/rules/{name}",
218 get(get_rule_provider).put(refresh_rule_provider),
219 )
220 .route("/listeners", get(get_listeners))
222 .route_layer(middleware::from_fn_with_state(
223 Arc::clone(&state),
224 require_auth_ws,
225 ));
226
227 let router = api;
234 let router = if let Some(dir) = state.external_ui.clone() {
235 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#[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 #[serde(skip_serializing_if = "Option::is_none")]
283 all: Option<Vec<String>>,
284 #[serde(skip_serializing_if = "Option::is_none")]
286 now: Option<String>,
287 #[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 #[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 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 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 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 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
754async 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(¶ms.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(¶ms.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
878async 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
898async 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
910async 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#[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 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 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 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 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#[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#[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#[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
1428fn msg_err(status: StatusCode, message: &'static str) -> Response {
1430 (status, Json(serde_json::json!({ "message": message }))).into_response()
1431}
1432
1433fn parse_delay_params(params: &DelayParams) -> Result<Duration, Box<Response>> {
1437 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 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
1460async 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(¶ms) {
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 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 Err(meow_proxy::health::UrlTestError::Timeout) => {
1495 msg_err(StatusCode::GATEWAY_TIMEOUT, "Timeout")
1496 }
1497 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 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(¶ms) {
1520 Ok(t) => t,
1521 Err(resp) => return *resp,
1522 };
1523
1524 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 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 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 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#[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 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 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 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 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
1700async fn get_metrics(State(_state): State<Arc<AppState>>) -> Response {
1704 #[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 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 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 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 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 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 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 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, ®istry).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#[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
1869async 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
1924static 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 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 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 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 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#[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
2164async 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(¶ms) {
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#[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
2349async 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}