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