1use anyhow::{Context, Result};
2use axum::{
3 Extension, Json, Router,
4 body::Body,
5 extract::ConnectInfo,
6 http::{Response, StatusCode},
7 response::IntoResponse,
8 routing::{get, post},
9};
10use chrono::{DateTime, Utc};
11use datafusion::arrow::{
12 array::RecordBatch,
13 json::{Writer, writer::JsonArray},
14};
15use http::{HeaderMap, Uri};
16use micromegas_analytics::time::TimeRange;
17use micromegas_tracing::info;
18use serde::{Deserialize, Serialize};
19use std::net::SocketAddr;
20use std::sync::Arc;
21use thiserror::Error;
22use tonic::transport::{Channel, ClientTlsConfig};
23
24use crate::client::flightsql_client::Client;
25use crate::servers::http_utils;
26
27#[derive(Debug, Clone, Deserialize)]
29pub struct HeaderForwardingConfig {
30 pub allowed_headers: Vec<String>,
32
33 pub allowed_prefixes: Vec<String>,
35
36 pub blocked_headers: Vec<String>,
38}
39
40impl Default for HeaderForwardingConfig {
41 fn default() -> Self {
42 Self {
43 allowed_headers: vec![
45 "Authorization".to_string(),
46 "User-Agent".to_string(),
47 "X-Client-Type".to_string(),
48 "X-Correlation-ID".to_string(),
49 "X-Request-ID".to_string(),
50 "X-User-Email".to_string(),
51 "X-User-ID".to_string(),
52 "X-User-Name".to_string(),
53 ],
54 allowed_prefixes: vec![],
55 blocked_headers: vec![
56 "Cookie".to_string(),
57 "Set-Cookie".to_string(),
58 "X-Client-IP".to_string(),
60 ],
61 }
62 }
63}
64
65impl HeaderForwardingConfig {
66 pub fn from_env() -> Result<Self> {
68 if let Ok(config_json) = std::env::var("MICROMEGAS_GATEWAY_HEADERS") {
69 serde_json::from_str(&config_json).context("Failed to parse MICROMEGAS_GATEWAY_HEADERS")
70 } else {
71 Ok(Self::default())
72 }
73 }
74
75 pub fn should_forward(&self, header_name: &str) -> bool {
77 let name_lower = header_name.to_lowercase();
78
79 if self
81 .blocked_headers
82 .iter()
83 .any(|h| h.to_lowercase() == name_lower)
84 {
85 return false;
86 }
87
88 if self
90 .allowed_headers
91 .iter()
92 .any(|h| h.to_lowercase() == name_lower)
93 {
94 return true;
95 }
96
97 self.allowed_prefixes
99 .iter()
100 .any(|prefix| name_lower.starts_with(&prefix.to_lowercase()))
101 }
102}
103
104#[derive(Debug, Clone)]
108pub struct GatewayConfig {
109 pub flight_url: Uri,
110 pub headers: HeaderForwardingConfig,
111}
112
113impl GatewayConfig {
114 pub fn new(flight_url: Uri) -> Result<Self> {
116 Ok(Self {
117 flight_url,
118 headers: HeaderForwardingConfig::from_env()?,
119 })
120 }
121}
122
123#[derive(Error, Debug)]
124pub enum GatewayError {
125 #[error("Bad request: {0}")]
126 BadRequest(String),
127
128 #[error("Unauthorized: {0}")]
129 Unauthorized(String),
130
131 #[error("Forbidden: {0}")]
132 Forbidden(String),
133
134 #[error("Service unavailable: {0}")]
135 ServiceUnavailable(String),
136
137 #[error("Internal server error: {0}")]
138 Internal(String),
139}
140
141impl IntoResponse for GatewayError {
142 fn into_response(self) -> Response<Body> {
143 let (status, message) = match self {
144 GatewayError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
145 GatewayError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
146 GatewayError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg),
147 GatewayError::ServiceUnavailable(msg) => (StatusCode::SERVICE_UNAVAILABLE, msg),
148 GatewayError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
149 };
150 (status, message).into_response()
151 }
152}
153
154#[derive(Debug, Deserialize)]
155pub struct QueryRequest {
156 sql: String,
157 #[serde(default)]
160 time_range_begin: Option<String>,
161 #[serde(default)]
164 time_range_end: Option<String>,
165}
166
167pub fn build_origin_metadata(
179 headers: &HeaderMap,
180 addr: &SocketAddr,
181) -> tonic::metadata::MetadataMap {
182 let mut metadata = tonic::metadata::MetadataMap::new();
183
184 let original_client_type = headers
186 .get("x-client-type")
187 .and_then(|v| v.to_str().ok())
188 .unwrap_or("unknown");
189 let augmented_client_type = format!("{original_client_type}+gateway");
190 if let Ok(value) = augmented_client_type.parse() {
191 metadata.insert("x-client-type", value);
192 }
193
194 let request_id = headers
196 .get("x-request-id")
197 .and_then(|v| v.to_str().ok())
198 .map(|s| s.to_string())
199 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
200 if let Ok(value) = request_id.parse() {
201 metadata.insert("x-request-id", value);
202 }
203
204 let mut extensions = http::Extensions::new();
207 extensions.insert(axum::extract::ConnectInfo(*addr));
208 let client_ip = http_utils::get_client_ip(headers, &extensions);
209 if let Ok(value) = client_ip.parse() {
210 metadata.insert("x-client-ip", value);
211 }
212
213 metadata
214}
215
216pub async fn handle_query(
217 Extension(config): Extension<Arc<GatewayConfig>>,
218 ConnectInfo(addr): ConnectInfo<SocketAddr>,
219 headers: HeaderMap,
220 Json(request): Json<QueryRequest>,
221) -> Result<String, GatewayError> {
222 let start_time = std::time::Instant::now();
223
224 let origin_metadata = build_origin_metadata(&headers, &addr);
226 let client_type_header = origin_metadata
227 .get("x-client-type")
228 .and_then(|v| v.to_str().ok())
229 .unwrap_or("unknown+gateway");
230 let request_id_header = origin_metadata
231 .get("x-request-id")
232 .and_then(|v| v.to_str().ok())
233 .unwrap_or("unknown");
234
235 let sql = request.sql.trim();
237 if sql.is_empty() {
238 return Err(GatewayError::BadRequest(
239 "SQL query cannot be empty".to_string(),
240 ));
241 }
242
243 const MAX_SQL_SIZE: usize = 1_048_576;
245 if sql.len() > MAX_SQL_SIZE {
246 return Err(GatewayError::BadRequest(format!(
247 "SQL query too large: {} bytes (max: {} bytes)",
248 sql.len(),
249 MAX_SQL_SIZE
250 )));
251 }
252
253 let time_range = match (&request.time_range_begin, &request.time_range_end) {
255 (Some(begin_str), Some(end_str)) => {
256 let begin = DateTime::parse_from_rfc3339(begin_str)
257 .map_err(|e| {
258 GatewayError::BadRequest(format!(
259 "Invalid time_range_begin format (expected RFC3339): {e}"
260 ))
261 })?
262 .with_timezone(&Utc);
263 let end = DateTime::parse_from_rfc3339(end_str)
264 .map_err(|e| {
265 GatewayError::BadRequest(format!(
266 "Invalid time_range_end format (expected RFC3339): {e}"
267 ))
268 })?
269 .with_timezone(&Utc);
270
271 if begin > end {
272 return Err(GatewayError::BadRequest(
273 "time_range_begin must be before time_range_end".to_string(),
274 ));
275 }
276
277 Some(TimeRange::new(begin, end))
278 }
279 (Some(_), None) => {
280 return Err(GatewayError::BadRequest(
281 "time_range_end must be provided when time_range_begin is specified".to_string(),
282 ));
283 }
284 (None, Some(_)) => {
285 return Err(GatewayError::BadRequest(
286 "time_range_begin must be provided when time_range_end is specified".to_string(),
287 ));
288 }
289 (None, None) => None,
290 };
291
292 info!(
293 "Gateway request: request_id={}, client_type={}, time_range={:?}, sql={}",
294 request_id_header, client_type_header, time_range, sql
295 );
296
297 let tls_config = ClientTlsConfig::new().with_native_roots();
298 let channel = Channel::builder(config.flight_url.clone())
299 .tls_config(tls_config)
300 .map_err(|e| GatewayError::Internal(format!("TLS config error: {e}")))?
301 .connect()
302 .await
303 .map_err(|e| {
304 GatewayError::ServiceUnavailable(format!("Failed to connect to FlightSQL: {e}"))
305 })?;
306
307 let mut client = Client::new(channel);
309
310 client
311 .inner_mut()
312 .set_header("x-client-type", client_type_header);
313 client
314 .inner_mut()
315 .set_header("x-request-id", request_id_header);
316
317 if let Some(client_ip) = origin_metadata.get("x-client-ip")
318 && let Ok(ip_str) = client_ip.to_str()
319 {
320 client.inner_mut().set_header("x-client-ip", ip_str);
321 }
322
323 for (name, value) in headers.iter() {
325 let header_name = name.as_str();
326
327 if header_name.eq_ignore_ascii_case("x-client-type")
329 || header_name.eq_ignore_ascii_case("x-request-id")
330 || header_name.eq_ignore_ascii_case("x-client-ip")
331 {
332 continue; }
334
335 if config.headers.should_forward(header_name)
336 && let Ok(value_str) = value.to_str()
337 {
338 client.inner_mut().set_header(header_name, value_str);
339 }
340 }
341
342 let batches = client
344 .query(sql.to_string(), time_range)
345 .await
346 .map_err(|e| {
347 if let Some(status) = e.downcast_ref::<tonic::Status>() {
349 match status.code() {
350 tonic::Code::Unauthenticated => {
351 GatewayError::Unauthorized(status.message().to_string())
352 }
353 tonic::Code::PermissionDenied => {
354 GatewayError::Forbidden(status.message().to_string())
355 }
356 tonic::Code::InvalidArgument => {
357 GatewayError::BadRequest(status.message().to_string())
358 }
359 tonic::Code::Unavailable => {
360 GatewayError::ServiceUnavailable(status.message().to_string())
361 }
362 _ => GatewayError::Internal(format!("Query failed: {}", status.message())),
363 }
364 } else {
365 GatewayError::Internal(format!("Query execution error: {e:?}"))
366 }
367 })?;
368
369 let elapsed = start_time.elapsed();
370 info!(
371 "Gateway request completed: request_id={}, duration={:?}",
372 request_id_header, elapsed
373 );
374
375 if batches.is_empty() {
376 return Ok("[]".to_string());
377 }
378
379 let mut buffer = Vec::new();
380 let mut json_writer = Writer::<_, JsonArray>::new(&mut buffer);
381 let batch_refs: Vec<&RecordBatch> = batches.iter().collect();
382 json_writer
383 .write_batches(&batch_refs)
384 .map_err(|e| GatewayError::Internal(format!("Failed to serialize results: {e}")))?;
385 json_writer
386 .finish()
387 .map_err(|e| GatewayError::Internal(format!("Failed to finish JSON output: {e}")))?;
388
389 String::from_utf8(buffer)
390 .map_err(|e| GatewayError::Internal(format!("Invalid UTF-8 in results: {e}")))
391}
392
393#[derive(Debug, Serialize)]
394pub struct HealthResponse {
395 pub status: &'static str,
396 pub timestamp: DateTime<Utc>,
397}
398
399pub async fn handle_health() -> Json<HealthResponse> {
400 Json(HealthResponse {
401 status: "healthy",
402 timestamp: Utc::now(),
403 })
404}
405
406pub fn register_routes(router: Router) -> Router {
407 router
408 .route("/gateway/query", post(handle_query))
409 .route("/gateway/health", get(handle_health))
410}