Skip to main content

meow_api/
lib.rs

1pub mod log_stream;
2pub mod routes;
3pub mod ui;
4
5use dashmap::DashMap;
6use log_stream::LogMessage;
7use meow_config::{
8    proxy_provider::ProxyProvider, raw::RawConfig, rule_provider::RuleProvider, NamedListener,
9};
10use meow_tunnel::Tunnel;
11use parking_lot::RwLock;
12use std::collections::HashMap;
13use std::net::SocketAddr;
14use std::path::PathBuf;
15use std::sync::Arc;
16use tokio::sync::broadcast;
17use tracing::{info, warn};
18
19/// How long to wait for the TUN readiness signal (device creation + stack
20/// init + child-task setup) before treating the listener as failed to
21/// start. Shared between the startup path (`meow-app/src/main.rs`) and the
22/// config-reload path (`routes.rs::spawn_tun_from_raw`) so the two don't
23/// drift. Windows wintun adapter creation plus smoltcp netstack init can
24/// take tens of seconds on slow machines; 5 s proved too aggressive.
25pub const TUN_STARTUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
26
27/// Map a parsed `TunConfig` onto a `TunListenerConfig`. Shared between the
28/// startup path (`meow-app/src/main.rs`) and the config-reload path
29/// (`routes.rs::spawn_tun_from_raw`) so the two don't drift.
30#[cfg(feature = "listener-tun")]
31pub fn tun_config_to_listener_config(
32    tun: &meow_config::TunConfig,
33) -> meow_listener::TunListenerConfig {
34    meow_listener::TunListenerConfig {
35        device: tun.device.clone(),
36        mtu: tun.mtu,
37        inet4_address: tun.inet4_address,
38        auto_route: tun.auto_route,
39        dns_hijack: tun.dns_hijack,
40        udp_timeout: tun.udp_timeout,
41    }
42}
43
44pub struct ApiServer {
45    tunnel: Tunnel,
46    listen_addr: SocketAddr,
47    secret: Option<String>,
48    config_path: String,
49    raw_config: Arc<RwLock<RawConfig>>,
50    log_tx: broadcast::Sender<LogMessage>,
51    proxy_providers: Arc<DashMap<String, Arc<ProxyProvider>>>,
52    rule_providers: Arc<RwLock<HashMap<String, Arc<RuleProvider>>>>,
53    listeners: Vec<NamedListener>,
54    external_ui: Option<PathBuf>,
55}
56
57impl ApiServer {
58    #[allow(clippy::too_many_arguments)]
59    pub fn new(
60        tunnel: Tunnel,
61        listen_addr: SocketAddr,
62        secret: Option<String>,
63        config_path: String,
64        raw_config: Arc<RwLock<RawConfig>>,
65        log_tx: broadcast::Sender<LogMessage>,
66        proxy_providers: Arc<DashMap<String, Arc<ProxyProvider>>>,
67        rule_providers: Arc<RwLock<HashMap<String, Arc<RuleProvider>>>>,
68        listeners: Vec<NamedListener>,
69        external_ui: Option<PathBuf>,
70    ) -> Self {
71        Self {
72            tunnel,
73            listen_addr,
74            secret,
75            config_path,
76            raw_config,
77            log_tx,
78            proxy_providers,
79            rule_providers,
80            listeners,
81            external_ui,
82        }
83    }
84
85    pub async fn run(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
86        let state = Arc::new(routes::AppState {
87            tunnel: self.tunnel.clone(),
88            secret: self.secret.clone(),
89            config_path: self.config_path.clone(),
90            raw_config: Arc::clone(&self.raw_config),
91            log_tx: self.log_tx.clone(),
92            proxy_providers: Arc::clone(&self.proxy_providers),
93            rule_providers: Arc::clone(&self.rule_providers),
94            listeners: self.listeners.clone(),
95            external_ui: self.resolve_external_ui(),
96            config_mutation_lock: tokio::sync::Mutex::new(()),
97        });
98
99        let app = routes::create_router(state);
100
101        let listener = tokio::net::TcpListener::bind(self.listen_addr).await?;
102        info!("REST API listening on {}", self.listen_addr);
103        info!("Web UI available at http://{}/ui", self.listen_addr);
104        axum::serve(listener, app).await?;
105        Ok(())
106    }
107
108    /// Validate the configured external-UI directory. Returns the path only when
109    /// it exists as a directory; otherwise logs a warning and falls back to the
110    /// built-in panel (issue #223).
111    fn resolve_external_ui(&self) -> Option<PathBuf> {
112        let dir = self.external_ui.as_ref()?;
113        if dir.is_dir() {
114            info!("Serving external Web UI from {}", dir.display());
115            Some(dir.clone())
116        } else {
117            warn!(
118                "external-ui directory {} not found; serving the built-in panel instead",
119                dir.display()
120            );
121            None
122        }
123    }
124}