tibba_router_common/
lib.rs1use axum::Json;
16use axum::Router;
17use axum::extract::State;
18use axum::http::header;
19use axum::response::IntoResponse;
20use axum::routing::get;
21use base64::{Engine, engine::general_purpose::STANDARD};
22use captcha::Captcha;
23use captcha::filters::{Noise, Wave};
24use serde::{Deserialize, Serialize};
25use std::io::Cursor;
26use std::time::Duration;
27use tibba_cache::RedisCache;
28use tibba_error::Error;
29use tibba_performance::get_process_system_info;
30use tibba_state::AppState;
31use tibba_util::{JsonResult, QueryParams, get_env, uuid};
32use validator::Validate;
33
34type Result<T> = std::result::Result<T, Error>;
35
36const ERROR_CATEGORY: &str = "common_router";
37
38async fn ping(State(state): State<&'static AppState>) -> Result<&'static str> {
40 if !state.is_running() {
41 return Err(Error::new("Server is not running")
42 .with_category(ERROR_CATEGORY)
43 .with_status(503));
44 }
45 Ok("pong")
46}
47
48#[derive(Debug, Clone, Serialize)]
49struct ApplicationInfo {
50 uptime: String,
51 env: String,
52 os: String,
53 arch: String,
54 commit_id: String,
55 hostname: String,
56 memory_usage_mb: u32,
57 cpu_usage: u32,
58 open_files: u32,
59 total_written_mb: u32,
60 total_read_mb: u32,
61 running: bool,
62}
63
64fn format_uptime_approx(duration: Duration) -> String {
65 humantime::format_duration(duration)
66 .to_string()
67 .split(' ')
68 .take(2)
69 .collect::<Vec<_>>()
70 .join(" ")
71}
72
73async fn get_application_info(
75 State(state): State<&'static AppState>,
76) -> JsonResult<ApplicationInfo> {
77 let uptime = state.get_started_at().elapsed().unwrap_or_default();
78 let info = os_info::get();
79 let os = info.os_type().to_string();
80 let arch = info.architecture().unwrap_or_default();
81 let performance = get_process_system_info(std::process::id() as usize);
82 let mb = 1024 * 1024;
83
84 let info = ApplicationInfo {
85 uptime: format_uptime_approx(uptime),
86 env: get_env().to_string(),
87 arch: arch.to_string(),
88 os,
89 commit_id: state.get_commit_id().to_string(),
90 hostname: hostname::get()
91 .unwrap_or_default()
92 .to_string_lossy()
93 .to_string(),
94 cpu_usage: performance.cpu_usage as u32,
95 memory_usage_mb: (performance.memory_usage / mb) as u32,
96 open_files: performance.open_files.unwrap_or_default() as u32,
97 total_written_mb: (performance.total_written_bytes / mb) as u32,
98 total_read_mb: (performance.total_read_bytes / mb) as u32,
99 running: state.is_running(),
100 };
101 Ok(Json(info))
102}
103
104#[derive(Debug, Deserialize, Clone, Validate)]
105pub struct CaptchaParams {
106 pub preview: Option<bool>,
107 pub theme: Option<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Default)]
111struct CaptchaInfo {
112 id: String,
113 data: String,
114}
115
116async fn captcha(
118 State(cache): State<&'static RedisCache>,
119 QueryParams(params): QueryParams<CaptchaParams>,
120) -> Result<impl IntoResponse> {
121 let is_dark = params.theme.unwrap_or_default() == "dark";
122 let (text, data) = {
124 let mut c = Captcha::new();
125 c.set_chars(&"123456789".chars().collect::<Vec<_>>())
127 .add_chars(4)
128 .apply_filter(Noise::new(0.4))
129 .apply_filter(Wave::new(2.0, 8.0).horizontal())
130 .apply_filter(Wave::new(2.0, 8.0).vertical())
131 .view(120, 38);
132 if is_dark {
133 c.set_color([60, 60, 60]);
134 }
135 let buffer = c.as_png().unwrap_or_default();
136 if is_dark {
137 let mut img = image::load_from_memory(&buffer)
138 .map_err(|e| Error::new(e.to_string()).with_exception(true))?;
139 img.invert();
140 let mut buffer: Vec<u8> = Vec::new();
141 img.write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png)
142 .map_err(|e| Error::new(e.to_string()).with_exception(true))?;
143 (c.chars_as_string(), buffer)
144 } else {
145 (c.chars_as_string(), buffer)
146 }
147 };
148
149 if params.preview.unwrap_or_default() {
150 let headers = [(header::CONTENT_TYPE, "image/png")];
151 return Ok((headers, data).into_response());
152 }
153
154 let mut info = CaptchaInfo {
155 data: STANDARD.encode(data),
156 ..Default::default()
157 };
158 let id = uuid();
159 cache
160 .set(&id, &text, Some(Duration::from_secs(5 * 60)))
161 .await?;
162 info.id = id;
163
164 Ok(Json(info).into_response())
165}
166
167pub struct CommonRouterParams {
168 pub state: &'static AppState,
169 pub cache: Option<&'static RedisCache>,
170 pub commit_id: String,
171}
172
173pub fn new_common_router(params: CommonRouterParams) -> Router {
174 let r = Router::new()
175 .route("/ping", get(ping).with_state(params.state))
176 .route(
177 "/commons/application",
178 get(get_application_info).with_state(params.state),
179 );
180 let Some(cache) = params.cache else {
181 return r;
182 };
183
184 r.route("/commons/captcha", get(captcha).with_state(cache))
185}