tiny_proxy/api/
endpoints.rs1use anyhow::Result;
4use arc_swap::ArcSwap;
5use bytes::Bytes;
6use http_body::Body;
7use http_body_util::{BodyExt, Full};
8use hyper::{Request, Response};
9use std::sync::Arc;
10use tracing::{error, info};
11
12use crate::config::Config;
13
14pub async fn handle_get_config<B>(
16 _req: Request<B>,
17 config: Arc<ArcSwap<Config>>,
18) -> Result<Response<Full<Bytes>>>
19where
20 B: Body,
21{
22 let config = config.load_full();
23
24 let json = serde_json::to_string_pretty(&*config)
25 .unwrap_or_else(|_| r#"{"error": "Failed to serialize config"}"#.to_string());
26
27 info!("GET /config - Returning configuration");
28
29 let response = Response::builder()
30 .status(200)
31 .header("Content-Type", "application/json")
32 .body(Full::new(Bytes::from(json)))
33 .expect("static response build");
34
35 Ok(response)
36}
37
38pub async fn handle_post_config<B>(
60 req: Request<B>,
61 config: Arc<ArcSwap<Config>>,
62) -> Result<Response<Full<Bytes>>>
63where
64 B: Body,
65 B::Error: std::fmt::Display,
66{
67 let body_bytes = match BodyExt::collect(req.into_body()).await {
68 Ok(collected) => collected.to_bytes(),
69 Err(e) => {
70 let error_json = serde_json::json!({
71 "status": "error",
72 "message": format!("Failed to read request body: {}", e)
73 });
74 let response = Response::builder()
75 .status(400)
76 .header("Content-Type", "application/json")
77 .body(Full::new(Bytes::from(
78 serde_json::to_string(&error_json).expect("json!() is always valid"),
79 )))
80 .expect("static response build");
81 return Ok(response);
82 }
83 };
84
85 let body_str = match std::str::from_utf8(&body_bytes) {
86 Ok(s) => s,
87 Err(_) => {
88 let error_json = serde_json::json!({
89 "status": "error",
90 "message": "Invalid UTF-8 in request body"
91 });
92 let response = Response::builder()
93 .status(400)
94 .header("Content-Type", "application/json")
95 .body(Full::new(Bytes::from(
96 serde_json::to_string(&error_json).expect("json!() is always valid"),
97 )))
98 .expect("static response build");
99 return Ok(response);
100 }
101 };
102
103 let new_config: Config = match serde_json::from_str(body_str) {
105 Ok(config) => config,
106 Err(e) => {
107 error!("Failed to parse config JSON: {}", e);
108 let error_json = serde_json::json!({
109 "status": "error",
110 "message": format!("Invalid configuration JSON: {}", e)
111 });
112 let response = Response::builder()
113 .status(400)
114 .header("Content-Type", "application/json")
115 .body(Full::new(Bytes::from(
116 serde_json::to_string(&error_json).expect("json!() is always valid"),
117 )))
118 .expect("static response build");
119 return Ok(response);
120 }
121 };
122
123 {
125 let sites_count = new_config.sites.len();
126 config.store(Arc::new(new_config));
127 info!(
128 "POST /config - Configuration updated successfully ({} sites)",
129 sites_count
130 );
131 }
132
133 let response = Response::builder()
134 .status(200)
135 .header("Content-Type", "application/json")
136 .body(Full::new(Bytes::from(
137 r#"{"status": "success", "message": "Configuration updated"}"#.to_string(),
138 )))
139 .expect("static response build");
140
141 Ok(response)
142}
143
144pub async fn handle_health_check<B>(_req: Request<B>) -> Result<Response<Full<Bytes>>>
146where
147 B: Body,
148{
149 info!("GET /health - Health check");
150
151 let health = serde_json::json!({
152 "status": "healthy",
153 "service": "tiny-proxy",
154 "version": env!("CARGO_PKG_VERSION")
155 });
156
157 let response = Response::builder()
158 .status(200)
159 .header("Content-Type", "application/json")
160 .body(Full::new(Bytes::from(
161 serde_json::to_string(&health).expect("json!() is always valid"),
162 )))
163 .expect("static response build");
164
165 Ok(response)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use http_body_util::Empty;
172 use std::collections::HashMap;
173 use std::sync::Arc;
174
175 #[tokio::test]
176 async fn test_handle_health_check() {
177 let req: Request<Empty<Bytes>> = Request::builder().body(Empty::new()).unwrap();
178
179 let response = handle_health_check(req).await.unwrap();
180 assert_eq!(response.status(), 200);
181 }
182
183 #[tokio::test]
184 async fn test_handle_get_config() {
185 let config = Arc::new(ArcSwap::from_pointee(Config {
186 sites: HashMap::new(),
187 }));
188
189 let req: Request<Empty<Bytes>> = Request::builder().body(Empty::new()).unwrap();
190
191 let response = handle_get_config(req, config).await.unwrap();
192 assert_eq!(response.status(), 200);
193 }
194
195 #[tokio::test]
196 async fn test_handle_post_config_valid_json() {
197 let config = Arc::new(ArcSwap::from_pointee(Config {
198 sites: HashMap::new(),
199 }));
200
201 let new_config_json = r#"{
202 "sites": {
203 "localhost:8080": {
204 "address": "localhost:8080",
205 "directives": [
206 {"ReverseProxy": {"to": "localhost:9001"}}
207 ]
208 }
209 }
210 }"#;
211
212 let req = Request::builder()
213 .method("POST")
214 .uri("/config")
215 .body(Full::new(Bytes::from(new_config_json.to_string())))
216 .unwrap();
217
218 let response = handle_post_config(req, config.clone()).await.unwrap();
219 assert_eq!(response.status(), 200);
220
221 let guard = config.load_full();
223 assert_eq!(guard.sites.len(), 1);
224 assert!(guard.sites.contains_key("localhost:8080"));
225 }
226
227 #[tokio::test]
228 async fn test_handle_post_config_invalid_json() {
229 let config = Arc::new(ArcSwap::from_pointee(Config {
230 sites: HashMap::new(),
231 }));
232
233 let req = Request::builder()
234 .method("POST")
235 .uri("/config")
236 .body(Full::new(Bytes::from("not valid json")))
237 .unwrap();
238
239 let response = handle_post_config(req, config.clone()).await.unwrap();
240 assert_eq!(response.status(), 400);
241
242 let guard = config.load_full();
244 assert_eq!(guard.sites.len(), 0);
245 }
246
247 #[tokio::test]
248 async fn test_handle_post_config_empty_body() {
249 let config = Arc::new(ArcSwap::from_pointee(Config {
250 sites: HashMap::new(),
251 }));
252
253 let req: Request<Empty<Bytes>> = Request::builder()
254 .method("POST")
255 .uri("/config")
256 .body(Empty::new())
257 .unwrap();
258
259 let response = handle_post_config(req, config).await.unwrap();
260 assert_eq!(response.status(), 400);
261 }
262}