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}