Skip to main content

rinova_proxy_sdk/
server.rs

1use std::net::SocketAddr;
2use std::sync::Arc;
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use axum::{
6    extract::{Request, State},
7    http::{header, HeaderValue, StatusCode},
8    middleware::{self, Next},
9    response::{IntoResponse, Response},
10    routing::{get, post},
11    Json, Router,
12};
13use serde::Serialize;
14use tokio::sync::Mutex;
15use tokio::task::JoinHandle;
16
17use crate::builder::{build_config, to_yaml};
18use crate::error::{ProxyError, Result};
19use crate::fetch::{deduplicate_names, fetch_subscription};
20use crate::i18n::t;
21use crate::parser::parse_lines;
22use crate::types::RuleMode;
23
24#[derive(Debug, Clone)]
25pub struct ServerOptions {
26    pub url: String,
27    pub port: u16,
28    pub interval_min: u64,
29    pub rule_mode: RuleMode,
30}
31
32struct CacheEntry {
33    yaml: String,
34    node_count: usize,
35    updated_at: u64,
36}
37
38struct AppState {
39    opts: ServerOptions,
40    cache: Option<CacheEntry>,
41    refresh_error: Option<String>,
42    refreshing: bool,
43}
44
45pub struct ServerHandle {
46    shutdown_tx: tokio::sync::oneshot::Sender<()>,
47    join_handle: JoinHandle<()>,
48    refresh_handle: JoinHandle<()>,
49}
50
51impl ServerHandle {
52    pub async fn shutdown(self) {
53        let _ = self.shutdown_tx.send(());
54        self.refresh_handle.abort();
55        let _ = self.refresh_handle.await;
56        let _ = self.join_handle.await;
57    }
58}
59
60#[derive(Serialize)]
61#[serde(rename_all = "camelCase")]
62struct HealthResponse {
63    status: &'static str,
64    nodes: usize,
65    updated_at: Option<String>,
66    next_refresh_min: u64,
67    last_error: Option<String>,
68}
69
70#[derive(Serialize)]
71struct RefreshResponse {
72    ok: bool,
73    skipped: bool,
74    nodes: usize,
75}
76
77#[derive(Serialize)]
78struct RefreshErrorResponse {
79    ok: bool,
80    error: &'static str,
81}
82
83pub async fn start_server(opts: ServerOptions) -> Result<ServerHandle> {
84    let state = Arc::new(Mutex::new(AppState {
85        opts: opts.clone(),
86        cache: None,
87        refresh_error: None,
88        refreshing: false,
89    }));
90
91    println!("🔗 {}", t("refreshing", &[]));
92    refresh_state(&state).await?;
93    println!(
94        "📡 {}\n",
95        t("http_start", &[("interval", &opts.interval_min.to_string())])
96    );
97
98    let refresh_state_clone = Arc::clone(&state);
99    let interval = opts.interval_min;
100    let refresh_handle = tokio::spawn(async move {
101        let mut ticker = tokio::time::interval(Duration::from_secs(interval * 60));
102        ticker.tick().await;
103        loop {
104            ticker.tick().await;
105            let _ = refresh_state(&refresh_state_clone).await;
106        }
107    });
108
109    let app_state = Arc::clone(&state);
110    let app = Router::new()
111        .route("/clash.yaml", get(clash_yaml))
112        .route("/health", get(health))
113        .route(
114            "/refresh",
115            post(refresh).get(refresh_method_not_allowed),
116        )
117        .layer(middleware::from_fn(add_cors))
118        .with_state(app_state);
119
120    let addr = SocketAddr::from(([127, 0, 0, 1], opts.port));
121    let listener = tokio::net::TcpListener::bind(addr)
122        .await
123        .map_err(|err| {
124            if err.kind() == std::io::ErrorKind::AddrInUse {
125                ProxyError::msg(t("port_in_use", &[("port", &opts.port.to_string())]))
126            } else {
127                ProxyError::Io(err)
128            }
129        })?;
130
131    print_banner(opts.port);
132
133    let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
134    let join_handle = tokio::spawn(async move {
135        axum::serve(listener, app)
136            .with_graceful_shutdown(async {
137                let _ = shutdown_rx.await;
138            })
139            .await
140            .ok();
141    });
142
143    Ok(ServerHandle {
144        shutdown_tx,
145        join_handle,
146        refresh_handle,
147    })
148}
149
150async fn refresh_state(state: &Arc<Mutex<AppState>>) -> Result<()> {
151    let mut guard = state.lock().await;
152    if guard.refreshing {
153        println!("  â­ī¸  {}", t("refresh_skip", &[]));
154        return Ok(());
155    }
156    guard.refreshing = true;
157    let opts = guard.opts.clone();
158    drop(guard);
159
160    let result = async {
161        println!(
162            "[{}] 🔄 {}",
163            chrono_like_time(),
164            t("refreshing", &[])
165        );
166        let lines = fetch_subscription(&opts.url).await?;
167        let mut nodes = parse_lines(&lines);
168        deduplicate_names(&mut nodes);
169        let config = build_config(&nodes, opts.rule_mode);
170        let yaml = to_yaml(&config)?;
171        let count = nodes.len();
172        Ok::<_, ProxyError>((yaml, count))
173    }
174    .await;
175
176    let mut guard = state.lock().await;
177    guard.refreshing = false;
178
179    match result {
180        Ok((yaml, count)) => {
181            guard.cache = Some(CacheEntry {
182                yaml,
183                node_count: count,
184                updated_at: now_ms(),
185            });
186            guard.refresh_error = None;
187            println!(
188                "  ✅ {}",
189                t("refresh_ok", &[("count", &count.to_string())])
190            );
191            Ok(())
192        }
193        Err(err) => {
194            let msg = err.to_string();
195            guard.refresh_error = Some(msg.clone());
196            if guard.cache.is_some() {
197                println!("  âš ī¸  {}", t("refresh_fail", &[("msg", &msg)]));
198                Ok(())
199            } else {
200                eprintln!("  ❌ {}", t("first_fetch_fail", &[("msg", &msg)]));
201                Err(err)
202            }
203        }
204    }
205}
206
207fn chrono_like_time() -> String {
208    let now = SystemTime::now()
209        .duration_since(UNIX_EPOCH)
210        .unwrap_or_default();
211    let secs = now.as_secs();
212    let h = (secs / 3600) % 24;
213    let m = (secs / 60) % 60;
214    let s = secs % 60;
215    format!("{h:02}:{m:02}:{s:02}")
216}
217
218fn now_ms() -> u64 {
219    SystemTime::now()
220        .duration_since(UNIX_EPOCH)
221        .unwrap_or_default()
222        .as_millis() as u64
223}
224
225fn iso_timestamp(ms: u64) -> String {
226    let secs = ms / 1000;
227    let datetime = time_from_unix(secs);
228    format!("{datetime}")
229}
230
231fn time_from_unix(secs: u64) -> String {
232    let days = secs / 86400;
233    let time = secs % 86400;
234    let h = time / 3600;
235    let m = (time % 3600) / 60;
236    let s = time % 60;
237
238    let mut year = 1970i64;
239    let mut remaining_days = days as i64;
240    loop {
241        let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
242        let year_days = if leap { 366 } else { 365 };
243        if remaining_days < year_days {
244            break;
245        }
246        remaining_days -= year_days;
247        year += 1;
248    }
249
250    let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
251    let month_days = if leap {
252        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
253    } else {
254        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
255    };
256
257    let mut month = 1u64;
258    for &md in &month_days {
259        if remaining_days < md as i64 {
260            break;
261        }
262        remaining_days -= md as i64;
263        month += 1;
264    }
265    let day = remaining_days + 1;
266
267    format!("{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}.000Z")
268}
269
270async fn add_cors(request: Request, next: Next) -> Response {
271    let mut response = next.run(request).await;
272    response.headers_mut().insert(
273        header::ACCESS_CONTROL_ALLOW_ORIGIN,
274        HeaderValue::from_static("*"),
275    );
276    response
277}
278
279async fn refresh_method_not_allowed() -> impl IntoResponse {
280    (StatusCode::METHOD_NOT_ALLOWED, "Method Not Allowed")
281}
282
283async fn clash_yaml(State(state): State<Arc<Mutex<AppState>>>) -> Response {
284    let guard = state.lock().await;
285    match &guard.cache {
286        Some(cache) => (
287            StatusCode::OK,
288            [
289                (header::CONTENT_TYPE, "text/yaml; charset=utf-8"),
290                (header::CACHE_CONTROL, "no-cache"),
291            ],
292            cache.yaml.clone(),
293        )
294            .into_response(),
295        None => (StatusCode::SERVICE_UNAVAILABLE, "Service unavailable").into_response(),
296    }
297}
298
299async fn health(State(state): State<Arc<Mutex<AppState>>>) -> Response {
300    let guard = state.lock().await;
301    let now = now_ms();
302    let next_refresh = match &guard.cache {
303        Some(cache) => {
304            let elapsed_min = (now.saturating_sub(cache.updated_at)) / 60_000;
305            guard.opts.interval_min.saturating_sub(elapsed_min)
306        }
307        None => guard.opts.interval_min,
308    };
309
310    let body = HealthResponse {
311        status: if guard.cache.is_some() {
312            "ok"
313        } else {
314            "initializing"
315        },
316        nodes: guard.cache.as_ref().map(|c| c.node_count).unwrap_or(0),
317        updated_at: guard
318            .cache
319            .as_ref()
320            .map(|c| iso_timestamp(c.updated_at)),
321        next_refresh_min: next_refresh,
322        last_error: guard.refresh_error.clone(),
323    };
324
325    (
326        StatusCode::OK,
327        [(header::CONTENT_TYPE, "application/json; charset=utf-8")],
328        Json(body),
329    )
330        .into_response()
331}
332
333async fn refresh(State(state): State<Arc<Mutex<AppState>>>) -> Response {
334    let skipped = {
335        let guard = state.lock().await;
336        guard.refreshing
337    };
338
339    if skipped {
340        let guard = state.lock().await;
341        let nodes = guard.cache.as_ref().map(|c| c.node_count).unwrap_or(0);
342        return (
343            StatusCode::OK,
344            [(header::CONTENT_TYPE, "application/json; charset=utf-8")],
345            Json(RefreshResponse {
346                ok: true,
347                skipped: true,
348                nodes,
349            }),
350        )
351            .into_response();
352    }
353
354    match refresh_state(&state).await {
355        Ok(()) => {
356            let guard = state.lock().await;
357            let nodes = guard.cache.as_ref().map(|c| c.node_count).unwrap_or(0);
358            (
359                StatusCode::OK,
360                [(header::CONTENT_TYPE, "application/json; charset=utf-8")],
361                Json(RefreshResponse {
362                    ok: true,
363                    skipped: false,
364                    nodes,
365                }),
366            )
367                .into_response()
368        }
369        Err(_) => (
370            StatusCode::INTERNAL_SERVER_ERROR,
371            [(header::CONTENT_TYPE, "application/json; charset=utf-8")],
372            Json(RefreshErrorResponse {
373                ok: false,
374                error: "Refresh failed",
375            }),
376        )
377            .into_response(),
378    }
379}
380
381fn print_banner(port: u16) {
382    let base = format!("http://127.0.0.1:{port}");
383    let c = BANNER_COLORS;
384    let title = format!(
385        "{}{}R{}I{}N{}O{}V{}A{}{}  {}{}",
386        c.b, c.g1, c.g2, c.g3, c.c1, c.c2, c.c3, c.rst, c.d, t("server_title", &[]), c.rst
387    );
388    println!();
389    println!("{}╔══════════════════════════════════════════════════╗{}", c.n2, c.rst);
390    println!("{}║{}  {}{}║{}", c.n2, c.rst, title, c.n2, c.rst);
391    println!("{}║{}                                              {}║{}", c.n2, c.rst, c.n2, c.rst);
392    println!(
393        "{}║{}  {}▸ {}{}{}/clash.yaml{}          {}║{}",
394        c.n2, c.rst, c.y, c.b, c.w, base, c.rst, c.n2, c.rst
395    );
396    println!(
397        "{}║{}  {}▸ {}{}{}{}{}║{}",
398        c.n2,
399        c.rst,
400        c.y,
401        c.d,
402        c.dim,
403        t("server_banner_clash", &[]),
404        "                            ",
405        c.n2,
406        c.rst
407    );
408    println!("{}║{}                                              {}║{}", c.n2, c.rst, c.n2, c.rst);
409    println!(
410        "{}║{}  {}▸ {}{}{}/health{}              {}║{}",
411        c.n2, c.rst, c.y, c.b, c.w, base, c.rst, c.n2, c.rst
412    );
413    println!(
414        "{}║{}  {}▸ {}{}{}{}{}║{}",
415        c.n2,
416        c.rst,
417        c.y,
418        c.d,
419        c.dim,
420        t("server_banner_health", &[]),
421        "                            ",
422        c.n2,
423        c.rst
424    );
425    println!("{}╚══════════════════════════════════════════════════╝{}\n", c.n2, c.rst);
426}
427
428struct BannerColors {
429    rst: &'static str,
430    b: &'static str,
431    d: &'static str,
432    g1: &'static str,
433    g2: &'static str,
434    g3: &'static str,
435    c1: &'static str,
436    c2: &'static str,
437    c3: &'static str,
438    n2: &'static str,
439    y: &'static str,
440    w: &'static str,
441    dim: &'static str,
442}
443
444impl BannerColors {
445    const fn new() -> Self {
446        Self {
447            rst: "\x1b[0m",
448            b: "\x1b[1m",
449            d: "\x1b[2m",
450            g1: "\x1b[38;5;82m",
451            g2: "\x1b[38;5;83m",
452            g3: "\x1b[38;5;84m",
453            c1: "\x1b[38;5;87m",
454            c2: "\x1b[38;5;117m",
455            c3: "\x1b[38;5;153m",
456            n2: "\x1b[38;5;45m",
457            y: "\x1b[38;5;228m",
458            w: "\x1b[38;5;255m",
459            dim: "\x1b[38;5;245m",
460        }
461    }
462}
463
464const BANNER_COLORS: BannerColors = BannerColors::new();
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469    use base64::Engine;
470    use std::sync::atomic::{AtomicU16, Ordering};
471
472    static TEST_PORT: AtomicU16 = AtomicU16::new(35500);
473
474    fn next_port_pair() -> (u16, u16) {
475        let base = TEST_PORT.fetch_add(2, Ordering::SeqCst);
476        (base, base + 1)
477    }
478
479    async fn start_mock_subscription(port: u16) -> tokio::task::JoinHandle<()> {
480        let proxy_data = base64::engine::general_purpose::STANDARD.encode(
481            "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#įžŽå›Ŋ-01\n\
482             trojan://pass@sg1.example.com:443?security=tls&sni=sg1.example.com#æ–°åŠ åĄ-01",
483        );
484
485        tokio::spawn(async move {
486            let app = Router::new().route(
487                "/sub",
488                get(|| async move { proxy_data.clone() }),
489            );
490            let addr = SocketAddr::from(([127, 0, 0, 1], port));
491            let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
492            axum::serve(listener, app).await.ok();
493        })
494    }
495
496    #[tokio::test]
497    async fn server_endpoints() {
498        let (sub_port, convert_port) = next_port_pair();
499        let _mock = start_mock_subscription(sub_port).await;
500        tokio::time::sleep(Duration::from_millis(200)).await;
501
502        let handle = start_server(ServerOptions {
503            url: format!("http://127.0.0.1:{sub_port}/sub"),
504            port: convert_port,
505            interval_min: 999,
506            rule_mode: RuleMode::Builtin,
507        })
508        .await
509        .unwrap();
510
511        tokio::time::sleep(Duration::from_millis(200)).await;
512
513        let health = reqwest::get(format!("http://127.0.0.1:{convert_port}/health"))
514            .await
515            .unwrap();
516        assert_eq!(health.status(), 200);
517        assert_eq!(
518            health.headers().get("access-control-allow-origin").unwrap(),
519            "*"
520        );
521        let body: serde_json::Value = health.json().await.unwrap();
522        assert_eq!(body["status"], "ok");
523        assert_eq!(body["nodes"], 2);
524
525        let yaml = reqwest::get(format!("http://127.0.0.1:{convert_port}/clash.yaml"))
526            .await
527            .unwrap()
528            .text()
529            .await
530            .unwrap();
531        assert!(yaml.contains("proxies:"));
532        assert!(yaml.contains("įžŽå›Ŋ-01"));
533
534        let yaml_with_query = reqwest::get(format!(
535            "http://127.0.0.1:{convert_port}/clash.yaml?t={}",
536            std::time::SystemTime::now()
537                .duration_since(std::time::UNIX_EPOCH)
538                .unwrap()
539                .as_millis()
540        ))
541        .await
542        .unwrap()
543        .text()
544        .await
545        .unwrap();
546        assert!(yaml_with_query.contains("įžŽå›Ŋ-01"));
547
548        let refresh = reqwest::Client::new()
549            .post(format!("http://127.0.0.1:{convert_port}/refresh"))
550            .send()
551            .await
552            .unwrap();
553        assert_eq!(refresh.status(), 200);
554        let body: serde_json::Value = refresh.json().await.unwrap();
555        assert_eq!(body["ok"], true);
556
557        let get_refresh = reqwest::get(format!("http://127.0.0.1:{convert_port}/refresh"))
558            .await
559            .unwrap();
560        assert_eq!(get_refresh.status(), 405);
561
562        let unknown = reqwest::get(format!("http://127.0.0.1:{convert_port}/unknown"))
563            .await
564            .unwrap();
565        assert_eq!(unknown.status(), 404);
566
567        handle.shutdown().await;
568    }
569}